Repository: SeaQL/sea-orm (Rust)

Issue:
More context for ActiveModelBehaviour
## Motivation
When designing entities it is pretty common to write constraints into the datalayer. Sea-orm provides the `ActiveModelBehaviour` to allow users to fill in these pieces.
However, it is usually necessary to do some further analysis in these cases, like dropping some queries to check for further conditions to allow or deny a certain change. It might also be important to know the state of a record before the change that is going to be made, for example to implement a proper state transition, that might only be valid if some more data has been added to the record.

## Proposed Solutions
It would be best if the `ActiveModelBehaviour`'s signature would change to be like follows: 
```rust
pub trait ActiveModelBehavior: ActiveModelTrait {
    /// Create a new ActiveModel with default values. Also used by `Default::default()`.
    fn new() -> Self {
        <Self as ActiveModelTrait>::default()
    }

    /// Will be called before saving
    // Note, that the former insert parameter is replaced by the option to the old_record, which fullfills the same purpose (and more), but is nevertheless a breaking change.
    fn before_save(self, old_record: Option<Model>, connection: &DatabaseConnection) -> Result<Self, DbErr> {
        Ok(self)
    }

    /// Will be called after saving
    fn after_save(
        model: <Self::Entity as EntityTrait>::Model,
        old_record: Option<Model>,
        connection: &DatabaseConnection,
    ) -> Result<<Self::Entity as EntityTrait>::Model, DbErr> {
        Ok(model)
    }

    /// Will be called before deleting
    fn before_delete(self, old_record: Option<Model>, connection: &DatabaseConnection) -> Result<Self, DbErr> {
        Ok(self)
    }

    /// Will be called after deleting
    fn after_delete(self, old_record: Option<Model>, connection: &DatabaseConnection) -> Result<Self, DbErr> {
        Ok(self)
    }
}
```
It might also be convenient to reduce the amount of parameters and pack them into a parameter-object struct like this:
```rust
pub struct SaveContext {
    old_record: …
    connection: …
}
```



Discussion:
Hey @mohs8421, thanks for the feature request!

Passing `&DatabaseConnection` into `*_save` and `*_delete` is a straightforward and easy change.

However, providing the old record / model isn't a trivial task. Currently, we don't store old value in ActiveModel.

One approach would be adding a `Changed` variant to `ActiveValue`, which keeps a tuple representing the old and new value of a field in `ActiveModel`
- https://github.com/SeaQL/sea-orm/blob/10a215d28e746b0fb8cdd82e9d146e6e8a82201a/src/entity/active_model.rs#L38-L49
That approach actually matches pretty good with a part of what I did before to get the "full" record. That means not only have the changed record I get from the client, but also the data, which is stored in the database:
```rust
/// When an ActiveModel is created, it does not necessarily know,
/// whether the values it contains are changed or not, comparing
/// it to the existing Model solves this problem.
pub fn align_changes<E, A>(model: E::Model, mut active_model: A) -> A
where
    E: EntityTrait,
    A: ActiveModelTrait<Entity = E>,
{
    for column in E::Column::iter() {
        let active_value = active_model.get(column);
        if let ActiveValue::Unchanged(value) = active_value {
            if model.get(column) != value {
                active_model.set(column, value);
            }
        }
    }
    // primary keys should never be updated here, so they have to be aligned from the existing model
    for primary in E::PrimaryKey::iter() {
        let col = primary.into_column();
        if model.get(col) != *active_model.get(col).as_ref() {
            active_model.set(col, model.get(col))
        }
    }
    active_model
}
```
It might even be possible to do something similar to this aligning method by actively providing a filter for the entity, how to get to that record (in case it is not as simple as in my model, where I always have a reliable primary key).

Anyway I think I like your suggestion.
Hey @mohs8421, sorry, I'm not sure what the snippet above what to do.

Btw...
```rs
for column in E::Column::iter() {
    let active_value = active_model.get(column);
    if let ActiveValue::Unchanged(value) = active_value {
        if model.get(column) != value {
            active_model.set(column, value);
            // Shouldn't the line above be...?
            // active_model.set(column, model.get(column));
        }
    }
}
```
The snippet is intended to put the two pieces together.
Let's say I get an active model from my frontend, and match that with the model as it is in the database.

