π We are pleased to release SeaORM 0.11.0
!
Data Loaderβ
[#1443, #1238] The LoaderTrait provides an API to load related entities in batches.
Consider this one to many relation:
let cake_with_fruits: Vec<(cake::Model, Vec<fruit::Model>)> = Cake::find()
.find_with_related(Fruit)
.all(db)
.await?;
The generated SQL is:
SELECT
"cake"."id" AS "A_id",
"cake"."name" AS "A_name",
"fruit"."id" AS "B_id",
"fruit"."name" AS "B_name",
"fruit"."cake_id" AS "B_cake_id"
FROM "cake"
LEFT JOIN "fruit" ON "cake"."id" = "fruit"."cake_id"
ORDER BY "cake"."id" ASC
The 1 side's (Cake) data will be duplicated. If N is a large number, this would results in more data being transferred over the wire. Using the Loader would ensure each model is transferred only once.
The following loads the same data as above, but with two queries:
let cakes: Vec<cake::Model> = Cake::find().all(db).await?;
let fruits: Vec<Vec<fruit::Model>> = cakes.load_many(Fruit, db).await?;
for (cake, fruits) in cakes.into_iter().zip(fruits.into_iter()) { .. }
SELECT "cake"."id", "cake"."name" FROM "cake"
SELECT "fruit"."id", "fruit"."name", "fruit"."cake_id" FROM "fruit" WHERE "fruit"."cake_id" IN (..)
You can even apply filters on the related entity:
let fruits_in_stock: Vec<Vec<fruit::Model>> = cakes.load_many(
fruit::Entity::find().filter(fruit::Column::Stock.gt(0i32))
db
).await?;
SELECT "fruit"."id", "fruit"."name", "fruit"."cake_id" FROM "fruit"
WHERE "fruit"."stock" > 0 AND "fruit"."cake_id" IN (..)
To learn more, read the relation docs.
Transaction Isolation Level and Access Modeβ
[#1230] The transaction_with_config
and begin_with_config
allows you to specify the IsolationLevel and AccessMode.
For now, they are only implemented for MySQL and Postgres. In order to align their semantic difference, MySQL will execute SET TRANSACTION
commands before begin transaction, while Postgres will execute SET TRANSACTION
commands after begin transaction.
db.transaction_with_config::<_, _, DbErr>(
|txn| { ... },
Some(IsolationLevel::ReadCommitted),
Some(AccessMode::ReadOnly),
)
.await?;
let transaction = db
.begin_with_config(IsolationLevel::ReadCommitted, AccessMode::ReadOnly)
.await?;
To learn more, read the transaction docs.
Cast Column Type on Select and Saveβ
[#1304] If you need to select a column as one type but save it into the database as another, you can specify the select_as
and the save_as
attributes to perform the casting. A typical use case is selecting a column of type citext
(case-insensitive text) as String
in Rust and saving it into the database as citext
. One should define the model field as below:
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
#[sea_orm(table_name = "ci_table")]
pub struct Model {
#[sea_orm(primary_key)]
pub id: i32,
#[sea_orm(select_as = "text", save_as = "citext")]
pub case_insensitive_text: String
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {}
impl ActiveModelBehavior for ActiveModel {}
Changes to ActiveModelBehavior
β
[#1328, #1145] The methods of ActiveModelBehavior
now have Connection
as an additional parameter. It enables you to perform database operations, for example, logging the changes made to the existing model or validating the data before inserting it.
#[async_trait]
impl ActiveModelBehavior for ActiveModel {
/// Create a new ActiveModel with default values. Also used by `Default::default()`.
fn new() -> Self {
Self {
uuid: Set(Uuid::new_v4()),
..ActiveModelTrait::default()
}
}
/// Will be triggered before insert / update
async fn before_save<C>(self, db: &C, insert: bool) -> Result<Self, DbErr>
where
C: ConnectionTrait,
{
// Logging changes
edit_log::ActiveModel {
action: Set("before_save".into()),
values: Set(serde_json::json!(model)),
..Default::default()
}
.insert(db)
.await?;
Ok(self)
}
}
To learn more, read the entity docs.
Execute Unprepared SQL Statementβ
[#1327] You can execute an unprepared SQL statement with ConnectionTrait::execute_unprepared
.
// Use `execute_unprepared` if the SQL statement doesn't have value bindings
db.execute_unprepared(
"CREATE TABLE `cake` (
`id` int NOT NULL AUTO_INCREMENT PRIMARY KEY,
`name` varchar(255) NOT NULL
)"
)
.await?;
// Construct a `Statement` if the SQL contains value bindings
let stmt = Statement::from_sql_and_values(
manager.get_database_backend(),
r#"INSERT INTO `cake` (`name`) VALUES (?)"#,
["Cheese Cake".into()]
);
db.execute(stmt).await?;
Select Into Tupleβ
[#1311] You can select a tuple (or single value) with the into_tuple
method.
let res: Vec<(String, i64)> = cake::Entity::find()
.select_only()
.column(cake::Column::Name)
.column(cake::Column::Id.count())
.group_by(cake::Column::Name)
.into_tuple()
.all(&db)
.await?;
Atomic Migrationβ
[#1379] Migration will be executed in Postgres atomically that means migration scripts will be executed inside a transaction. Changes done to the database will be rolled back if the migration failed. However, atomic migration is not supported in MySQL and SQLite.
You can start a transaction inside each migration to perform operations like seeding sample data for a newly created table.
Types Supportβ
- [#1325] Support various UUID formats that are available in
uuid::fmt
module
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
#[sea_orm(table_name = "uuid_fmt")]
pub struct Model {
#[sea_orm(primary_key)]
pub id: i32,
pub uuid: Uuid,
pub uuid_braced: uuid::fmt::Braced,
pub uuid_hyphenated: uuid::fmt::Hyphenated,
pub uuid_simple: uuid::fmt::Simple,
pub uuid_urn: uuid::fmt::Urn,
}
- [#1210] Support vector of enum for Postgres
#[derive(Debug, Clone, PartialEq, Eq, EnumIter, DeriveActiveEnum)]
#[sea_orm(rs_type = "String", db_type = "Enum", enum_name = "tea")]
pub enum Tea {
#[sea_orm(string_value = "EverydayTea")]
EverydayTea,
#[sea_orm(string_value = "BreakfastTea")]
BreakfastTea,
}
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
#[sea_orm(table_name = "enum_vec")]
pub struct Model {
#[sea_orm(primary_key)]
pub id: i32,
pub teas: Vec<Tea>,
pub teas_opt: Option<Vec<Tea>>,
}
- [#1414] Support
ActiveEnum
field as primary key
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
#[sea_orm(table_name = "enum_primary_key")]
pub struct Model {
#[sea_orm(primary_key, auto_increment = false)]
pub id: Tea,
pub category: Option<Category>,
pub color: Option<Color>,
}
Opt-in Unstable Internal APIsβ
By enabling sea-orm-internal
feature you opt-in unstable internal APIs including:
- Accessing the inner connection pool of SQLx with
get_*_connection_pool
method - Re-exporting SQLx errors types:
SqlxError
,SqlxMySqlError
,SqlxPostgresError
andSqlxSqliteError
Breaking Changesβ
[#1366]
sea-query
has been upgraded to0.28.x
, which comes with some improvements and breaking changes. Please follow the release notes for more details[#1420] sea-orm-cli:
generate entity
command enable--universal-time
flag by default[#1425] Added
RecordNotInserted
andRecordNotUpdated
toDbErr
[#1327] Added
ConnectionTrait::execute_unprepared
method[#1311] The required method of
TryGetable
changed:
// then
fn try_get(res: &QueryResult, pre: &str, col: &str) -> Result<Self, TryGetError>;
// now; ColIdx can be `&str` or `usize`
fn try_get_by<I: ColIdx>(res: &QueryResult, index: I) -> Result<Self, TryGetError>;
So if you implemented it yourself:
impl TryGetable for XXX {
- fn try_get(res: &QueryResult, pre: &str, col: &str) -> Result<Self, TryGetError> {
+ fn try_get_by<I: sea_orm::ColIdx>(res: &QueryResult, idx: I) -> Result<Self, TryGetError> {
- let value: YYY = res.try_get(pre, col).map_err(TryGetError::DbErr)?;
+ let value: YYY = res.try_get_by(idx).map_err(TryGetError::DbErr)?;
..
}
}
- [#1328] The
ActiveModelBehavior
trait becomes async trait. If you overridden the defaultActiveModelBehavior
implementation:
#[async_trait::async_trait]
impl ActiveModelBehavior for ActiveModel {
async fn before_save<C>(self, db: &C, insert: bool) -> Result<Self, DbErr>
where
C: ConnectionTrait,
{
// ...
}
// ...
}
- [#1425]
DbErr::RecordNotFound("None of the database rows are affected")
is moved to a dedicated error variantDbErr::RecordNotUpdated
let res = Update::one(cake::ActiveModel {
name: Set("Cheese Cake".to_owned()),
..model.into_active_model()
})
.exec(&db)
.await;
// then
assert_eq!(
res,
Err(DbErr::RecordNotFound(
"None of the database rows are affected".to_owned()
))
);
// now
assert_eq!(res, Err(DbErr::RecordNotUpdated));
- [#1395]
sea_orm::ColumnType
was replaced bysea_query::ColumnType
- Method
ColumnType::def
was moved toColumnTypeTrait
ColumnType::Binary
becomes a tuple variant which takes in additional optionsea_query::BlobSize
ColumnType::Custom
takes asea_query::DynIden
instead ofString
and thus a new methodcustom
is added (note the lowercase)
- Method
// Compact Entity
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
#[sea_orm(table_name = "fruit")]
pub struct Model {
- #[sea_orm(column_type = r#"Custom("citext".to_owned())"#)]
+ #[sea_orm(column_type = r#"custom("citext")"#)]
pub column: String,
}
// Expanded Entity
impl ColumnTrait for Column {
type EntityName = Entity;
fn def(&self) -> ColumnDef {
match self {
- Self::Column => ColumnType::Custom("citext".to_owned()).def(),
+ Self::Column => ColumnType::custom("citext").def(),
}
}
}
SeaORM Enhancementsβ
- [#1256] Refactor schema module to expose functions for database alteration
- [#1346] Generate compact entity with
#[sea_orm(column_type = "JsonBinary")]
macro attribute MockDatabase::append_exec_results()
,MockDatabase::append_query_results()
,MockDatabase::append_exec_errors()
andMockDatabase::append_query_errors()
[#1367] take any types implementedIntoIterator
trait- [#1362]
find_by_id
anddelete_by_id
take anyInto
primary key value - [#1410]
QuerySelect::offset
andQuerySelect::limit
takes inInto<Option<u64>>
whereNone
would reset them - [#1236] Added
DatabaseConnection::close
- [#1381] Added
is_null
getter forColumnDef
- [#1177] Added
ActiveValue::reset
to convertUnchanged
intoSet
- [#1415] Added
QueryTrait::apply_if
to optionally apply a filter - Added the
sea-orm-internal
feature flag to expose some SQLx types
CLI Enhancementsβ
- [#846, #1186, #1318] Generate
#[serde(skip_deserializing)]
for primary key columns - [#1171, #1320] Generate
#[serde(skip)]
for hidden columns - [#1124, #1321] Generate entity with extra derives and attributes for model struct
Integration Examplesβ
SeaORM plays well with the other crates in the async ecosystem. We maintain an array of example projects for building REST, GraphQL and gRPC services. More examples wanted!
- Actix v4 Example
- Axum Example
- GraphQL Example
- jsonrpsee Example
- Poem Example
- Rocket Example
- Salvo Example
- Tonic Example
Sponsorβ
Our GitHub Sponsor profile is up! SeaQL.org is an independent open-source organization run by passionate developers. If you enjoy using SeaORM, please star and share our repositories. If you feel generous, a small donation will be greatly appreciated, and goes a long way towards sustaining the project.
A big shout out to our sponsors π:
What's Next?β
SeaQL is a community driven project. We welcome you to participate, contribute and build together for Rust's future.
Here is the roadmap for SeaORM 0.12.x
.