A Sneak Peek at SeaORM 2.0

SeaORM 1.0 debuted on 2024-08-04. Over the past year, we've shipped 16 minor releases - staying true to our promise of delivering new features without compromising stability.
While building new features in 1.0, we often found ourselves bending over backwards to avoid breaking changes, which meant leaving in a few bits that aren't exactly elegant, intuitive, or frankly, "footgun".
To make SeaORM friendlier and more intuitive for newcomers (and a little kinder to seasoned users too), we've decided it's time for a 2.0 release - one that embraces necessary breaking changes to clean things up and set a stronger foundation for the future.
1.0 New Featuresβ
If you haven't been following every update, here's a quick tour of some quality-of-life improvements you can start using right now. Otherwise, you can skip to the 2.0 section.
Nested Selectβ
This is the most requested feature by far, and we've implemented nested select in SeaORM. We've added nested alias
and ActiveEnum support too.
use sea_orm::DerivePartialModel;
#[derive(DerivePartialModel)]
#[sea_orm(entity = "cake::Entity", from_query_result)]
struct CakeWithFruit {
id: i32,
name: String,
#[sea_orm(nested)]
fruit: Option<Fruit>,
}
#[derive(DerivePartialModel)]
#[sea_orm(entity = "fruit::Entity", from_query_result)]
struct Fruit {
id: i32,
name: String,
}
let cakes: Vec<CakeWithFruit> = cake::Entity::find()
.left_join(fruit::Entity)
.into_partial_model()
.all(db)
.await?;
PartialModel -> ActiveModelβ
DerivePartialModel
got another extension to derive IntoActiveModel
as well. Absent attributes will be filled with NotSet
. This allows you to use partial models to perform insert / updates as well.
#[derive(DerivePartialModel)]
#[sea_orm(entity = "cake::Entity", into_active_model)]
struct PartialCake {
id: i32,
name: String,
}
let partial_cake = PartialCake {
id: 12,
name: "Lemon Drizzle".to_owned(),
};
// this is now possible:
assert_eq!(
cake::ActiveModel {
..partial_cake.into_active_model()
},
cake::ActiveModel {
id: Set(12),
name: Set("Lemon Drizzle".to_owned()),
..Default::default()
}
);
Insert active models with non-uniform column setsβ
Insert many now allows active models to have different column sets. Previously, it'd panic when encountering this. Missing columns will be filled with NULL
. This makes seeding data a seamless operation.
let apple = cake_filling::ActiveModel {
cake_id: ActiveValue::set(2),
filling_id: ActiveValue::NotSet,
};
let orange = cake_filling::ActiveModel {
cake_id: ActiveValue::NotSet,
filling_id: ActiveValue::set(3),
};
assert_eq!(
Insert::<cake_filling::ActiveModel>::new()
.add_many([apple, orange])
.build(DbBackend::Postgres)
.to_string(),
r#"INSERT INTO "cake_filling" ("cake_id", "filling_id") VALUES (2, NULL), (NULL, 3)"#,
);
Support Postgres PgVector & IpNetworkβ
Under feature flag postgres-vector
/ with-ipnetwork
.
#[derive(Clone, Debug, PartialEq, DeriveEntityModel)]
#[sea_orm(table_name = "demo_table")]
pub struct Model {
#[sea_orm(primary_key)]
pub id: i32,
pub embedding: PgVector,
pub ipaddress: IpNetwork,
}
2.0 New Featuresβ
These are small touch-ups, but added-up can make great differences.
Nested Select on any Modelβ
#2642 Wait... we've seen this before? No, there is a small detail here: now every Model can be used in nested select! This requires a small breaking change to basically derive PartialModelTrait
on regular Model
s. And also notice the removed from_query_result
.
use sea_orm::DerivePartialModel;
#[derive(DerivePartialModel)]
#[sea_orm(entity = "cake::Entity")] // <- from_query_result not needed
struct CakeWithFruit {
id: i32,
name: String,
#[sea_orm(nested)]
fruit: Option<fruit::Model>, // <- this is just a regular Model
}
let cakes: Vec<CakeWithFruit> = cake::Entity::find()
.left_join(fruit::Entity)
.into_partial_model()
.all(db)
.await?;
Wrapper type as primary keyβ
#2643 Wrapper type derived with DeriveValueType
can now be used as primary key. Now you can embrace Rust's type system to make your code more robust!
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
#[sea_orm(table_name = "my_value_type")]
pub struct Model {
#[sea_orm(primary_key)]
pub id: MyInteger,
}
#[derive(Clone, Debug, PartialEq, Eq, DeriveValueType)]
pub struct MyInteger(pub i32);
// only for i8 | i16 | i32 | i64 | u8 | u16 | u32 | u64
Multi-part unique keysβ
#2651 You can now define unique keys that span multiple columns in Entity.
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
#[sea_orm(table_name = "lineitem")]
pub struct Model {
#[sea_orm(primary_key)]
pub id: i32,
#[sea_orm(unique_key = "item")]
pub order_id: i32,
#[sea_orm(unique_key = "item")]
pub cake_id: i32,
}
let stmts = Schema::new(backend).create_index_from_entity(lineitem::Entity);
assert_eq!(
stmts[0],
Index::create()
.name("idx-lineitem-item")
.table(lineitem::Entity)
.col(lineitem::Column::OrderId)
.col(lineitem::Column::CakeId)
.unique()
.take()
);
assert_eq!(
backend.build(stmts[0]),
r#"CREATE UNIQUE INDEX "idx-lineitem-item" ON "lineitem" ("order_id", "cake_id")"#
);
Allow missing fields when using ActiveModel::from_json
β
#2599 Improved utility of ActiveModel::from_json
when dealing with inputs probably coming from REST APIs.
Consider the following Entity:
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel, Serialize, Deserialize)]
#[sea_orm(table_name = "cake")]
pub struct Model {
#[sea_orm(primary_key)]
pub id: i32, // <- not nullable
pub name: String,
}
Previously, the following would result in error missing field "id"
. The usual solution is to add #[serde(skip_deserializing)]
to the Model.
assert!(
cake::ActiveModel::from_json(json!({
"name": "Apple Pie",
})).is_err();
);
Now, the above will just work. The ActiveModel will be partially filled:
assert_eq!(
cake::ActiveModel::from_json(json!({
"name": "Apple Pie",
}))
.unwrap(),
cake::ActiveModel {
id: NotSet,
name: Set("Apple Pie".to_owned()),
}
);
How it works under the hood? It's actually quite interesting. This requires a small breaking to the trait bound of the method.
2.0 Overhaulβ
Overhauled Entity::insert_many
β
#2628 We've received many issue reports around the insert_many
API. After careful examination, I made a number of changes in 2.0:
- removed APIs that can panic
- new helper struct
InsertMany
,last_insert_id
is nowOption<Value>
- on empty iterator,
None
orvec![]
is returned on exec operations TryInsert
API is unchanged
Previously, insert_many
shares the same helper struct with insert_one
, which led to an awkard API.
let res = Bakery::insert_many(std::iter::empty())
.on_empty_do_nothing() // <- you needed to add this
.exec(db)
.await;
assert!(matches!(res, Ok(TryInsertResult::Empty)));
Now, last_insert_id
is Option<Value>
for InsertMany
:
struct InsertManyResult<A: ActiveModelTrait>
{
pub last_insert_id: Option<<PrimaryKey<A> as PrimaryKeyTrait>::ValueType>,
}
Which means the awkardness is removed:
let res = Entity::insert_many::<ActiveModel, _>([]).exec(db).await;
assert_eq!(res?.last_insert_id, None); // insert nothing return None
let res = Entity::insert_many([ActiveModel { id: Set(1) }, ActiveModel { id: Set(2) }])
.exec(db)
.await;
assert_eq!(res?.last_insert_id, Some(2)); // insert something return Some
Same on conflict API as before:
let res = Entity::insert_many([ActiveModel { id: Set(3) }, ActiveModel { id: Set(4) }])
.on_conflict_do_nothing()
.exec(db)
.await;
assert!(matches!(conflict_insert, Ok(TryInsertResult::Conflicted)));
Exec with returning now returns a Vec<Model>
, so it feels intuitive:
assert!(
Entity::insert_many::<ActiveModel, _>([])
.exec_with_returning(db)
.await?
.is_empty() // no footgun, nice
);
assert_eq!(
Entity::insert_many([
ActiveModel {
id: NotSet,
value: Set("two".into()),
}
])
.exec_with_returning(db)
.await
.unwrap(),
[
Model {
id: 2,
value: "two".into(),
}
]
);
Overhauled ConnectionTrait
APIβ
#2657
We overhauled the ConnectionTrait
API. execute
, query_one
, query_all
, stream
now takes in SeaQuery statement instead of raw SQL statement.
So you don't have to access the backend to build the query yourself.
// old
let query: SelectStatement = Entity::find().filter(..).into_query();
let backend = self.db.get_database_backend();
let stmt = backend.build(&query);
let rows = self.db.query_all(stmt).await?;
// new
let query: SelectStatement = Entity::find().filter(..).into_query();
let rows = self.db.query_all(&query).await?;
A new set of methods execute_raw
, query_one_raw
, query_all_raw
, stream_raw
is added, so you can still do the following:
let backend = self.db.get_database_backend();
let stmt = backend.build(&query);
// new
let rows = self.db.query_all_raw(stmt).await?;
Removing panics from APIsβ
SeaORM has a large API surface. We've already removed a great number of unwrap
s from the codebase in 1.0, but some panics due to "mis-use of API" can still happen.
Once again, we've tried to remove the remaining panics.
- #2630 Added new error variant
BackendNotSupported
. Previously, it panics with e.g. "Database backend doesn't support RETURNING"
let result = cake::Entity::insert_many([])
.exec_with_returning_keys(db)
.await;
if db.support_returning() {
// Postgres and SQLite
assert_eq!(result.unwrap(), []);
} else {
// MySQL
assert!(matches!(result, Err(DbErr::BackendNotSupported { .. })));
}
- #2627 Added new error variant
PrimaryKeyNotSet
. Previously, it panics with "PrimaryKey is not set"
assert!(matches!(
Update::one(cake::ActiveModel {
..Default::default()
})
.exec(&db)
.await,
Err(DbErr::PrimaryKeyNotSet { .. })
));
- #2634 Remove panics in
Schema::create_enum_from_active_enum
fn create_enum_from_active_enum<A>(&self) -> Option<TypeCreateStatement>
// method can now return None
- #2628 Remove panickable APIs from insert
/// Add a Model to `Insert`
///
/// # Panics
///
/// Panics if the rows have different column sets from what've previously been cached in the query statement
- pub fn add<M>(mut self, m: M) -> Self
2.0 Exciting New Featuresβ
We've planned some exciting new features for SeaORM too.
Raw SQL macroβ
While already explained in detail in a previous blog post, we've integrated the raw_sql!
macro nicely into SeaORM.
It's not a ground-breaking new feature, but it does unlock exciting new ways to use SeaORM. After all, SeaORM isn't just an ORM; it's a flexible SQL toolkit you can tailour to your own programming style. Use it as a backend-agnostic SQLx wrapper, SeaQuery with built-in connection management, or a lightweight ORM with enchanted raw SQL. The choice is yours!
Find Model by raw SQLβ
let id = 1;
let cake: Option<cake::Model> = cake::Entity::find()
.from_raw_sql(raw_sql!(
Postgres,
r#"SELECT "cake"."id", "cake"."name" FROM "cake" WHERE "id" = {id}"#
))
.one(&db)
.await?;
Find custom Model by raw SQLβ
#[derive(FromQueryResult)]
struct Cake {
name: String,
#[sea_orm(nested)]
bakery: Option<Bakery>,
}
#[derive(FromQueryResult)]
struct Bakery {
#[sea_orm(alias = "bakery_name")]
name: String,
}
let cake_ids = [2, 3, 4]; // expanded by the `..` operator
let cake: Option<Cake> = Cake::find_by_statement(raw_sql!(
Sqlite,
r#"SELECT "cake"."name", "bakery"."name" AS "bakery_name"
FROM "cake"
LEFT JOIN "bakery" ON "cake"."bakery_id" = "bakery"."id"
WHERE "cake"."id" IN ({..cake_ids})"#
))
.one(db)
.await?;
Paginate raw SQL queryβ
You can paginate SelectorRaw
and fetch Model
in batch.
let ids = vec![1, 2, 3, 4];
let mut cake_pages = cake::Entity::find()
.from_raw_sql(raw_sql!(
Postgres,
r#"SELECT "cake"."id", "cake"."name" FROM "cake" WHERE "id" IN ({..ids})"#
))
.paginate(db, 10);
while let Some(cakes) = cake_pages.fetch_and_next().await? {
// Do something on cakes: Vec<cake::Model>
}
Role Based Access Controlβ
#2683 We will cover this in detail in a future blog post, but here's a sneak peek.
SeaORM RBACβ
- A hierarchical RBAC engine that is table scoped
- a user has 1 (and only 1) role
- a role has a set of permissions on a set of resources
- permissions here are CRUD operations and resources are tables
- but the engine is generic so can be used for other things
- roles have hierarchy, and can inherit permissions from multiple roles
- there is a wildcard
*
(opt-in) to grant all permissions or resources - individual users can have rules override
- A set of Entities to load / store the access control rules to / from database
- A query auditor that dissect queries for necessary permissions (implemented in SeaQuery)
- Integration of RBAC into SeaORM in form of
RestrictedConnection
. It implementsConnectionTrait
, behaves like a normal connection, but will audit all queries and perform permission check before execution, and reject them accordingly. All Entity operations except raw SQL are supported. Complex nested joins,INSERT INTO SELECT FROM
, and even CTE queries are supported.
// load rules from database
db_conn.load_rbac().await?;
// admin can create bakery
let db: RestrictedConnection = db_conn.restricted_for(admin)?;
let seaside_bakery = bakery::ActiveModel {
name: Set("SeaSide Bakery".to_owned()),
..Default::default()
};
assert!(Bakery::insert(seaside_bakery).exec(&db).await.is_ok());
// manager cannot create bakery
let db: RestrictedConnection = db_conn.restricted_for(manager)?;
assert!(matches!(
Bakery::insert(bakery::ActiveModel::default())
.exec(&db)
.await,
Err(DbErr::AccessDenied { .. })
));
// transaction works too
let txn: RestrictedTransaction = db.begin().await?;
baker::Entity::insert(baker::ActiveModel {
name: Set("Master Baker".to_owned()),
bakery_id: Set(Some(1)),
..Default::default()
})
.exec(&txn)
.await?;
txn.commit().await?;
π₯οΈ SeaORM Pro: Professional Admin Panelβ


