Работа с базами данных в Rust с помощью Diesel
Привет, Хабр!
Сегодня мы поговорим о Diesel ORM — инструменте, который превращает работу с базами данных в Rust в настоящее удовольствие. Diesel ORM — это расширяемый и безопасный объектно-реляционный маппер и конструктор запросов для Rust. Он имеет высокоуровневый API для взаимодействия с различными СУБД: PostgreSQL, MySQL и SQLite.
Начнем с установки и настройки!
Установка / настройка
Создаем новый проект на Rust:
cargo new my_diesel_project
cd my_diesel_project
Открываем файлик Cargo.toml
проекта и добавляем зависимости для Diesel и необходимых библиотек:
[dependencies]
diesel = { version = "2.0.0", features = ["postgres"] }
dotenv = "0.15"
Файл указывает Cargo установить Diesel ORM с поддержкой PostgreSQL (выбрали постгрес для примера), а также библиотеку dotenv
. Также допустим то, что Postqre уже установлен.
Для работы с Diesel также нужен Diesel CLI:
sudo apt-get update
sudo apt-get install postgresql postgresql-contrib
Это установит Diesel CLI с поддержкой PostgreSQL. Далее, инициализируем Diesel в проекте:
brew install postgresql
brew services start postgresql
Команда создаст файл diesel.toml
и папку migrations
в проекте. Теперь создаем файл .env
в корне вашего проекта и добавляем в него строку подключения к БД:
sudo -u postgres createuser myuser -s
sudo -u postgres createdb mydatabase
Теперь создаем файлик src/schema.rs
для хранения схемы БД:
touch src/schema.rs
В main.rs
подключаем библиотеки Diesel и dotenv:
#[macro_use]
extern crate diesel;
extern crate dotenv;
pub mod schema;
pub mod models;
use diesel::prelude::*;
use dotenv::dotenv;
use std::env;
pub fn establish_connection() -> PgConnection {
dotenv().ok();
let database_url = env::var("DATABASE_URL")
.expect("DATABASE_URL must be set");
PgConnection::establish(&database_url)
.expect(&format!("Error connecting to {}", database_url))
}
Так мы загрузили переменные окружения из файла .env
, устанавили соединение с базой данных и возвращаем его.
CRUD операции
Создание
Создаем файл src/models.rs
и определяем структуру модели данных:
#[derive(Queryable)]
pub struct Post {
pub id: i32,
pub title: String,
pub body: String,
pub published: bool,
}
#[derive(Insertable)]
#[table_name="posts"]
pub struct NewPost<'a> {
pub title: &'a str,
pub body: &'a str,
}
В src/schema.rs
добавляем схему таблицы:
table! {
posts (id) {
id -> Int4,
title -> Varchar,
body -> Text,
published -> Bool,
}
}
Для вставки новых данных, в src/lib.rs
создадим функцию для вставки новых записей:
use diesel::prelude::*;
use diesel::pg::PgConnection;
use crate::schema::posts;
use crate::models::{Post, NewPost};
pub fn create_post<'a>(conn: &PgConnection, title: &'a str, body: &'a str) -> Post {
let new_post = NewPost {
title,
body,
};
diesel::insert_into(posts::table)
.values(&new_post)
.get_result(conn)
.expect("Error saving new post")
}
Чтение
Для чтения данных создадим функцию в src/lib.rs
:
pub fn get_posts(conn: &PgConnection) -> Vec {
use crate::schema::posts::dsl::*;
posts
.load::(conn)
.expect("Error loading posts")
}
Например, фильтрация опубликованных постов и сортировка по ID выглядит так:
pub fn get_published_posts(conn: &PgConnection) -> Vec {
use crate::schema::posts::dsl::*;
posts
.filter(published.eq(true))
.order(id.desc())
.load::(conn)
.expect("Error loading published posts")
}
Обновление и удаление
Для обновления записи создадим функцию:
pub fn update_post_title(conn: &PgConnection, post_id: i32, new_title: &str) -> Post {
use crate::schema::posts::dsl::{posts, id, title};
diesel::update(posts.find(post_id))
.set(title.eq(new_title))
.get_result::(conn)
.expect("Error updating post title")
}
Для удаления записи есть такая функция:
pub fn delete_post(conn: &PgConnection, post_id: i32) -> usize {
use crate::schema::posts::dsl::posts;
diesel::delete(posts.find(post_id))
.execute(conn)
.expect("Error deleting post")
}
Миграции
Миграции в Diesel очень удобны и легки в использовании.
Чтобы создать новую миграцию есть команда migration:
diesel migration generate create_posts
Команда создаст новую миграцию с именем create_posts
и создаст две SQL файла: up.sql
и down.sql
в папке migrations
.
В файле up.sql
определим SQL команды для создания таблицы. Например, для создания таблицы posts
:
CREATE TABLE posts (
id SERIAL PRIMARY KEY,
title VARCHAR NOT NULL,
body TEXT NOT NULL,
published BOOLEAN NOT NULL DEFAULT 'f'
);
В файле down.sql
определим SQL команды для отката изменений, например, удаления таблицы posts
:
DROP TABLE posts;
Для применения миграций используем команду:
diesel migration run
Команда выполнит все миграции, которые еще не были применены, и обновит структуру базы данных.
Если хочется откатить последнюю миграцию есть такая команда:
diesel migration revert
Эта команда выполнит команды из файла down.sql
последней миграции и вернет базу данных в предыдущее состояние.
Когда миграции применены, Diesel автоматом обновляет файл src/schema.rs
, который содержит описание схемы базы данных в виде Rust кода. Этот файл генерируется Diesel CLI и не должен изменяться вручную.
Обработка ошибок
В Rust тип Result
используется для обработки операций, которые могут завершиться ошибкой. Этот тип либо содержит значение успешной операции Ok
, либо описание ошибки Err
). В Diesel ORM этот механизм используется довольно часто.
Рассмотрим функцию, которая извлекает пост из базы данных по его ID:
use diesel::prelude::*;
use crate::models::Post;
use crate::schema::posts::dsl::*;
pub fn find_post_by_id(conn: &PgConnection, post_id: i32) -> Result {
posts.find(post_id).first(conn)
}
Функция возвращает Result
, т.е она либо успешно возвращает объект Post
, либо ошибку типа diesel::result::Error
.
При вызове этой функции, можно использовать выражение match
для обработки возможных ошибок:
fn main() {
let connection = establish_connection();
match find_post_by_id(&connection, 1) {
Ok(post) => println!("Found post: {}", post.title),
Err(err) => println!("Error finding post: {:?}", err),
}
}
Так можно обрабатывать ошибки на месте вызова функции.
Тип Option
в Rust используется для обработки случаев, когда значение может быть отсутствующим. В контексте БД, это, например, может быть полезно для операций, которые могут не вернуть ни одной строки:
pub fn find_post_by_title(conn: &PgConnection, post_title: &str) -> Option {
posts.filter(title.eq(post_title)).first(conn).ok()
}
Функция возвращает Option
, что означает, что она либо возвращает объект Post
, либо None
, если пост с указанным заголовком не найден.
Diesel ORM помимо вышеописанных функций автоматом использует параметризованные запросы. Например, напишем функцию, которая выполняет параметризованный запрос для поиска постов по заголовку:
pub fn search_posts_by_title(conn: &PgConnection, search: &str) -> Vec {
posts.filter(title.like(format!("%{}%", search)))
.load::(conn)
.expect("Error loading posts")
}
Метод filter
автоматически использует параметры вместо прямой вставки значений в SQL-запрос, что предотвращает возможность SQL-инъекций.
Когда работаешь с пользовательским вводом, всегда важно проверять и очищать данные. Например:
pub fn create_post(conn: &PgConnection, new_title: &str, new_body: &str) -> Post {
let new_post = NewPost {
title: new_title.trim(),
body: new_body.trim(),
};
diesel::insert_into(posts::table)
.values(&new_post)
.get_result(conn)
.expect("Error saving new post")
}
С методом trim()
можно удалять лишние пробелы из пользовательского ввода.
Подробнее с Diesel можно ознакомиться здесь.
А с другими языками программирования инструментами можно ознакомиться в рамках практических онлайн-курсов от моих друзей из OTUS. Подробнее в каталоге.