Skip to content

Commit f2ffa10

Browse files
committed
NestedSaveBuilder: async relation save ops
Replace stored JSON relation snapshots with boxed async save operations so related models are persisted during nested saves. Adds OneRelationSaveOp/ManyRelationSaveOp types and helper functions (save_related_model_as_json, save_related_models_as_json) that apply foreign keys, run callbacks, and serialize results. Updates NestedSaveBuilder.one_relations/many_relations to store closures that return futures, adjusts with_one/with_many to capture foreign_key and required trait bounds (IntoActiveModel for bulk inserts), and awaits these ops when saving the parent. Also adds a test (nested_save_builder_persists_related_models_with_parent_fk) to verify related children are persisted with the parent's FK. Includes small imports for Future/Pin and error handling for serialization.
1 parent 7f56d6d commit f2ffa10

2 files changed

Lines changed: 123 additions & 36 deletions

File tree

src/model/nested.rs

Lines changed: 85 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,23 @@
22

33
use async_trait::async_trait;
44
use sea_orm::sea_query::OnConflict;
5+
use std::future::Future;
6+
use std::pin::Pin;
57

68
use crate::callbacks::{AfterCreateDispatch, AfterDeleteDispatch, AfterUpdateDispatch, BeforeCreateDispatch, BeforeDeleteDispatch, BeforeUpdateDispatch};
79
use crate::error::{Error, Result};
810
use crate::internal::{EntityTrait, InternalModel, IntoActiveModel, translate_error};
911

1012
use super::Model;
1113

14+
type OneRelationSaveOp = Box<
15+
dyn FnOnce(serde_json::Value) -> Pin<Box<dyn Future<Output = Result<serde_json::Value>>>>,
16+
>;
17+
18+
type ManyRelationSaveOp = Box<
19+
dyn FnOnce(serde_json::Value) -> Pin<Box<dyn Future<Output = Result<Vec<serde_json::Value>>>>>,
20+
>;
21+
1222
fn parent_primary_key_json<M: Model>(parent: &M) -> serde_json::Value {
1323
let pk = parent.primary_key();
1424
serde_json::Value::String(format!("{}", pk))
@@ -163,6 +173,54 @@ where
163173
reorder_models_by_primary_key(fetched, &pk_order)
164174
}
165175

