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

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

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

Еще одной проблемой является сложность маппинга данных из базы данных в объекты 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.