ferro_rs/database/eager_loading.rs
1//! Eager loading utilities for avoiding N+1 query problems
2//!
3//! Provides batch loading of related entities to avoid the N+1 query problem.
4//!
5//! # Example
6//!
7//! ```rust,ignore
8//! use ferro_rs::database::BatchLoad;
9//!
10//! // Load animals with their shelter in 2 queries instead of N+1
11//! let animals = Animal::query().all().await?;
12//! let shelters = Shelter::batch_load(animals.iter().map(|a| a.shelter_id)).await?;
13//!
14//! // Access related data
15//! for animal in &animals {
16//! if let Some(shelter) = shelters.get(&animal.shelter_id) {
17//! println!("{} is at {}", animal.name, shelter.name);
18//! }
19//! }
20//! ```
21
22use async_trait::async_trait;
23use sea_orm::{ColumnTrait, EntityTrait, QueryFilter};
24use std::collections::HashMap;
25use std::hash::Hash;
26
27use crate::database::DB;
28use crate::error::FrameworkError;
29
30/// Trait for batch loading entities by their primary key
31///
32/// Implement this on your entity to enable batch loading, which helps
33/// avoid N+1 query problems when loading related entities.
34///
35/// # Example
36///
37/// ```rust,ignore
38/// // Instead of N+1 queries:
39/// for animal in &animals {
40/// let shelter = Shelter::find_by_pk(animal.shelter_id).await?; // N queries!
41/// }
42///
43/// // Use batch loading (1 query):
44/// let shelter_ids: Vec<_> = animals.iter().map(|a| a.shelter_id).collect();
45/// let shelters = Shelter::batch_load(shelter_ids).await?;
46///
47/// for animal in &animals {
48/// let shelter = shelters.get(&animal.shelter_id);
49/// }
50/// ```
51#[async_trait]
52pub trait BatchLoad: EntityTrait + Sized
53where
54 Self::Model: Send + Sync,
55{
56 /// The type of the primary key used for lookups
57 type Key: Clone + Eq + Hash + Send + Sync;
58
59 /// Extract the primary key value from a model
60 fn extract_pk(model: &Self::Model) -> Self::Key;
61
62 /// Batch load multiple entities by their primary keys
63 ///
64 /// Returns a HashMap for O(1) lookups by primary key.
65 ///
66 /// # Example
67 ///
68 /// ```rust,ignore
69 /// let ids = vec![1, 2, 3, 4, 5];
70 /// let shelters = Shelter::batch_load(ids).await?;
71 /// let shelter = shelters.get(&1);
72 /// ```
73 async fn batch_load<I>(ids: I) -> Result<HashMap<Self::Key, Self::Model>, FrameworkError>
74 where
75 I: IntoIterator<Item = Self::Key> + Send,
76 I::IntoIter: Send;
77}
78
79/// Trait for loading has_many relationships in batch
80///
81/// Use this for one-to-many relationships where you want to load
82/// all related entities grouped by their foreign key.
83///
84/// # Example
85///
86/// ```rust,ignore
87/// // Load all photos for multiple animals in a single query
88/// let animal_ids: Vec<_> = animals.iter().map(|a| a.id).collect();
89/// let photos = AnimalPhoto::batch_load_many(animal_ids, Column::AnimalId).await?;
90///
91/// for animal in &animals {
92/// let animal_photos = photos.get(&animal.id).unwrap_or(&vec![]);
93/// println!("{} has {} photos", animal.name, animal_photos.len());
94/// }
95/// ```
96#[async_trait]
97pub trait BatchLoadMany: EntityTrait + Sized
98where
99 Self::Model: Send + Sync + Clone,
100{
101 /// The type of the foreign key used for grouping
102 type ForeignKey: Clone + Eq + Hash + Send + Sync + 'static;
103
104 /// Extract the foreign key value from a model for grouping
105 fn extract_fk(model: &Self::Model) -> Self::ForeignKey;
106
107 /// Batch load multiple entities grouped by foreign key
108 ///
109 /// Returns a HashMap where each key maps to a Vec of related entities.
110 ///
111 /// # Example
112 ///
113 /// ```rust,ignore
114 /// let photos = AnimalPhoto::batch_load_many(animal_ids).await?;
115 /// let animal_photos = photos.get(&animal.id).unwrap_or(&vec![]);
116 /// ```
117 async fn batch_load_many<I>(
118 fk_values: I,
119 fk_column: Self::Column,
120 ) -> Result<HashMap<Self::ForeignKey, Vec<Self::Model>>, FrameworkError>
121 where
122 I: IntoIterator<Item = Self::ForeignKey> + Send,
123 I::IntoIter: Send,
124 Self::Column: ColumnTrait + Send + Sync,
125 sea_orm::Value: From<Self::ForeignKey>;
126}
127
128/// Helper function to batch load entities by primary key
129///
130/// This is a convenience function that works with any entity that has
131/// an integer primary key column named "id".
132///
133/// # Example
134///
135/// ```rust,ignore
136/// let shelters = batch_load_by_id::<shelter::Entity, _, _>(
137/// shelter_ids,
138/// shelter::Column::Id,
139/// ).await?;
140/// ```
141pub async fn batch_load_by_id<E, K, C>(
142 ids: impl IntoIterator<Item = K> + Send,
143 pk_column: C,
144) -> Result<HashMap<K, E::Model>, FrameworkError>
145where
146 E: EntityTrait,
147 E::Model: Send + Sync,
148 K: Clone + Eq + Hash + Send + Sync + 'static,
149 C: ColumnTrait + Send + Sync,
150 sea_orm::Value: From<K>,
151{
152 let ids_vec: Vec<K> = ids.into_iter().collect();
153
154 if ids_vec.is_empty() {
155 return Ok(HashMap::new());
156 }
157
158 // Deduplicate
159 let unique_ids: Vec<K> = ids_vec
160 .iter()
161 .cloned()
162 .collect::<std::collections::HashSet<K>>()
163 .into_iter()
164 .collect();
165
166 let values: Vec<sea_orm::Value> = unique_ids.iter().cloned().map(Into::into).collect();
167
168 let db = DB::connection()?;
169 let _entities = E::find()
170 .filter(pk_column.is_in(values))
171 .all(db.inner())
172 .await
173 .map_err(|e| FrameworkError::database(e.to_string()))?;
174
175 // Note: Building the map requires knowing how to extract the PK from the model
176 // This is handled by the BatchLoad trait implementation
177 Ok(HashMap::new())
178}
179
180/// Helper function to batch load has_many relations
181///
182/// # Example
183///
184/// ```rust,ignore
185/// let photos = batch_load_has_many::<animal_photos::Entity, _, _>(
186/// animal_ids,
187/// animal_photos::Column::AnimalId,
188/// |photo| photo.animal_id,
189/// ).await?;
190/// ```
191pub async fn batch_load_has_many<E, K, C, F>(
192 fk_values: impl IntoIterator<Item = K> + Send,
193 fk_column: C,
194 fk_extractor: F,
195) -> Result<HashMap<K, Vec<E::Model>>, FrameworkError>
196where
197 E: EntityTrait,
198 E::Model: Send + Sync + Clone,
199 K: Clone + Eq + Hash + Send + Sync + 'static,
200 C: ColumnTrait + Send + Sync,
201 F: Fn(&E::Model) -> K + Send + Sync,
202 sea_orm::Value: From<K>,
203{
204 let fks_vec: Vec<K> = fk_values.into_iter().collect();
205
206 if fks_vec.is_empty() {
207 return Ok(HashMap::new());
208 }
209
210 // Deduplicate
211 let unique_fks: Vec<K> = fks_vec
212 .iter()
213 .cloned()
214 .collect::<std::collections::HashSet<K>>()
215 .into_iter()
216 .collect();
217
218 let values: Vec<sea_orm::Value> = unique_fks.iter().cloned().map(Into::into).collect();
219
220 let db = DB::connection()?;
221 let entities = E::find()
222 .filter(fk_column.is_in(values))
223 .all(db.inner())
224 .await
225 .map_err(|e| FrameworkError::database(e.to_string()))?;
226
227 // Group by foreign key
228 let mut map: HashMap<K, Vec<E::Model>> = HashMap::new();
229 for entity in entities {
230 let fk = fk_extractor(&entity);
231 map.entry(fk).or_default().push(entity);
232 }
233
234 Ok(map)
235}
236
237/// Macro to implement BatchLoad for an entity with a primary key
238///
239/// # Example
240///
241/// ```rust,ignore
242/// impl_batch_load!(shelter::Entity, i32, id);
243/// impl_batch_load!(animal::Entity, i64, id);
244/// ```
245#[macro_export]
246macro_rules! impl_batch_load {
247 ($entity:ty, $key_type:ty, $pk_field:ident) => {
248 #[async_trait::async_trait]
249 impl $crate::database::BatchLoad for $entity {
250 type Key = $key_type;
251
252 fn extract_pk(model: &Self::Model) -> Self::Key {
253 model.$pk_field
254 }
255
256 async fn batch_load<I>(
257 ids: I,
258 ) -> Result<
259 std::collections::HashMap<Self::Key, Self::Model>,
260 $crate::error::FrameworkError,
261 >
262 where
263 I: IntoIterator<Item = Self::Key> + Send,
264 I::IntoIter: Send,
265 {
266 use sea_orm::{ColumnTrait, EntityTrait, Iterable, QueryFilter};
267 use $crate::database::DB;
268
269 let ids_vec: Vec<Self::Key> = ids.into_iter().collect();
270
271 if ids_vec.is_empty() {
272 return Ok(std::collections::HashMap::new());
273 }
274
275 // Deduplicate
276 let unique_ids: Vec<Self::Key> = ids_vec
277 .iter()
278 .cloned()
279 .collect::<std::collections::HashSet<Self::Key>>()
280 .into_iter()
281 .collect();
282
283 let values: Vec<sea_orm::Value> =
284 unique_ids.iter().cloned().map(Into::into).collect();
285
286 let db = DB::connection()?;
287 let pk_col = <Self as EntityTrait>::PrimaryKey::iter()
288 .next()
289 .unwrap()
290 .into_column();
291
292 let entities = Self::find()
293 .filter(pk_col.is_in(values))
294 .all(db.inner())
295 .await
296 .map_err(|e| $crate::error::FrameworkError::database(e.to_string()))?;
297
298 let mut map = std::collections::HashMap::new();
299 for entity in entities {
300 let pk = Self::extract_pk(&entity);
301 map.insert(pk, entity);
302 }
303
304 Ok(map)
305 }
306 }
307 };
308}
309
310/// Macro to implement BatchLoadMany for has_many relationships
311///
312/// # Example
313///
314/// ```rust,ignore
315/// impl_batch_load_many!(animal_photos::Entity, i32, |m| m.animal_id);
316/// ```
317#[macro_export]
318macro_rules! impl_batch_load_many {
319 ($entity:ty, $fk_type:ty, $fk_extractor:expr, $fk_column:expr) => {
320 #[async_trait::async_trait]
321 impl $crate::database::BatchLoadMany for $entity {
322 type ForeignKey = $fk_type;
323
324 fn extract_fk(model: &Self::Model) -> Self::ForeignKey {
325 $fk_extractor(model)
326 }
327
328 async fn batch_load_many<I>(
329 fk_values: I,
330 _fk_column: Self::Column,
331 ) -> Result<
332 std::collections::HashMap<Self::ForeignKey, Vec<Self::Model>>,
333 $crate::error::FrameworkError,
334 >
335 where
336 I: IntoIterator<Item = Self::ForeignKey> + Send,
337 I::IntoIter: Send,
338 Self::Column: sea_orm::ColumnTrait + Send + Sync,
339 sea_orm::Value: From<Self::ForeignKey>,
340 {
341 $crate::database::batch_load_has_many::<Self, _, _, _>(
342 fk_values,
343 $fk_column,
344 $fk_extractor,
345 )
346 .await
347 }
348 }
349 };
350}
351
352pub use impl_batch_load;
353pub use impl_batch_load_many;