Разрабатываем свою ORM библиотеку на Rust: Оптимизация и Простота

Kate

Administrator
Команда форума
Почему я решил разработать свою ORM библиотеку?

Мои первые шаги в мире ORM были сделаны с помощью библиотеки Diesel. В то время он был одним из немногих вариантов для работы с базами данных на Rust, и, конечно же, его популярность не оставила меня равнодушным. Вскоре, однако, я обратил внимание на SeaORM - другую перспективную библиотеку для ORM на Rust, которая также набирала обороты. Но у меня были с ними некоторые проблемы.

115d419159367698a6b9d85817433587.png

Во-первых, я считаю, что несмотря на свою популярность и функциональность, обе библиотеки, на мой взгляд, имеют некоторые недостатки. Главная проблема - избыточный код. Для начала работы с обеими библиотеками требуется много дополнительных строк кода. Да, это может быть частью цены за высокий уровень абстракции, но часто это кажется излишним.

Еще одной проблемой является сложность маппинга данных из базы данных в объекты Rust. В Diesel и SeaORM это часто требует не только описания структур данных, но и создания схем или использования кодо-генераторов.

Во-вторых, мне не нравится, что эти библиотеки пытаются избавиться от написания SQL-запросов. В итоге, чтобы получить какой-то простой запрос, приходится писать длинные цепочки вызовов функций.

Главную идею, которую я заложил в свою ORM библиотеку, это минимум тупого кода и легкое использование библиотеки. Я хотел, чтобы пользователям не приходилось писать длинные цепочки вызовов функций, чтобы сконструировать простой SQL-запрос.

Для того чтобы упростить доступ к названиям полям структуры и избежать рутинной работы, связанной с сериализацией и десериализацией, я реализовал собственный derive макрос. Этот макрос генерирует необходимые методы для доступа к полям структуры.

Взглянем на пример:

#[derive(TableDeserialize, TableSerialize, Serialize, Deserialize, Debug, Clone)]
#[table(name = "user")]
pub struct User {
pub id: i32,
pub name: Option<String>,
pub age: i32,
}
Чтобы сериализовать и десерилизовать значение полей структуры в SQL-запросы, я реализовал свои собственные сериализаторы и десериализаторы, используя библиотеку Serde.

В процессе разработки собственной ORM библиотеки на Rust я столкнулся с необходимостью реализовать базовый набор CRUD операций для работы с данными.

В итоге я реализовал CRUD операции функциями insert, find_one, update, delete. Вот пример их использования:

let mut user = User {
id: 0,
name: Some("John".to_string()),
age: 30,
};

let mut user_from_db: User = conn.insert(user.clone()).apply().await?;
user_from_db.name = Some("Mary".to_string());
let query_where = format!("id = {}", user_from_db.id);
let user_opt: Option<User> = conn.find_one(query_where.as_str()).run().await?;
let updated_rows: usize = conn.update(user_from_db, query_where.as_str()).run().await?;
Такой подход позволяет избежать написания SQL-запросов вручную и сосредоточиться на бизнес-логике приложения.

Также я реализовал функции find_many и find_all, чтобы через SELECT-запрос получить объекты структуры из базы данных. При этом можно добавить в вызов функции find_many условие выборки просто строкой:

id=1 and balance > 10 order by balance
Вот пример их использования:

let users: Vec<User> = conn.find_all().run().await?;
let users: Vec<User> = conn.find_many("id > 0").limit(2).run().await?;
Такая реализация позволяет гибко формировать запросы к базе данных, указывая нужные условия отбора и сортировки результатов.

Также мной была реализована возможность посылать произвольные SQL-запросы напрямую в базу данных через функции query и query_update. Это позволяет выполнять сложные запросы, которые сложно или невозможно реализовать через объектную модель ORM.

Например, так можно выполнить произвольный select-запрос с подстановкой параметров:

let query = format!("select * from user where name like {}", conn.protect("%oh%"));
let result_set: Vec<Row> = conn.query(query.as_str()).exec().await?;
for row in result_set {
let id: i32 = row.get(0).unwrap();
let name: Option<String> = row.get(1);
log::debug!("User = id: {}, name: {:?}", id, name);
}
Аналогично можно выполнить update/delete запрос:

let updated_rows = conn.query_update("delete from user").exec().await?;
В заключение хочу отметить, что разработка собственной ORM библиотеки - это интересный опыт, позволяющий глубже понять принципы работы объектно-реляционного отображения.

Главной целью при создании моей библиотеки было добиться простоты использования и оптимального баланса между объектной моделью и возможностью написания низкоуровневых SQL-запросов.

Благодаря подходу с минимумом "магии" и использованием сериализаторов я добился компактного и понятного кода для основных CRUD операций. При этом сохранена гибкость при построении произвольных запросов напрямую через SQL.

Надеюсь, представленный подход будет полезен для разработчиков, ищущих простое и эффективное решение для доступа к базам данных в языке Rust. Код библиотеки открыт и доступен для изучения и улучшений сообществом.

Чтобы подключить библиотеку в свой проект, нужно добавить в Cargo.toml:

[dependencies]
ormlib = "0.2.3"
ormlib_derive = "0.2.3"
Код библиотеки доступен на GitHub по ссылке https://github.com/evgenyigumnov/orm-lib.

 
Сверху