176+
async fn save_related_model_as_json<R>(
177+
related: R,
178+
foreign_key: String,
179+
parent_pk_value: serde_json::Value,
180+
) -> Result<serde_json::Value>
181+
where
182+
R: Model,
183+
{
184+
let related = apply_foreign_key(related, &foreign_key, &parent_pk_value)?;
185+
let related = related.save().await?;
186+
serde_json::to_value(&related)
187+
.map_err(|e| Error::conversion(format!("Failed to serialize related model: {}", e)))
188+
}
189+
190+
async fn save_related_models_as_json<R>(
191+
related: Vec<R>,
192+
foreign_key: String,
193+
parent_pk_value: serde_json::Value,
194+
) -> Result<Vec<serde_json::Value>>
195+
where
196+
R: Model,
197+
<<R as InternalModel>::Entity as EntityTrait>::Model: IntoActiveModel<R::ActiveModel>,
198+
{
199+
if related.is_empty() {
200+
return Ok(Vec::new());
201+
}
202+
203+
let mut prepared_related = Vec::with_capacity(related.len());
204+
for item in related {
205+
let mut item = apply_foreign_key(item, &foreign_key, &parent_pk_value)?;
206+
(&mut item).run_before_create()?;
207+
prepared_related.push(item);
208+
}
209+
210+
let saved_related = R::insert_all(prepared_related).await?;
211+
let mut saved_json = Vec::with_capacity(saved_related.len());
212+
for item in saved_related {
213+
(&item).run_after_create()?;
214+
saved_json.push(
215+
serde_json::to_value(&item).map_err(|e| {
216+
Error::conversion(format!("Failed to serialize related model: {}", e))
217+
})?,
218+
);
219+
}
220+
221+
Ok(saved_json)
222+
}
223+
166224
/// Extension trait for cascade save operations.
167225
#[async_trait]
168226
pub trait NestedSave: Model {
@@ -305,8 +363,8 @@ impl<M: Model> NestedSave for M {}
305363
/// Builder for nested/cascade saves.
306364
pub struct NestedSaveBuilder<M: Model> {
307365
parent: M,
308-
one_relations: Vec<(serde_json::Value, String)>,
309-
many_relations: Vec<(Vec<serde_json::Value>, String)>,
366+
one_relations: Vec<OneRelationSaveOp>,
367+
many_relations: Vec<ManyRelationSaveOp>,
310368
}
311369

312370
impl<M: Model> NestedSaveBuilder<M> {
@@ -318,20 +376,30 @@ impl<M: Model> NestedSaveBuilder<M> {
318376
}
319377
}
320378

321-
pub fn with_one<R: Model>(mut self, related: R, foreign_key: &str) -> Self {
322-
if let Ok(json) = serde_json::to_value(&related) {
323-
self.one_relations.push((json, foreign_key.to_string()));
324-
}
379+
pub fn with_one<R: Model + 'static>(mut self, related: R, foreign_key: &str) -> Self {
380+
let foreign_key = foreign_key.to_string();
381+
self.one_relations.push(Box::new(move |parent_pk_value| {
382+
Box::pin(save_related_model_as_json(
383+
related,
384+
foreign_key,
385+
parent_pk_value,
386+
))
387+
}));
325388
self
326389
}
327390

328-
pub fn with_many<R: Model>(mut self, related: Vec<R>, foreign_key: &str) -> Self {
329-
let json_values: Vec<serde_json::Value> = related
330-
.into_iter()
331-
.filter_map(|r| serde_json::to_value(&r).ok())
332-
.collect();
333-
self.many_relations
334-
.push((json_values, foreign_key.to_string()));
391+
pub fn with_many<R: Model + 'static>(mut self, related: Vec<R>, foreign_key: &str) -> Self
392+
where
393+
<<R as InternalModel>::Entity as EntityTrait>::Model: IntoActiveModel<R::ActiveModel>,
394+
{
395+
let foreign_key = foreign_key.to_string();
396+
self.many_relations.push(Box::new(move |parent_pk_value| {
397+
Box::pin(save_related_models_as_json(
398+
related,
399+
foreign_key,
400+
parent_pk_value,
401+
))
402+
}));
335403
self
336404
}
337405

@@ -345,30 +413,12 @@ impl<M: Model> NestedSaveBuilder<M> {
345413

346414
let mut saved_json = Vec::new();
347415

348-
for (mut json, fk) in self.one_relations {
349-
if let serde_json::Value::Object(ref mut map) = json {
350-
let pk_str = pk_value.as_str().unwrap_or_default();
351-
if let Ok(pk_i64) = pk_str.parse::<i64>() {
352-
map.insert(fk, serde_json::json!(pk_i64));
353-
} else {
354-
map.insert(fk, pk_value.clone());
355-
}
356-
}
357-
saved_json.push(json);
416+
for save_relation in self.one_relations {
417+
saved_json.push(save_relation(pk_value.clone()).await?);
358418
}
359419

360-
for (items, fk) in self.many_relations {
361-
for mut json in items {
362-
if let serde_json::Value::Object(ref mut map) = json {
363-
let pk_str = pk_value.as_str().unwrap_or_default();
364-
if let Ok(pk_i64) = pk_str.parse::<i64>() {
365-
map.insert(fk.clone(), serde_json::json!(pk_i64));
366-
} else {
367-
map.insert(fk.clone(), pk_value.clone());
368-
}
369-
}
370-
saved_json.push(json);
371-
}
420+
for save_relations in self.many_relations {
421+
saved_json.extend(save_relations(pk_value.clone()).await?);
372422
}
373423

374424
Ok((parent, saved_json))

src/testing/model_nested_tests.rs

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use super::NestedSave;
1+
use super::{NestedSave, NestedSaveBuilder};
22
use crate::internal::ConnectionTrait;
33
use crate::model::Model;
44
use crate::{Database, GlobalProfiler, TideConfig};
@@ -91,6 +91,43 @@ async fn save_with_many_persists_children_with_parent_fk() {
9191
assert_eq!(fetched, saved_children);
9292
}
9393

94+
#[tokio::test]
95+
async fn nested_save_builder_persists_related_models_with_parent_fk() {
96+
let _db = setup_nested_test_db().await;
97+
98+
let parent = NestedTestParent {
99+
id: 0,
100+
name: "parent".to_string(),
101+
};
102+
103+
let (saved_parent, saved_related) = NestedSaveBuilder::new(parent)
104+
.with_many(nested_children(&["alpha", "beta"]), "parent_id")
105+
.save()
106+
.await
107+
.expect("nested builder save should succeed");
108+
109+
assert!(saved_parent.id > 0);
110+
assert_eq!(saved_related.len(), 2);
111+
112+
let fetched = NestedTestChild::query()
113+
.where_eq("parent_id", saved_parent.id)
114+
.order_by("id", crate::query::Order::Asc)
115+
.get()
116+
.await
117+
.expect("should fetch nested children saved by builder");
118+
119+
assert_eq!(fetched.len(), 2);
120+
assert_eq!(fetched[0].name, "alpha");
121+
assert_eq!(fetched[1].name, "beta");
122+
assert!(fetched.iter().all(|child| child.parent_id == saved_parent.id));
123+
124+
let returned_parent_ids: Vec<_> = saved_related
125+
.iter()
126+
.map(|value| value.get("parent_id").and_then(serde_json::Value::as_i64))
127+
.collect();
128+
assert_eq!(returned_parent_ids, vec![Some(saved_parent.id), Some(saved_parent.id)]);
129+
}
130+
94131
#[tokio::test]
95132
async fn delete_with_many_uses_bulk_delete_for_related_models() {
96133
let _db = setup_nested_test_db().await;

0 commit comments

Comments
 (0)