To give you some more context on that snippet: before this is called, I have this method:
```rust
pub(crate) async fn deserialize_active_model<T>(request: &mut Request) -> TideResult<T>
where
    T: ActiveModelTrait,
    <<T as ActiveModelTrait>::Entity as EntityTrait>::Model: IntoActiveModel<T>,
    for<'de> <<T as ActiveModelTrait>::Entity as EntityTrait>::Model: serde::de::DeserializeOwned,
{
    let result = match request.content_type() {
        Some(c) if c == mime::FORM => {
            let result: T = request
                .body_form::<<<T as ActiveModelTrait>::Entity as EntityTrait>::Model>()
                .await?
                .into_active_model();
            Ok(result)
        }
        Some(c) if c == mime::JSON => T::from_json(request.body_json::<serde_json::Value>().await?),
        _ => {
            return Err(Error::from_str(
                StatusCode::NotAcceptable,
                "unrecognized content type",
            ));
        }
    };
    result.map_err(|error| Error::new(StatusCode::BadRequest, error))
}
```
The important part there is, that I can only deserialize into a model, and not into an active model, because that one is generated somewhere inside of sea-orm and I cannot declare it to be Deserialize directly.

Here comes the previously stated function into the game, because it now looks at the model received from the database and compares that with the active_model, which has been translated from a model from the request.
So it only has to specify the correct enum type - which is set for the given active_model, because it should still have the same value, but not as unchanged.

Yes it is a hacky kind of glue code, but it does what it should. Nevertheless I would love to get rid of that at some point in the future though.
Interesting! Will this be helpful? https://www.sea-ql.org/SeaORM/docs/basic-crud/insert/#set-activemodel-from-json-value
Please note the `T::from_json(…)` part, where I already use what you suggested.
Hey @billy1624 

> Passing &DatabaseConnection into *_save and *_delete is a straightforward and easy change.

What has to be done? Maybe I could help
@teenjuna take a look at https://github.com/SeaQL/sea-orm/blob/master/src/entity/active_model.rs#L281 as an example, there you would only need to pass C into the behavior and similar things would need to be done in the other corresponding methods, and probably all related tests need to be adapted. For this part of the change.



