Skip to main content

SeaORM 2.0 Migration Guide

ยท 8 min read
SeaQL Team
Chris Tsang
SeaORM 2.0 Banner

Over the past few months, we've rolled out a series of SeaORM 2.0 releases packed with new capabilities that reshape how you'd use SeaORM. We've stablized our API surface now. Other than dependency upgrades (sqlx 0.9), there won't be more major breaking changes.

If you're eager to upgrade, let's walk through the changes you're likely to encounter.

SeaORM 2.0 is designed as a seamless step forward: all existing features are preserved, no rewrites are required, and it remains functionally backward-compatible. Most migrations are straightforward and mechanical, with many changes amounting to simple find-and-replace operations.

SeaQuery Type Systemโ€‹

Full details can be found here.

ExprTrait is neededโ€‹

#890 For many methods on Expr, e.g. add, eq, ExprTrait is required.

Possible compile errorsโ€‹

error[E0599]: no method named `like` found for enum `sea_query::Expr` in the current scope
|
| Expr::col((self.entity_name(), *self)).like(s)
|
| fn like<L>(self, like: L) -> Expr
| ---- the method is available for `sea_query::Expr` here
|
= help: items from traits can only be used if the trait is in scope
help: trait `ExprTrait` which provides `like` is implemented but not in scope; perhaps you want to import it
|
-> + use sea_orm::ExprTrait;
error[E0308]: mismatched types
--> src/sqlite/discovery.rs:27:57
|
| .and_where(Expr::col(Alias::new("type")).eq("table"))
| -- ^^^^^^^ expected `&Expr`, found `&str`
| |
| arguments to this method are incorrect
|
= note: expected reference `&sea_query::Expr`
found reference `&'static str`
error[E0308]: mismatched types
|
390 | Some(Expr::col(Name).eq(PgFunc::any(query.symbol)))
| -- ^^^^^^^^^^^^^^^^^^^^^^^^^^^ expected `&Expr`, found `FunctionCall`
| |
| arguments to this method are incorrect
|
note: method defined here
--> /rustc/6b00bc3880198600130e1cf62b8f8a93494488cc/library/core/src/cmp.rs:254:8

IntoCondition is no longer neededโ€‹

Can simply use .into

.on_condition(|_left, right| Expr::col((right, super::users::Column::IsSuperAdmin)).eq(true).into_condition())

Replace with:

.on_condition(|_left, right| Expr::col((right, super::users::Column::IsSuperAdmin)).eq(true).into())

ConditionExpression has been removed. Instead, just convert between Condition and Expr using From/Into:

Cond::all().add(Expr::new(..))

Possible compile errorsโ€‹

error[E0603]: enum `ConditionExpression` is private
--> tests/mysql/query.rs:734:20
|
> | use sea_query::ConditionExpression;
| ^^^^^^^^^^^^^^^^^^^ private enum
> | Cond::all().add(ConditionExpression::Expr(Expr::new(
| ^^^^^^^^^^^^^^^^^^^ use of undeclared type `ConditionExpression`

Change of Iden traitโ€‹

#909 The method signature of Iden::unquoted is changed. If you're implementing Iden manually, you can modify it like below:

impl Iden for Glyph {
- fn unquoted(&self, s: &mut dyn fmt::Write) {
+ fn unquoted(&self) -> &str {
- write!(
- s,
- "{}",
match self {
Self::Table => "glyph",
Self::Id => "id",
Self::Tokens => "tokens",
}
- )
- .unwrap();
}
}

Possible compile errorsโ€‹

error[E0050]: method `unquoted` has 2 parameters but the declaration in trait `types::Iden::unquoted` has 1
--> src/tests_cfg.rs:31:17
|
| fn unquoted(&self, s: &mut dyn std::fmt::Write) {
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ expected 1 parameter, found 2
|
::: src/types.rs:63:17
|
| fn unquoted(&self) -> &str;
| ----- trait requires 1 parameter

Alias is no longer needed for &'static strโ€‹

You can use the string "name" in place of all Alias::new("name").

Expr::col(Alias::new("my_col"))

Replace with:

Expr::col("my_col")

SeaORM APIโ€‹

DeriveValueType changesโ€‹

DeriveValueType now implements NotU8, IntoActiveValue, TryFromU64.

You can remove these manual implementation. As a side effects, you can use custom wrapper types as primary keys.

#[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

execute -> execute_rawโ€‹

#2657 Overhauled ConnectionTrait API: execute, query_one, query_all, stream now takes in SeaQuery statement instead of raw SQL statement, and methods accepting Statement has been moved to *_raw.

// old
db.execute(Statement::from_sql_and_values(..)).await?;

// new
db.execute_raw(Statement::from_sql_and_values(..)).await?;

You no longer have to build SeaQuery statements manually:

// 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?;

Possible compile errorsโ€‹

  --> src/executor/paginator.rs:53:38
|
> | let rows = self.db.query_all(stmt).await?;
| --------- ^^^^ expected `&_`, found `Statement`
| |
| arguments to this method are incorrect
|
= note: expected reference `&_`
found struct `statement::Statement`

Overhauled Entity::insert_manyโ€‹

#2628 We've made a number of changes to the insert many API:

  1. Removed APIs that can panic
  2. New helper struct InsertMany, last_insert_id is now Option<Value>
  3. On empty iterator, None or vec![] is returned on exec operations
  4. Allowing active models to have different column sets #2433

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)));

last_insert_id is now Option<Value>:

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(),
}
]
);

