DEV Community

SeaQL
SeaQL

Posted on

SeaORM 2.0: A closer look

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)));
Enter fullscreen mode Exit fullscreen mode

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>,
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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(),
        }
    ]
);
Enter fullscreen mode Exit fullscreen mode

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)));
Enter fullscreen mode Exit fullscreen mode

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?;
Enter fullscreen mode Exit fullscreen mode

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?;
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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>`
    | |_____________________|
    |
Enter fullscreen mode Exit fullscreen mode

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,
}
Enter fullscreen mode Exit fullscreen mode
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
#[sea_orm(table_name = "worker")]
pub struct Model {
    #[sea_orm(primary_key)]
    pub id: i32,
    pub name: String,
}
Enter fullscreen mode Exit fullscreen mode

There exist two relations between them:

Bakery -> Worker (Manager)
       -> Worker (Cashier)
Enter fullscreen mode Exit fullscreen mode

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,
}
Enter fullscreen mode Exit fullscreen mode

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""#
);
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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 { .. })));
}
Enter fullscreen mode Exit fullscreen mode
  • #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 { .. })
));
Enter fullscreen mode Exit fullscreen mode
  • #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>
Enter fullscreen mode Exit fullscreen mode
  • #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
Enter fullscreen mode Exit fullscreen mode
  • #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])"#
);
Enter fullscreen mode Exit fullscreen mode

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()
}
Enter fullscreen mode Exit fullscreen mode

Added chrono::Utc to entity prelude

pub type ChronoUtc = chrono::Utc;
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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>;
}
Enter fullscreen mode Exit fullscreen mode

Previously dyn SqlWriter is used everywhere.

fn prepare_table_create_statement(
    &self,
    create: &TableCreateStatement,
    sql: &mut dyn SqlWriter,
);
Enter fullscreen mode Exit fullscreen mode

Now, it's a generic method:

fn prepare_table_create_statement(
    &self,
    create: &TableCreateStatement,
    sql: &mut impl SqlWriter, // note the impl
);
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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),
    ..
}
Enter fullscreen mode Exit fullscreen mode

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(),
        }
    }
}
Enter fullscreen mode Exit fullscreen mode
// Do the following instead:
#[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)]
pub enum Column {
    Id,
    #[sea_orm(column_name = "my_name")]
    Name,
}
Enter fullscreen mode Exit fullscreen mode

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.

🦀 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

Support SeaQL and get a Sticker Pack!

Rustacean Sticker Pack by SeaQL

Top comments (0)