One derive, four databases, rollbacks you don’t write.
A model is a struct with #[derive(Entity)]. The same entity runs on SQLite, MySQL/MariaDB, Postgres and Cloudflare D1 — and on wasm, where CI checks the ORM builds.
What you get
- Drivers: MySQL/MariaDB, Postgres and SQLite through
sea-orm-executor; Cloudflare D1 (d1-http) and libSQL/Turso (libsql-http) over HTTP. - Read/write split with sticky reads, and sharding.
Step::create_table::<Post>builds the table from the model;Step::tablederives its own rollback.- Relationships load for a whole slice in one
WHERE … IN— N+1 is unrepresentable. - Soft deletes, route keys, pagination, model hooks on the event bus (a
-inghook can veto).
In the docs
src/app/models/post.rs
use rainier_framework::prelude::*;
use serde::Serialize;
#[derive(Entity, Clone, Debug, Serialize)]
#[orm(table = "posts")]
pub struct Post {
#[orm(pk, auto_increment)]
pub id: u64,
#[orm(unique)]
pub slug: String,
pub title: String,
pub published: bool,
#[orm(index, references = "users(id)", on_delete = "cascade")]
pub author_id: u64,
#[orm(soft_delete)]
pub deleted_at: Option<DateTime<Utc>>,
}
impl Post {
pub fn author() -> BelongsTo<Post, User> {
BelongsTo::new().foreign_key("author_id")
}
}
src/database/migrations/m0008_posts_add_excerpt.rs
use rainier_framework::database::Step;
pub fn migration() -> Step {
// No `down` to write: adding a column reverses to dropping it,
// an index to dropping the index — in reverse order.
Step::table("0008_posts_add_excerpt", "posts", |table| {
table.string_len("excerpt", 500).nullable();
table.index(["author_id", "created_at"]);
})
}
src/app/http/controllers/post_controller.rs
pub async fn index(Validated(query): Validated<ListPostsRequest>) -> Result<Response> {
let posts = resolve::<PostRepository>()?;
let users = resolve::<UserRepository>()?;
let page = posts
.paginate_matching(Criteria::new().where_eq("published", true), query.page, 20)
.await?;
// One `WHERE id IN (…)` for every author on the page. `load` takes the
// slice, not a post — there is no per-row call to put in a loop.
let authors = Post::author().load(&page.data, &**users).await?;
Ok(Response::json(&page))
}
Up close
Select an image to see it full size.