Skip to main content

A Sneak Peek at SeaORM 2.0

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

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 Models. 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:

  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. 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 unwraps 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​

  1. 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
  2. A set of Entities to load / store the access control rules to / from database
  3. A query auditor that dissect queries for necessary permissions (implemented in SeaQuery)
  4. Integration of RBAC into SeaORM in form of RestrictedConnection. It implements ConnectionTrait, 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 πŸ˜‡:

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