Update ActiveModel by JSON (#492)
* Update ActiveModel by JSON * Add `ActiveModel::from_json` * Update test cases
This commit is contained in:
parent
de57934061
commit
351efd0d6b
@ -483,6 +483,71 @@ pub trait ActiveModelTrait: Clone + Debug {
|
|||||||
ActiveModelBehavior::after_delete(am_clone)?;
|
ActiveModelBehavior::after_delete(am_clone)?;
|
||||||
Ok(delete_res)
|
Ok(delete_res)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Set the corresponding attributes in the ActiveModel from a JSON value
|
||||||
|
///
|
||||||
|
/// Note that this method will not alter the primary key values in ActiveModel.
|
||||||
|
#[cfg(feature = "with-json")]
|
||||||
|
fn set_from_json(&mut self, json: serde_json::Value) -> Result<(), DbErr>
|
||||||
|
where
|
||||||
|
<<Self as ActiveModelTrait>::Entity as EntityTrait>::Model: IntoActiveModel<Self>,
|
||||||
|
for<'de> <<Self as ActiveModelTrait>::Entity as EntityTrait>::Model:
|
||||||
|
serde::de::Deserialize<'de>,
|
||||||
|
{
|
||||||
|
use crate::Iterable;
|
||||||
|
|
||||||
|
// Backup primary key values
|
||||||
|
let primary_key_values: Vec<(<Self::Entity as EntityTrait>::Column, ActiveValue<Value>)> =
|
||||||
|
<<Self::Entity as EntityTrait>::PrimaryKey>::iter()
|
||||||
|
.map(|pk| (pk.into_column(), self.take(pk.into_column())))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
// Replace all values in ActiveModel
|
||||||
|
*self = Self::from_json(json)?;
|
||||||
|
|
||||||
|
// Restore primary key values
|
||||||
|
for (col, active_value) in primary_key_values {
|
||||||
|
match active_value {
|
||||||
|
ActiveValue::Unchanged(v) | ActiveValue::Set(v) => self.set(col, v),
|
||||||
|
NotSet => self.not_set(col),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create ActiveModel from a JSON value
|
||||||
|
#[cfg(feature = "with-json")]
|
||||||
|
fn from_json(json: serde_json::Value) -> Result<Self, DbErr>
|
||||||
|
where
|
||||||
|
<<Self as ActiveModelTrait>::Entity as EntityTrait>::Model: IntoActiveModel<Self>,
|
||||||
|
for<'de> <<Self as ActiveModelTrait>::Entity as EntityTrait>::Model:
|
||||||
|
serde::de::Deserialize<'de>,
|
||||||
|
{
|
||||||
|
use crate::{Iden, Iterable};
|
||||||
|
|
||||||
|
// Mark down which attribute exists in the JSON object
|
||||||
|
let json_keys: Vec<(<Self::Entity as EntityTrait>::Column, bool)> =
|
||||||
|
<<Self::Entity as EntityTrait>::Column>::iter()
|
||||||
|
.map(|col| (col, json.get(col.to_string()).is_some()))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
// Convert JSON object into ActiveModel via Model
|
||||||
|
let model: <Self::Entity as EntityTrait>::Model =
|
||||||
|
serde_json::from_value(json).map_err(|e| DbErr::Json(e.to_string()))?;
|
||||||
|
let mut am = model.into_active_model();
|
||||||
|
|
||||||
|
// Transform attribute that exists in JSON object into ActiveValue::Set, otherwise ActiveValue::NotSet
|
||||||
|
for (col, json_key_exists) in json_keys {
|
||||||
|
if json_key_exists && !am.is_not_set(col) {
|
||||||
|
am.set(col, am.get(col).unwrap());
|
||||||
|
} else {
|
||||||
|
am.not_set(col);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(am)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A Trait for overriding the ActiveModel behavior
|
/// A Trait for overriding the ActiveModel behavior
|
||||||
@ -768,13 +833,15 @@ where
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use crate::tests_cfg::*;
|
use crate::{entity::*, tests_cfg::*, DbErr};
|
||||||
|
use pretty_assertions::assert_eq;
|
||||||
|
|
||||||
|
#[cfg(feature = "with-json")]
|
||||||
|
use serde_json::json;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
#[cfg(feature = "macros")]
|
#[cfg(feature = "macros")]
|
||||||
fn test_derive_into_active_model_1() {
|
fn test_derive_into_active_model_1() {
|
||||||
use crate::entity::*;
|
|
||||||
|
|
||||||
mod my_fruit {
|
mod my_fruit {
|
||||||
pub use super::fruit::*;
|
pub use super::fruit::*;
|
||||||
use crate as sea_orm;
|
use crate as sea_orm;
|
||||||
@ -806,8 +873,6 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
#[cfg(feature = "macros")]
|
#[cfg(feature = "macros")]
|
||||||
fn test_derive_into_active_model_2() {
|
fn test_derive_into_active_model_2() {
|
||||||
use crate::entity::*;
|
|
||||||
|
|
||||||
mod my_fruit {
|
mod my_fruit {
|
||||||
pub use super::fruit::*;
|
pub use super::fruit::*;
|
||||||
use crate as sea_orm;
|
use crate as sea_orm;
|
||||||
@ -852,4 +917,175 @@ mod tests {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[cfg(feature = "with-json")]
|
||||||
|
#[should_panic(
|
||||||
|
expected = r#"called `Result::unwrap()` on an `Err` value: Json("missing field `id`")"#
|
||||||
|
)]
|
||||||
|
fn test_active_model_set_from_json_1() {
|
||||||
|
let mut cake: cake::ActiveModel = Default::default();
|
||||||
|
|
||||||
|
cake.set_from_json(json!({
|
||||||
|
"name": "Apple Pie",
|
||||||
|
}))
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[cfg(feature = "with-json")]
|
||||||
|
fn test_active_model_set_from_json_2() -> Result<(), DbErr> {
|
||||||
|
let mut fruit: fruit::ActiveModel = Default::default();
|
||||||
|
|
||||||
|
fruit.set_from_json(json!({
|
||||||
|
"name": "Apple",
|
||||||
|
}))?;
|
||||||
|
assert_eq!(
|
||||||
|
fruit,
|
||||||
|
fruit::ActiveModel {
|
||||||
|
id: ActiveValue::NotSet,
|
||||||
|
name: ActiveValue::Set("Apple".to_owned()),
|
||||||
|
cake_id: ActiveValue::NotSet,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
fruit::ActiveModel::from_json(json!({
|
||||||
|
"name": "Apple",
|
||||||
|
}))?,
|
||||||
|
fruit::ActiveModel {
|
||||||
|
id: ActiveValue::NotSet,
|
||||||
|
name: ActiveValue::Set("Apple".to_owned()),
|
||||||
|
cake_id: ActiveValue::NotSet,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
fruit.set_from_json(json!({
|
||||||
|
"name": "Apple",
|
||||||
|
"cake_id": null,
|
||||||
|
}))?;
|
||||||
|
assert_eq!(
|
||||||
|
fruit,
|
||||||
|
fruit::ActiveModel {
|
||||||
|
id: ActiveValue::NotSet,
|
||||||
|
name: ActiveValue::Set("Apple".to_owned()),
|
||||||
|
cake_id: ActiveValue::Set(None),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
fruit.set_from_json(json!({
|
||||||
|
"id": null,
|
||||||
|
"name": "Apple",
|
||||||
|
"cake_id": 1,
|
||||||
|
}))?;
|
||||||
|
assert_eq!(
|
||||||
|
fruit,
|
||||||
|
fruit::ActiveModel {
|
||||||
|
id: ActiveValue::NotSet,
|
||||||
|
name: ActiveValue::Set("Apple".to_owned()),
|
||||||
|
cake_id: ActiveValue::Set(Some(1)),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
fruit.set_from_json(json!({
|
||||||
|
"id": 2,
|
||||||
|
"name": "Apple",
|
||||||
|
"cake_id": 1,
|
||||||
|
}))?;
|
||||||
|
assert_eq!(
|
||||||
|
fruit,
|
||||||
|
fruit::ActiveModel {
|
||||||
|
id: ActiveValue::NotSet,
|
||||||
|
name: ActiveValue::Set("Apple".to_owned()),
|
||||||
|
cake_id: ActiveValue::Set(Some(1)),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut fruit = fruit::ActiveModel {
|
||||||
|
id: ActiveValue::Set(1),
|
||||||
|
name: ActiveValue::NotSet,
|
||||||
|
cake_id: ActiveValue::NotSet,
|
||||||
|
};
|
||||||
|
fruit.set_from_json(json!({
|
||||||
|
"id": 8,
|
||||||
|
"name": "Apple",
|
||||||
|
"cake_id": 1,
|
||||||
|
}))?;
|
||||||
|
assert_eq!(
|
||||||
|
fruit,
|
||||||
|
fruit::ActiveModel {
|
||||||
|
id: ActiveValue::Set(1),
|
||||||
|
name: ActiveValue::Set("Apple".to_owned()),
|
||||||
|
cake_id: ActiveValue::Set(Some(1)),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[smol_potat::test]
|
||||||
|
#[cfg(feature = "with-json")]
|
||||||
|
async fn test_active_model_set_from_json_3() -> Result<(), DbErr> {
|
||||||
|
use crate::*;
|
||||||
|
|
||||||
|
let db = MockDatabase::new(DbBackend::Postgres)
|
||||||
|
.append_exec_results(vec![
|
||||||
|
MockExecResult {
|
||||||
|
last_insert_id: 1,
|
||||||
|
rows_affected: 1,
|
||||||
|
},
|
||||||
|
MockExecResult {
|
||||||
|
last_insert_id: 1,
|
||||||
|
rows_affected: 1,
|
||||||
|
},
|
||||||
|
])
|
||||||
|
.append_query_results(vec![
|
||||||
|
vec![fruit::Model {
|
||||||
|
id: 1,
|
||||||
|
name: "Apple".to_owned(),
|
||||||
|
cake_id: None,
|
||||||
|
}],
|
||||||
|
vec![fruit::Model {
|
||||||
|
id: 2,
|
||||||
|
name: "Orange".to_owned(),
|
||||||
|
cake_id: Some(1),
|
||||||
|
}],
|
||||||
|
])
|
||||||
|
.into_connection();
|
||||||
|
|
||||||
|
let mut fruit: fruit::ActiveModel = Default::default();
|
||||||
|
fruit.set_from_json(json!({
|
||||||
|
"name": "Apple",
|
||||||
|
}))?;
|
||||||
|
fruit.save(&db).await?;
|
||||||
|
|
||||||
|
let mut fruit = fruit::ActiveModel {
|
||||||
|
id: Set(2),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
fruit.set_from_json(json!({
|
||||||
|
"id": 9,
|
||||||
|
"name": "Orange",
|
||||||
|
"cake_id": 1,
|
||||||
|
}))?;
|
||||||
|
fruit.save(&db).await?;
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
db.into_transaction_log(),
|
||||||
|
vec![
|
||||||
|
Transaction::from_sql_and_values(
|
||||||
|
DbBackend::Postgres,
|
||||||
|
r#"INSERT INTO "fruit" ("name") VALUES ($1) RETURNING "id", "name", "cake_id""#,
|
||||||
|
vec!["Apple".into()]
|
||||||
|
),
|
||||||
|
Transaction::from_sql_and_values(
|
||||||
|
DbBackend::Postgres,
|
||||||
|
r#"UPDATE "fruit" SET "name" = $1, "cake_id" = $2 WHERE "fruit"."id" = $3 RETURNING "id", "name", "cake_id""#,
|
||||||
|
vec!["Orange".into(), 1i32.into(), 2i32.into()]
|
||||||
|
),
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -13,6 +13,8 @@ pub enum DbErr {
|
|||||||
Custom(String),
|
Custom(String),
|
||||||
/// Error occurred while parsing value as target type
|
/// Error occurred while parsing value as target type
|
||||||
Type(String),
|
Type(String),
|
||||||
|
/// Error occurred while parsing json value as target type
|
||||||
|
Json(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl std::error::Error for DbErr {}
|
impl std::error::Error for DbErr {}
|
||||||
@ -26,6 +28,7 @@ impl std::fmt::Display for DbErr {
|
|||||||
Self::RecordNotFound(s) => write!(f, "RecordNotFound Error: {}", s),
|
Self::RecordNotFound(s) => write!(f, "RecordNotFound Error: {}", s),
|
||||||
Self::Custom(s) => write!(f, "Custom Error: {}", s),
|
Self::Custom(s) => write!(f, "Custom Error: {}", s),
|
||||||
Self::Type(s) => write!(f, "Type Error: {}", s),
|
Self::Type(s) => write!(f, "Type Error: {}", s),
|
||||||
|
Self::Json(s) => write!(f, "Json Error: {}", s),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,11 @@
|
|||||||
use crate as sea_orm;
|
use crate as sea_orm;
|
||||||
use crate::entity::prelude::*;
|
use crate::entity::prelude::*;
|
||||||
|
|
||||||
|
#[cfg(feature = "with-json")]
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel)]
|
#[derive(Clone, Debug, PartialEq, DeriveEntityModel)]
|
||||||
|
#[cfg_attr(feature = "with-json", derive(Serialize, Deserialize))]
|
||||||
#[sea_orm(table_name = "cake")]
|
#[sea_orm(table_name = "cake")]
|
||||||
pub struct Model {
|
pub struct Model {
|
||||||
#[sea_orm(primary_key)]
|
#[sea_orm(primary_key)]
|
||||||
|
@ -1,10 +1,15 @@
|
|||||||
use crate as sea_orm;
|
use crate as sea_orm;
|
||||||
use crate::entity::prelude::*;
|
use crate::entity::prelude::*;
|
||||||
|
|
||||||
|
#[cfg(feature = "with-json")]
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel)]
|
#[derive(Clone, Debug, PartialEq, DeriveEntityModel)]
|
||||||
|
#[cfg_attr(feature = "with-json", derive(Serialize, Deserialize))]
|
||||||
#[sea_orm(table_name = "fruit")]
|
#[sea_orm(table_name = "fruit")]
|
||||||
pub struct Model {
|
pub struct Model {
|
||||||
#[sea_orm(primary_key)]
|
#[sea_orm(primary_key)]
|
||||||
|
#[serde(skip_deserializing)]
|
||||||
pub id: i32,
|
pub id: i32,
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub cake_id: Option<i32>,
|
pub cake_id: Option<i32>,
|
||||||
|
Loading…
x
Reference in New Issue
Block a user