SeaORM Pro is an admin panel solution allowing you to quickly and easily launch an admin panel for your application - frontend development skills not required, but certainly nice to have!
Features:
- Full CRUD
- Built on React + GraphQL
- Built-in GraphQL resolver
- Customize the UI with simple TOML
- RBAC (coming soon with SeaORM 2.0)
More to comeβ
SeaORMβ―2.0 is shaping up to be our most significant release yet - with a few breaking changes, plenty of enhancements, and a clear focus on developer experience. We'll unpack everything in the posts to come, so keep an eye out for the next update!
If you have suggestions on breaking changes, you are welcome to post them in the discussions:
Sponsorsβ
If you feel generous, a small donation will be greatly appreciated, and goes a long way towards sustaining the organization.
Gold Sponsorβ
QDX pioneers quantum dynamicsβpowered drug discovery, leveraging AI and supercomputing to accelerate molecular modeling. We're grateful to QDX for sponsoring the development of SeaORM, the SQL toolkit that powers their data intensive applications.
GitHub Sponsorsβ
A big shout out to our GitHub sponsors π:
Rustacean Sticker Pack π¦β
The Rustacean Sticker Pack is the perfect way to express your passion for Rust. Our stickers are made with a premium water-resistant vinyl with a unique matte finish. Stick them on your laptop, notebook, or any gadget to show off your love for Rust!
Moreover, all proceeds contributes directly to the ongoing development of SeaQL projects.
Sticker Pack Contents:
- Logo of SeaQL projects: SeaQL, SeaORM, SeaQuery, Seaography, FireDBG
- Mascot of SeaQL: Terres the Hermit Crab
- Mascot of Rust: Ferris the Crab
- The Rustacean word
Support SeaQL and get a Sticker Pack!
