SeaORM
SeaORM is an async ORM built on sqlx - dynamic queries, relations, and codegen for service-style Rust backends.
Recipe
Quick-reference recipe card - copy-paste ready.
use sea_orm::{entity::*, query::*, Database};
let db = Database::connect(&database_url).await?;
let users = User::find()
.filter(user::Column::Active.eq(true))
.all(&db)
.await?;When to reach for this: You want async ORM ergonomics, relation helpers, and codegen without Diesel's sync-first model.
Working Example
use sea_orm::{entity::prelude::*, Database, Set};
mod item {
use super::*;
#[derive(Clone, Debug, PartialEq, DeriveEntityModel)]
#[sea_orm(table_name = "items")]
pub struct Model { #[sea_orm(primary_key)] pub id: i64, pub name: String }
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {}
impl ActiveModelBehavior for ActiveModel {}
}
use item::{self, Entity as Item};
#[tokio::main]
async fn main() -> Result<(), sea_orm::DbErr> {
let db = Database::connect(&std::env::var("DATABASE_URL").unwrap()).await?;
let am = item::ActiveModel { name: Set("widget".to_string()), ..Default::default() };
let model = am.insert(&db).await?;
let all = Item::find().all(&db).await?;
println!("{:?} {:?}", model, all);
Ok(())
}What this demonstrates:
DeriveEntityModelcodegen for table mapping.ActiveModelfor inserts withSetfields.Entity::find()fluent query API.- Async
Databaseconnection throughout.
Deep Dive
How It Works
- SeaORM uses sqlx drivers under the hood.
sea-orm-cligenerates entities from migrations or database introspection.- Relations express joins and eager loading helpers.
SeaORM vs sqlx Raw
| Topic | SeaORM | sqlx |
|---|---|---|
| Queries | Builder + entities | SQL strings |
| Relations | Built-in | Manual joins |
| Dynamic filters | Strong support | Manual SQL building |
| Control | Less SQL visible | Full SQL |
Rust Notes
// Pagination
let page = Item::find().paginate(&db, 50);
let models = page.fetch_page(0).await?;Gotchas
- ORM leakage into handlers - Returning
ActiveModelfrom APIs. Fix: Map to DTOs at boundary. - Implicit N+1 -
load_relatedin loops. Fix: Eagerfind_with_relatedpatterns. - Migration dual stack - SeaORM migrations vs sqlx migrate confusion. Fix: Pick one migration tool per repo.
- Heavy codegen - Entity regen after schema change is mandatory. Fix: CI step
sea-orm-cli generate. - Complex reporting SQL - ORM struggles with analytic queries. Fix: Raw sqlx for reports only.
Alternatives
| Alternative | Use When | Don't Use When |
|---|---|---|
| sqlx | SQL control, simpler stack | Rich relation API needed |
| Diesel | Compile-time query builder | Native async required |
tokio-postgres | Minimal abstraction | CRUD-heavy services |
FAQs
Which databases?
PostgreSQL, MySQL, SQLite via sqlx backends.
Transactions?
db.begin().await? returns transaction handle for entity ops.
Soft deletes?
Global filter patterns on deleted_at column in entity queries.
JSON fields?
Json column type with serde types.
Testing?
In-memory SQLite Database::connect("sqlite::memory:").
Axum integration?
Store DatabaseConnection in AppState - cheap clone.
Batch inserts?
Entity::insert_many(active_models).exec(&db).
Migrations tool?
sea-orm-migration crate with Rust migration files.
Performance?
Profile generated SQL - ORM can emit suboptimal queries.
When not SeaORM?
Heavy custom SQL, data warehouse queries, or tiny micro-CRUD with sqlx only.
Related
- sqlx - Underlying driver layer
- Diesel - Sync ORM alternative
- Migrations - Schema evolution
- Query Patterns & N+1 - Relation loading
- Connection Pooling - Pool config
Stack versions: This page was written for Rust 1.97.0 (edition 2024), Tokio 1.x, Axum 0.8, serde 1.0, sqlx 0.8, clap 4, and Polars 0.46+.