Possible compile errorsโ€‹

You will get a warning that exec_with_returning_many is now deprecated. Simply change it to exec_with_returning.

Database Specific Changesโ€‹

Postgresโ€‹

#918 serial is now being replaced with GENERATED BY DEFAULT AS IDENTITY by default

To restore legacy behaviour, you can enable the option-postgres-use-serial feature flag.

let table = Table::create()
.table(Char::Table)
.col(ColumnDef::new(Char::Id).integer().not_null().auto_increment().primary_key())
.to_owned();

assert_eq!(
table.to_string(PostgresQueryBuilder),
[
r#"CREATE TABLE "character" ("#,
r#""id" integer GENERATED BY DEFAULT AS IDENTITY NOT NULL PRIMARY KEY,"#,
r#")"#,
].join(" ")
);

Alternatively, you can also do the following:

// if you needed to support legacy system you can still do:
let table = Table::create()
.table(Char::Table)
.col(ColumnDef::new(Char::Id).custom("serial").not_null().primary_key())
.to_owned();

assert_eq!(
table.to_string(PostgresQueryBuilder),
[
r#"CREATE TABLE "character" ("#,
r#""id" serial NOT NULL PRIMARY KEY"#,
r#")"#,
].join(" ")
);

SQLiteโ€‹

Both Integer and BigInteger (column type) is mapped to integer, and integer would be mapped to i64 by entity generator.

You can use --big-integer-type=i32 to use i32 for integer in sea-orm-cli.

sqlite-use-returning-for-3_35 is now enabled by default.

MariaDBโ€‹

#2710 Added mariadb-use-returning to use returning syntax for MariaDB

Deprecationsโ€‹

Rust Versionโ€‹

We've updated to Rust 2024 edition and MSRV to 1.85.

async-std is deprecatedโ€‹

If you have been using async-std in migration crates, simply switch to tokio.

Framework Supportโ€‹

Locoโ€‹

Loco has not officially migrated to 2.0 yet, but you can try out our fork meanwhile.

GraphQLโ€‹

Seaography has been updated to support SeaORM 2.0.

Why Upgrade?โ€‹

Here are a few reasons that upgrading to 2.0 is worth it:

  1. Exciting new features: new entity format with entity first workflow, nested queries and operations
  2. Removed panics from API surface, less footguns and better error handling
  3. More ergononmics and better compile-time constraints
  4. Admin Panel: SeaORM Pro with Role Based Access Control
  5. Performance improvements: less internal copying and 20% faster query building

Need Help?โ€‹

If you're using SeaORM in a professional environment, please consider asking your company to become a Startup / Enterprise tier sponsor. Company sponsorship comes with benefits such as direct communication with the SeaQL team, as well as professional technical guidance on SeaORM usage, data engineering and Rust development in general.

We've helped companies successfully deploy Rust web services in production on high-throughput workloads.