Экспериментальная типизированная ORM для Rust. Сейчас рабочий runtime-backend —
SQLite через deadpool-sqlite, а строки декодируются обратно в модели,
сгенерированные procedural macro.
#[derive(Model)]для генерации метаданных модели и typed fields.- SQL AST и параметризованная генерация
SELECT,INSERT,UPDATE,DELETE. - Генерация
CREATE TABLE, индексов и ограничений. - Async SQLite pool через
deadpool-sqliteи Tokio. - High-level
get,all,create,update,deleteи typedqueryfacade. - Низкоуровневые
insert,fetch_all,fetch_oneи произвольныйQueryAst. - Транзакции с commit/rollback.
- Foreign keys с каскадным удалением и typed
fetch_byдля связанных строк. - Versioned migrations через Atlas и встроенный
manageCLI. - Разделение моделей по application-модулям через
AppConfigиAppRegistry. OffsetDateTimeи managed-поляauto_now_add/auto_now.- Защита от пустых mutation-запросов и случайных массовых update/delete.
Проект находится в разработке. PostgreSQL executor пока не реализован.
Добавьте зависимость:
[dependencies]
che-orm2 = { path = "../che-orm2" }
tokio = { version = "1", features = ["macros", "rt-multi-thread"] }
time = { version = "0.3", features = ["formatting", "parsing"] }Опишите модель:
use che_orm2::Model;
use time::OffsetDateTime;
#[derive(Debug, Model)]
#[orm(table = "users", index("name"))]
struct User {
#[orm(primary_key)]
id: i64,
#[orm(unique)]
email: String,
name: String,
is_active: bool,
#[orm(auto_now_add)]
created_at: OffsetDateTime,
#[orm(auto_now)]
updated_at: OffsetDateTime,
}Используйте модель через async SQLite database:
use che_orm2::Database;
#[tokio::main]
async fn main() -> Result<(), che_orm2::OrmError> {
let database = Database::connect("app.db")?;
database.create_table::<User>().await?;
let user = database
.create::<User>()
.set(User::EMAIL, "alice@example.test")
.set(User::NAME, "Alice")
.set(User::IS_ACTIVE, true)
.execute()
.await?;
let users = database
.query::<User>()
.filter(User::NAME.eq("Alice"))
.all(&database)
.await?;
let loaded = database.get::<User>(user.id).await?;
println!("{users:?}");
println!("{loaded:?}");
Ok(())
}Managed timestamp-поля не передаются в INSERT: их значения создаёт SQLite.
updated_at автоматически добавляется ORM в каждый update-запрос.
Для фиксированного набора строк используйте DbEnum. Одно значение variant используется в JSON,
SQLite и schema metadata:
use che_orm2::{DbEnum, Model};
#[derive(Debug, Clone, Copy, DbEnum)]
enum TaskStatus {
Draft,
#[db_enum(rename = "in_progress")]
InProgress,
Done,
}
#[derive(Debug, Model)]
#[orm(table = "tasks")]
struct Task {
#[orm(primary_key)]
id: i64,
status: TaskStatus,
}DbEnum generates Serialize and Deserialize; do not derive the serde traits separately. It
stores values as TEXT and emits a CHECK constraint for the declared choices.
По умолчанию создаётся пул размером 4:
let database = Database::connect("app.db")?;В application binary можно использовать единый путь из settings:
let database = Database::connect_configured()?;Размер можно задать явно:
let database = Database::connect_with_pool_size("app.db", 8)?;Для in-memory SQLite используется пул размера 1:
let database = Database::connect_in_memory()?;Это важно: разные SQLite-соединения с :memory: имеют разные базы данных.
Основной facade автоматически ограничивает update/delete primary key:
let updated = database
.update::<User>(user.id)
.set(User::NAME, "Alice Cooper")
.execute()
.await?;
let deleted = database.delete::<User>(user.id).await?;
assert!(deleted);Для сложных условий и массовых операций используйте низкоуровневый AST.
UPDATE и DELETE должны иметь фильтр. Массовую операцию нужно разрешить
явно через .allow_all():
let query = User::update()
.set(User::NAME, "New name")
.filter(User::ID.eq(1))
.into_ast()?;
database.execute(query).await?;Транзакция выполняется в worker-потоке SQLite pool:
database
.transaction(|connection| {
connection.execute("DELETE FROM users WHERE id = ?1", [1])?;
Ok(())
})
.await?;Ошибка closure вызывает rollback, успешное выполнение вызывает commit.
Foreign key задаётся на поле дочерней модели:
#[derive(Debug, Model)]
#[orm(table = "posts")]
struct Post {
#[orm(primary_key)]
id: i64,
#[orm(foreign_key = User, on_delete = "cascade")]
user_id: i64,
title: String,
}Загрузить записи has_many можно через typed helper:
let user = database
.create::<User>()
.set(User::EMAIL, "alice@example.test")
.set(User::NAME, "Alice")
.execute()
.await?;
let user_id = user.id;
let posts = database.fetch_by(Post::USER_ID, user_id).await?;Для нескольких пользователей используйте fetch_by_many, а не вызывайте
fetch_by в цикле: это устраняет N+1 запросов.
let users = database.all::<User>().await?;
let user_ids = users.iter().map(|user| user.id);
let posts = database.fetch_by_many(Post::USER_ID, user_ids).await?;Дочерние строки затем группируются по post.user_id в памяти. Пустой набор
возвращает пустой список; для очень больших наборов ключей используйте
несколько порций из-за лимита SQLite на bind-параметры.
fetch_by строит обычный параметризованный SELECT. SQLite foreign keys
включаются для каждого соединения пула автоматически. Поэтому
on_delete = "cascade" будет работать и при удалении пользователя.
Post::USER описывает forward relation, а Post::USER.reverse() имеет
reverse name post_set по умолчанию. Связанные модели загружаются через typed
queryset:
let users = database
.query::<User>()
.prefetch_related(Post::USER.reverse())
.all(&database)
.await?;Результат prefetch_related содержит Loaded<User, (LoadedMany<Post, _>,)> и
можно передать в ModelSerializer; serializer получает только
материализованные данные и не имеет доступа к Database.
В workspace есть crate che-orm2-examples:
cargo run -p che-orm2-examples --bin schema
cargo run --bin manage -- migrate
cargo run -p che-orm2-examples --bin sqlite_crud
cargo run -p che-orm2-examples --bin transactions
cargo run -p che-orm2-examples --bin serializersCLI миграций является частью приложения:
cargo run --bin manage -- schema
cargo run --bin manage -- makemigrations
cargo run --bin manage -- migrate
cargo run --bin manage -- migrate status
cargo run --bin manage -- migrate lintmakemigrations собирает schema из Rust-моделей, записывает её во временный
файл, передаёт Atlas через --to file://... и удаляет файл после завершения.
Миграции хранятся в migrations/ и применяются только отдельной командой
deploy, а не при старте приложения. Подробности: docs/atlas.md.
В некоторых версиях Atlas migrate lint требует atlas login/Pro; это
ограничение Atlas, а не приложения.
Модели можно группировать по приложениям, как в Django:
pub struct AccountsApp;
impl che_orm2::AppConfig for AccountsApp {
fn name() -> &'static str { "accounts" }
fn schema() -> che_orm2::SchemaSet {
che_orm2::SchemaSet::new().model::<User>()
}
}
let registry = che_orm2::AppRegistry::new()
.register::<AccountsApp>()
.register::<ContentApp>();Каждое приложение хранит свои модели и возвращает свой SchemaSet. Общий
registry используется manage schema и makemigrations. Регистрируйте
приложения в порядке зависимостей: сначала таблицы-родители, затем модели с
foreign keys.
| Backend | SQL compiler | Pool/executor |
|---|---|---|
| SQLite | Да | Да, async deadpool-sqlite |
| PostgreSQL | Да, placeholders и DDL dialect | Нет |
Для проверки PostgreSQL compiler без SQLite runtime:
cargo test -p che-orm2 --no-default-features --features postgresFeatures sqlite и postgres взаимоисключающие.
cargo fmt --check
cargo test --workspace
cargo doc --workspace --no-depsПодробности находятся в docs/models.md и
docs/sqlite.md. Пошаговый путь от первой модели до
migrations: docs/tutorial.md.