DEV Community

SeaQL
SeaQL

Posted on • Originally published at sea-ql.org

What's new in SeaORM 0.11.0

πŸŽ‰ 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?;
Enter fullscreen mode Exit fullscreen mode

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

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()) { .. }
Enter fullscreen mode Exit fullscreen mode
SELECT "cake"."id", "cake"."name" FROM "cake"
SELECT "fruit"."id", "fruit"."name", "fruit"."cake_id" FROM "fruit" WHERE "fruit"."cake_id" IN (..)
Enter fullscreen mode Exit fullscreen mode

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?;
Enter fullscreen mode Exit fullscreen mode
SELECT "fruit"."id", "fruit"."name", "fruit"."cake_id" FROM "fruit"
WHERE "fruit"."stock" > 0 AND "fruit"."cake_id" IN (..)
Enter fullscreen mode Exit fullscreen mode

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

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

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

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

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

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

Opt-in Unstable Internal APIs

By enabling sea-orm-internal feature you opt-in unstable internal APIs including:

Breaking Changes

  • [#1420] sea-orm-cli: generate entity command enable --universal-time flag by default

  • [#1425] Added RecordNotInserted and RecordNotUpdated to DbErr

  • [#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>;
Enter fullscreen mode Exit fullscreen mode

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)?;
        ..
    }
}
Enter fullscreen mode Exit fullscreen mode
  • [#1328] The ActiveModelBehavior trait becomes async trait. If you overridden the default ActiveModelBehavior 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,
    {
        // ...
    }

    // ...
}
Enter fullscreen mode Exit fullscreen mode
  • [#1425] DbErr::RecordNotFound("None of the database rows are affected") is moved to a dedicated error variant DbErr::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));
Enter fullscreen mode Exit fullscreen mode
  • [#1395] sea_orm::ColumnType was replaced by sea_query::ColumnType
    • Method ColumnType::def was moved to ColumnTypeTrait
    • ColumnType::Binary becomes a tuple variant which takes in additional option sea_query::BlobSize
    • ColumnType::Custom takes a sea_query::DynIden instead of String and thus a new method custom is added (note the lowercase)
// 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,
}
Enter fullscreen mode Exit fullscreen mode
// 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(),
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

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() and MockDatabase::append_query_errors() [#1367] take any types implemented IntoIterator trait
  • [#1362] find_by_id and delete_by_id take any Into primary key value
  • [#1410] QuerySelect::offset and QuerySelect::limit takes in Into<Option<u64>> where None would reset them
  • [#1236] Added DatabaseConnection::close
  • [#1381] Added is_null getter for ColumnDef
  • [#1177] Added ActiveValue::reset to convert Unchanged into Set
  • [#1415] Added QueryTrait::apply_if to optionally apply a filter
  • Added the sea-orm-internal feature flag to expose some SQLx types
    • [#1297] Added DatabaseConnection::get_*_connection_pool() for accessing the inner SQLx connection pool
    • [#1434] Re-exporting SQLx errors

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!

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.

Top comments (0)