
Привет, Хабр!
Сегодня мы поговорим о 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<Post> { use crate::schema::posts::dsl::*; posts .load::<Post>(conn) .expect("Error loading posts") }
Например, фильтрация опубликованных постов и сортировка по ID выглядит так::
pub fn get_published_posts(conn: &PgConnection) -> Vec<Post> { use crate::schema::posts::dsl::*; posts .filter(published.eq(true)) .order(id.desc()) .load::<Post>(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::<Post>(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<Post, diesel::result::Error> { posts.find(post_id).first(conn) }
Функция возвращает Result<Post, diesel::result::Error>, т.е она либо успешно возвращает объект 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<Post> { posts.filter(title.eq(post_title)).first(conn).ok() }
Функция возвращает Option<Post>, что означает, что она либо возвращает объект Post, либо None, если пост с указанным заголовком не найден.
Diesel ORM помимо вышеописанных функций автоматом использует параметризованные запросы. Например, напишем функцию, которая выполняет параметризованный запрос для поиска постов по заголовку:
pub fn search_posts_by_title(conn: &PgConnection, search: &str) -> Vec<Post> { posts.filter(title.like(format!("%{}%", search))) .load::<Post>(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. Подробнее в каталоге.
