Skip to main content

SeaORM 2.0: A closer look

Β· 15 min read
SeaQL Team
Chris Tsang
SeaORM 2.0 Banner

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:

  1. removed APIs (e.g. Insert::add) that can panic
  2. new helper struct InsertMany, last_insert_id is now Option<Value>
  3. on empty iterator, None (for last_insert_id) or vec![] (when returning) is returned on execution
  4. TryInsert API 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 typeAssociated transaction type
DatabaseConnectionDatabaseTransaction
RestrictedConnectionRestrictedTransaction
// 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:

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​

  • tokio is now used in place of async-std in sea-orm-cli and examples as async-std has been deprecated.
  • Returning is now enabled for SQLite by default. SQLite introduced returning in 3.35 which was released in 2021, it should be the default by now.
  • #2596 Upgraded Rust Edition to 2024
  • Upgraded strum to 0.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.

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 πŸ˜‡:

Subscribe Pro

Variant9
Ryan Swart
OteroRafael
Yuta Hinokuma
wh7f
MS
Numeus
Data Intuitive
Caido Community
Marcus Buffett

MasakiMiyazaki
KallyDev
Manfred Lee
Afonso Barracha
Dean Sheather

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!

Rustacean Sticker Pack by SeaQL