In the previous blog post, we highlighted some of the new features in SeaORM 2.0. In this post, we're going to take a closer look to some of the changes under the hood.
Overhauled Entity::insert_many
#2628 We've received many issue reports around the insert_many API. 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,
// otherwise insert empty [] would lead to error
.exec(db)
.await;
assert!(matches!(res, Ok(TryInsertResult::Empty)));
After careful consideration, we made a number of changes in 2.0:
- removed APIs (e.g.
Insert::add) that can panic - new helper struct
InsertMany,last_insert_idis nowOption<Value> - on empty iterator,
None(forlast_insert_id) orvec is returned on execution -
TryInsertAPI is unchanged
i.e. 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
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(),
}
]
);
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)));
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?;
Better error handling in UpdateOne / DeleteOne
#2726 UpdateOne and DeleteOne no longer implement QueryFilter and QueryTrait
directly. Those implementations could expose an incomplete SQL query with an incomplete condition that touches too many records.
// bad: the following is basically update all
let query: UpdateStatement = Update::one(cake::ActiveModel::default()).into_query();
To generate the right condition, we must make sure that the primary key is set on the input ActiveModel by
calling the validate() method:
Update::one(active_model)
+ .validate()? // checks the query; may yield PrimaryKeyNotSet error
.build(DbBackend::Postgres)
Potential compile errors
If you need to access the generated SQL query, convert into ValidatedUpdateOne/ValidatedDeleteOne first.
error[E0599]: no method named `build` found for struct `query::update::UpdateOne` in the current scope
--> src/entity/column.rs:607:22
|
> | / Update::one(active_model)
> | | .build(DbBackend::Postgres)
| | -^^^^^ method not found in `UpdateOne<A>`
| |_____________________|
|
Added has_many_via for reverse has many relation
Consider the following entities:
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
#[sea_orm(table_name = "bakery")]
pub struct Model {
#[sea_orm(primary_key)]
pub id: i32,
pub name: String,
pub manager_id: i32,
pub cashier_id: i32,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {
#[sea_orm(
belongs_to = "super::worker::Entity",
from = "Column::ManagerId",
to = "super::worker::Column::Id"
)]
Manager,
#[sea_orm(
belongs_to = "super::worker::Entity",
from = "Column::CashierId",
to = "super::worker::Column::Id"
)]
Cashier,
}
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
#[sea_orm(table_name = "worker")]
pub struct Model {
#[sea_orm(primary_key)]
pub id: i32,
pub name: String,
}
There exist two relations between them:
Bakery -> Worker (Manager)
-> Worker (Cashier)
It's now possible to define the inverse side of the relations in Worker:
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
#[sea_orm(table_name = "worker")]
pub struct Model {
#[sea_orm(primary_key)]
pub id: i32,
pub name: String,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {
#[sea_orm(has_many = "super::bakery::Entity", via = "Relation::Manager")]
BakeryManager,
#[sea_orm(has_many = "super::bakery::Entity", via = "Relation::Cashier")]
BakeryCashier,
}
These relations can then be used in queries:
assert_eq!(
worker::Entity::find().join(
JoinType::LeftJoin,
worker::Relation::BakeryManager.def(),
)
.build(DbBackend::Sqlite)
.to_string(),
r#"SELECT "worker"."id", "worker"."name" FROM "worker"
LEFT JOIN "bakery" ON "worker"."id" = "bakery"."manager_id""#
);
Use of transaction with generic connections
You can already use TransactionTrait as a generic parameter to define functions accepting any connection object that can initiate transactions.
In SeaORM 2.0, there are new database-connection-like objects: RestrictedConnection and RestrictedTransaction. They implement ConnectionTrait and TransactionTrait, and behaves just like normal DatabaseConnections except that they performs additional checks on queries.
| Connection type | Associated transaction type |
|---|---|
DatabaseConnection |
DatabaseTransaction |
RestrictedConnection |
RestrictedTransaction |
// new connection type
pub struct RestrictedConnection {
conn: DatabaseConnection, // just a wrapper
user_id: UserId,
}
impl TransactionTrait for RestrictedConnection {
type Transaction = RestrictedTransaction; // added associated type
}
Meaning the following would continue to work:
// accepts any one of DatabaseConnection / DatabaseTransaction / RestrictedConnection / RestrictedTransaction.
// nested transactions will be spawned for transaction objects
async fn perform_actions<C: TransactionTrait>(
db: &C,
actions: &[Action],
) -> Result<(), DbErr> {
let txn = db.begin().await?;
for action in actions {
txn.execute(perform(action)).await?;
}
txn.commit().await
}
Removing panics from API
SeaORM has a large API surface. We've already removed a great number of unwraps from the codebase in 1.0 release, 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
// method can now return None
fn create_enum_from_active_enum<A>(&self) -> Option<TypeCreateStatement>
- #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
- #2637 Remove panics in loader
Enhancements
These are small touch‑ups, but added up they can make a big difference.
Added shorthand for Postgres= ANY
Added ColumnTrait::eq_any as a shorthand for the = ANY operator. Postgres only.
// old: have to import sea-query
use sea_orm::sea_query::{Expr, extension::postgres::PgFunc};
cake::Entity::find()
.filter(
// have to qualify column manually
Expr::col((cake::Entity, cake::Column::Id)).eq(PgFunc::any(vec![4, 5]))
);
// new: just use sea-orm
assert_eq!(
cake::Entity::find()
.filter(cake::Column::Id.eq_any(vec![4, 5]))
.build(DbBackend::Postgres)
.to_string(),
r#"SELECT "cake"."id", "cake"."name" FROM "cake"
WHERE "cake"."id" = ANY(ARRAY [4,5])"#
);
Added big_pk_auto
// old
pub fn pk_auto<T: IntoIden>(name: T) -> ColumnDef {
integer(name).auto_increment().primary_key().take()
}
// new: same as above but use big integer
pub fn big_pk_auto<T: IntoIden>(name: T) -> ColumnDef {
big_integer(name).auto_increment().primary_key().take()
}
Added chrono::Utc to entity prelude
pub type ChronoUtc = chrono::Utc;
We can now rely on sea-orm's re-export:
// old: chrono has to be added in Cargo.toml
let ts: ChronoDateTimeUtc = chrono::Utc::now();
// new: use sea-orm's re-export
let ts: ChronoDateTimeUtc = ChronoUtc::now();
Breaking changes
Use &'static str in identifiers
#2667 Changed IdenStatic and EntityName definition. This change stemmed from the revamp of the Iden type system in SeaQuery, in which &'static str now has slightly less overhead.
trait IdenStatic {
fn as_str(&self) -> &'static str; // added static lifetime
}
trait EntityName {
fn table_name(&self) -> &'static str; // added static lifetime
}
QueryBuilder is no longer object safe
Removed DbBackend::get_query_builder() because QueryBuilder is no longer object safe. This change improved query building performance by 5-10%.
impl DbBackend {
// This is removed
- fn get_query_builder(&self) -> Box<dyn QueryBuilder>;
}
Previously dyn SqlWriter is used everywhere.
fn prepare_table_create_statement(
&self,
create: &TableCreateStatement,
sql: &mut dyn SqlWriter,
);
Now, it's a generic method:
fn prepare_table_create_statement(
&self,
create: &TableCreateStatement,
sql: &mut impl SqlWriter, // note the impl
);
This change shouldn't impact most users because we have the following API:
pub trait StatementBuilder {
fn build(&self, db_backend: &DbBackend) -> Statement;
}
// implemented for SelectStatement, InsertStatement, UpdateStatement, DeleteStatement, etc
Changed Database Connection
#2671 DatabaseConnection is changed from enum to struct. The original enum is moved into DatabaseConnection::inner. The new enum is named DatabaseConnectionType.
This allows DatabaseConnection to hold additional metadata.
// old
pub enum DatabaseConnection {
SqlxMySqlPoolConnection(crate::SqlxMySqlPoolConnection),
SqlxPostgresPoolConnection(crate::SqlxPostgresPoolConnection),
SqlxSqlitePoolConnection(crate::SqlxSqlitePoolConnection),
..
}
// new
pub struct DatabaseConnection {
pub inner: DatabaseConnectionType,
..
}
pub enum DatabaseConnectionType {
SqlxMySqlPoolConnection(crate::SqlxMySqlPoolConnection),
SqlxPostgresPoolConnection(crate::SqlxPostgresPoolConnection),
SqlxSqlitePoolConnection(crate::SqlxSqlitePoolConnection),
..
}
Removed Derive Custom Column
#2667 Removed DeriveCustomColumn macro and default_as_str trait method. This was a legacy of the expanded entity format.
// This is no longer supported:
#[derive(Copy, Clone, Debug, EnumIter, DeriveCustomColumn)]
pub enum Column {
Id,
Name,
}
impl IdenStatic for Column {
fn as_str(&self) -> &str {
match self {
Self::Name => "my_name",
_ => self.default_as_str(),
}
}
}
// Do the following instead:
#[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)]
pub enum Column {
Id,
#[sea_orm(column_name = "my_name")]
Name,
}
Upgrades
-
tokiois now used in place ofasync-stdin sea-orm-cli and examples as async-std has been deprecated. - Returning is now enabled for SQLite by default. SQLite introduced returning in
3.35which was released in 2021, it should be the default by now. - #2596 Upgraded Rust Edition to 2024
- Upgraded
strumto0.27
SQL Server Support
SQL Server for SeaORM offers the same SeaORM API for MSSQL. We ported all test cases and examples, complemented by MSSQL specific documentation. If you are building enterprise software, you can request commercial access. It is currently based on SeaORM 1.0, but we will offer free upgrade to existing users when SeaORM 2.0 is finalized.
🖥️ 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!
SeaORM Pro will be updated to support the latest features in SeaORM 2.0.
Features:
- Full CRUD
- Built on React + GraphQL
- Built-in GraphQL resolver
- Customize the UI with TOML config
- Custom GraphQL endpoints (new in 2.0)
- Role Based Access Control (new in 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 dive into Role Based Access Control in the next post, so keep an eye out for the next update!
SeaORM 2.0 will launch alongside SeaQuery 1.0. If you make extensive use of SeaORM's underlying query builder, we recommend checking out our earlier blog post on SeaQuery 1.0 to get familiar with the changes.
SeaORM 2.0 has reached its release candidate phase. We'd love for you to try it out and help shape the final release by sharing your feedback.
🦀 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.
Sticker Pack Contents:
- Logo of SeaQL projects: SeaQL, SeaORM, SeaQuery, Seaography
- Mascots: Ferris the Crab x 3, Terres the Hermit Crab
- The Rustacean wordmark



Top comments (0)