Patch:
diff --git a/src/entity/active_model.rs b/src/entity/active_model.rs
index 4b79291201..519cb8ca43 100644
--- a/src/entity/active_model.rs
+++ b/src/entity/active_model.rs
@@ -278,11 +278,11 @@ pub trait ActiveModelTrait: Clone + Debug {
         Self: ActiveModelBehavior + 'a,
         C: ConnectionTrait,
     {
-        let am = ActiveModelBehavior::before_save(self, true)?;
+        let am = ActiveModelBehavior::before_save(self, db, true).await?;
         let model = <Self::Entity as EntityTrait>::insert(am)
             .exec_with_returning(db)
             .await?;
-        Self::after_save(model, true)
+        Self::after_save(model, db, true).await
     }
 
     /// Perform the `UPDATE` operation on an ActiveModel
@@ -400,9 +400,9 @@ pub trait ActiveModelTrait: Clone + Debug {
         Self: ActiveModelBehavior + 'a,
         C: ConnectionTrait,
     {
-        let am = ActiveModelBehavior::before_save(self, false)?;
+        let am = ActiveModelBehavior::before_save(self, db, false).await?;
         let model: <Self::Entity as EntityTrait>::Model = Self::Entity::update(am).exec(db).await?;
-        Self::after_save(model, false)
+        Self::after_save(model, db, false).await
     }
 
     /// Insert the model if primary key is `NotSet`, update otherwise.
@@ -477,10 +477,10 @@ pub trait ActiveModelTrait: Clone + Debug {
         Self: ActiveModelBehavior + 'a,
         C: ConnectionTrait,
     {
-        let am = ActiveModelBehavior::before_delete(self)?;
+        let am = ActiveModelBehavior::before_delete(self, db).await?;
         let am_clone = am.clone();
         let delete_res = Self::Entity::delete(am).exec(db).await?;
-        ActiveModelBehavior::after_delete(am_clone)?;
+        ActiveModelBehavior::after_delete(am_clone, db).await?;
         Ok(delete_res)
     }
 
@@ -584,6 +584,7 @@ pub trait ActiveModelTrait: Clone + Debug {
 /// ```
 /// See module level docs [crate::entity] for a full example
 #[allow(unused_variables)]
+#[async_trait]
 pub trait ActiveModelBehavior: ActiveModelTrait {
     /// Create a new ActiveModel with default values. Also used by `Default::default()`.
     fn new() -> Self {
@@ -591,25 +592,38 @@ pub trait ActiveModelBehavior: ActiveModelTrait {
     }
 
     /// Will be called before saving
-    fn before_save(self, insert: bool) -> Result<Self, DbErr> {
+    async fn before_save<C>(self, db: &C, insert: bool) -> Result<Self, DbErr>
+    where
+        C: ConnectionTrait,
+    {
         Ok(self)
     }
 
     /// Will be called after saving
-    fn after_save(
+    async fn after_save<C>(
         model: <Self::Entity as EntityTrait>::Model,
+        db: &C,
         insert: bool,
-    ) -> Result<<Self::Entity as EntityTrait>::Model, DbErr> {
+    ) -> Result<<Self::Entity as EntityTrait>::Model, DbErr>
+    where
+        C: ConnectionTrait,
+    {
         Ok(model)
     }
 
     /// Will be called before deleting
-    fn before_delete(self) -> Result<Self, DbErr> {
+    async fn before_delete<C>(self, db: &C) -> Result<Self, DbErr>
+    where
+        C: ConnectionTrait,
+    {
         Ok(self)
     }
 
     /// Will be called after deleting
-    fn after_delete(self) -> Result<Self, DbErr> {
+    async fn after_delete<C>(self, db: &C) -> Result<Self, DbErr>
+    where
+        C: ConnectionTrait,
+    {
         Ok(self)
     }
 }


Test patch:
diff --git a/tests/common/bakery_chain/cake.rs b/tests/common/bakery_chain/cake.rs
index 73b70195dc..c844806fe7 100644
--- a/tests/common/bakery_chain/cake.rs
+++ b/tests/common/bakery_chain/cake.rs
@@ -1,4 +1,4 @@
-use sea_orm::entity::prelude::*;
+use sea_orm::{entity::prelude::*, ConnectionTrait};
 
 #[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
 #[sea_orm(table_name = "cake")]
@@ -49,6 +49,7 @@ impl Related<super::lineitem::Entity> for Entity {
     }
 }
 
+#[async_trait::async_trait]
 impl ActiveModelBehavior for ActiveModel {
     fn new() -> Self {
         use sea_orm::Set;
@@ -58,7 +59,10 @@ impl ActiveModelBehavior for ActiveModel {
         }
     }
 
-    fn before_save(self, insert: bool) -> Result<Self, DbErr> {
+    async fn before_save<C>(self, _db: &C, insert: bool) -> Result<Self, DbErr>
+    where
+        C: ConnectionTrait,
+    {
         use rust_decimal_macros::dec;
         if self.price.as_ref() == &dec!(0) {
             Err(DbErr::Custom(format!(
@@ -70,7 +74,10 @@ impl ActiveModelBehavior for ActiveModel {
         }
     }
 
-    fn after_save(model: Model, insert: bool) -> Result<Model, DbErr> {
+    async fn after_save<C>(model: Model, _db: &C, insert: bool) -> Result<Model, DbErr>
+    where
+        C: ConnectionTrait,
+    {
         use rust_decimal_macros::dec;
         if model.price < dec!(0) {
             Err(DbErr::Custom(format!(
@@ -82,7 +89,10 @@ impl ActiveModelBehavior for ActiveModel {
         }
     }
 
-    fn before_delete(self) -> Result<Self, DbErr> {
+    async fn before_delete<C>(self, _db: &C) -> Result<Self, DbErr>
+    where
+        C: ConnectionTrait,
+    {
         if self.name.as_ref().contains("(err_on_before_delete)") {
             Err(DbErr::Custom(
                 "[before_delete] Cannot be deleted".to_owned(),
@@ -92,7 +102,10 @@ impl ActiveModelBehavior for ActiveModel {
         }
     }
 
-    fn after_delete(self) -> Result<Self, DbErr> {
+    async fn after_delete<C>(self, _db: &C) -> Result<Self, DbErr>
+    where
+        C: ConnectionTrait,
+    {
         if self.name.as_ref().contains("(err_on_after_delete)") {
             Err(DbErr::Custom("[after_delete] Cannot be deleted".to_owned()))
         } else {
diff --git a/tests/common/features/edit_log.rs b/tests/common/features/edit_log.rs
new file mode 100644
index 0000000000..7fe5d23c26
--- /dev/null
+++ b/tests/common/features/edit_log.rs
@@ -0,0 +1,15 @@
+use sea_orm::entity::prelude::*;
+
+#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
+#[sea_orm(table_name = "edit_log")]
+pub struct Model {
+    #[sea_orm(primary_key)]
+    pub id: i32,
+    pub action: String,
+    pub values: Json,
+}
+
+#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
+pub enum Relation {}
+
+impl ActiveModelBehavior for ActiveModel {}
diff --git a/tests/common/features/mod.rs b/tests/common/features/mod.rs
index 36f76a4529..b944836337 100644
--- a/tests/common/features/mod.rs
+++ b/tests/common/features/mod.rs
@@ -4,6 +4,7 @@ pub mod applog;
 pub mod byte_primary_key;
 pub mod collection;
 pub mod custom_active_model;
+pub mod edit_log;
 pub mod event_trigger;
 pub mod insert_default;
 pub mod json_struct;
@@ -23,6 +24,7 @@ pub use active_enum_child::Entity as ActiveEnumChild;
 pub use applog::Entity as Applog;
 pub use byte_primary_key::Entity as BytePrimaryKey;
 pub use collection::Entity as Collection;
+pub use edit_log::Entity as EditLog;
 pub use event_trigger::Entity as EventTrigger;
 pub use insert_default::Entity as InsertDefault;
 pub use json_struct::Entity as JsonStruct;
diff --git a/tests/common/features/repository.rs b/tests/common/features/repository.rs
index 651a0bc519..73bbcb303d 100644
--- a/tests/common/features/repository.rs
+++ b/tests/common/features/repository.rs
@@ -1,6 +1,8 @@
-use sea_orm::entity::prelude::*;
+use super::edit_log;
+use sea_orm::{entity::prelude::*, ConnectionTrait, Set, TryIntoModel};
+use serde::Serialize;
 
-#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
+#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel, Serialize)]
 #[sea_orm(table_name = "repository")]
 pub struct Model {
     #[sea_orm(primary_key, auto_increment = false)]
@@ -13,4 +15,57 @@ pub struct Model {
 #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
 pub enum Relation {}
 
-impl ActiveModelBehavior for ActiveModel {}
+#[async_trait::async_trait]
+impl ActiveModelBehavior for ActiveModel {
+    async fn before_save<C>(self, db: &C, _: bool) -> Result<Self, DbErr>
+    where
+        C: ConnectionTrait,
+    {
+        let model = self.clone().try_into_model()?;
+        insert_edit_log("before_save", &model, db).await?;
+        Ok(self)
+    }
+
+    async fn after_save<C>(model: Model, db: &C, _: bool) -> Result<Model, DbErr>
+    where
+        C: ConnectionTrait,
+    {
+        insert_edit_log("after_save", &model, db).await?;
+        Ok(model)
+    }
+
+    async fn before_delete<C>(self, db: &C) -> Result<Self, DbErr>
+    where
+        C: ConnectionTrait,
+    {
+        let model = self.clone().try_into_model()?;
+        insert_edit_log("before_delete", &model, db).await?;
+        Ok(self)
+    }
+
+    async fn after_delete<C>(self, db: &C) -> Result<Self, DbErr>
+    where
+        C: ConnectionTrait,
+    {
+        let model = self.clone().try_into_model()?;
+        insert_edit_log("after_delete", &model, db).await?;
+        Ok(self)
+    }
+}
+
+async fn insert_edit_log<T, M, C>(action: T, model: &M, db: &C) -> Result<(), DbErr>
+where
+    T: Into<String>,
+    M: Serialize,
+    C: ConnectionTrait,
+{
+    edit_log::ActiveModel {
+        action: Set(action.into()),
+        values: Set(serde_json::json!(model)),
+        ..Default::default()
+    }
+    .insert(db)
+    .await?;
+
+    Ok(())
+}
diff --git a/tests/common/features/schema.rs b/tests/common/features/schema.rs
index 7dee0611a5..8cc8801671 100644
--- a/tests/common/features/schema.rs
+++ b/tests/common/features/schema.rs
@@ -43,6 +43,7 @@ pub async fn create_tables(db: &DatabaseConnection) -> Result<(), DbErr> {
     create_insert_default_table(db).await?;
     create_pi_table(db).await?;
     create_uuid_fmt_table(db).await?;
+    create_edit_log_table(db).await?;
 
     if DbBackend::Postgres == db_backend {
         create_collection_table(db).await?;
@@ -469,3 +470,20 @@ pub async fn create_uuid_fmt_table(db: &DbConn) -> Result<ExecResult, DbErr> {
 
     create_table(db, &stmt, UuidFmt).await
 }
+
+pub async fn create_edit_log_table(db: &DbConn) -> Result<ExecResult, DbErr> {
+    let stmt = sea_query::Table::create()
+        .table(edit_log::Entity)
+        .col(
+            ColumnDef::new(edit_log::Column::Id)
+                .integer()
+                .not_null()
+                .auto_increment()
+                .primary_key(),
+        )
+        .col(ColumnDef::new(edit_log::Column::Action).string().not_null())
+        .col(ColumnDef::new(edit_log::Column::Values).json().not_null())
+        .to_owned();
+
+    create_table(db, &stmt, EditLog).await
+}
diff --git a/tests/string_primary_key_tests.rs b/tests/string_primary_key_tests.rs
index 56496bdc47..94d1ae554c 100644
--- a/tests/string_primary_key_tests.rs
+++ b/tests/string_primary_key_tests.rs
@@ -3,6 +3,7 @@ pub mod common;
 pub use common::{features::*, setup::*, TestContext};
 use pretty_assertions::assert_eq;
 use sea_orm::{entity::prelude::*, entity::*, DatabaseConnection};
+use serde_json::json;
 
 #[sea_orm_macros::test]
 #[cfg(any(
@@ -14,13 +15,13 @@ async fn main() -> Result<(), DbErr> {
     let ctx = TestContext::new("features_schema_string_primary_key_tests").await;
     create_tables(&ctx.db).await?;
     create_and_update_repository(&ctx.db).await?;
-    insert_repository(&ctx.db).await?;
+    insert_and_delete_repository(&ctx.db).await?;
     ctx.delete().await;
 
     Ok(())
 }
 
-pub async fn insert_repository(db: &DatabaseConnection) -> Result<(), DbErr> {
+pub async fn insert_and_delete_repository(db: &DatabaseConnection) -> Result<(), DbErr> {
     let repository = repository::Model {
         id: "unique-id-001".to_owned(),
         owner: "GC".to_owned(),
@@ -41,6 +42,54 @@ pub async fn insert_repository(db: &DatabaseConnection) -> Result<(), DbErr> {
         }
     );
 
+    result.delete(db).await?;
+
+    assert_eq!(
+        edit_log::Entity::find().all(db).await?,
+        [
+            edit_log::Model {
+                id: 1,
+                action: "before_save".into(),
+                values: json!({
+                    "description": null,
+                    "id": "unique-id-001",
+                    "name": "G.C.",
+                    "owner": "GC",
+                }),
+            },
+            edit_log::Model {
+                id: 2,
+                action: "after_save".into(),
+                values: json!({
+                    "description": null,
+                    "id": "unique-id-001",
+                    "name": "G.C.",
+                    "owner": "GC",
+                }),
+            },
+            edit_log::Model {
+                id: 3,
+                action: "before_delete".into(),
+                values: json!({
+                    "description": null,
+                    "id": "unique-id-001",
+                    "name": "G.C.",
+                    "owner": "GC",
+                }),
+            },
+            edit_log::Model {
+                id: 4,
+                action: "after_delete".into(),
+                values: json!({
+                    "description": null,
+                    "id": "unique-id-001",
+                    "name": "G.C.",
+                    "owner": "GC",
+                }),
+            },
+        ]
+    );
+
     Ok(())
 }