Skip to main content

sz_orm_core/
entity_graph.rs

1//! Entity Graph + @BatchSize 批量抓取
2//!
3//! 对应文档 6.8 节改进项 27(Entity Graph)+ 28(@BatchSize 批量抓取)。
4//!
5//! # 核心概念
6//!
7//! - **EntityGraph**:实体图,定义一组关联关系一起抓取,避免 N+1 查询
8//! - **BatchSizeConfig**:批量抓取配置(大小 + 策略)
9//! - **BatchLoader**:通用批量加载器,将 N 次单条查询合并为 ⌈N/batch_size⌉ 次批量查询
10//! - **BatchStrategy**:批量策略(IN / JOIN / SUBQUERY)
11//!
12//! # 设计灵感
13//!
14//! - Hibernate `@NamedEntityGraph` / `@BatchSize`
15//! - Doctrine `FetchMode::EAGER` / partial
16//! - Django `select_related` / `prefetch_related`
17//! - Sequelize `include` + `separate: true`
18//!
19//! # 使用示例
20//!
21//! ```
22//! use sz_orm_core::entity_graph::{EntityGraph, BatchLoader, BatchSizeConfig, BatchStrategy};
23//! use std::collections::HashMap;
24//!
25//! // 1. 定义 EntityGraph:抓取 user.posts.comments
26//! let mut graph = EntityGraph::new();
27//! graph.add_edge("user", "posts");
28//! graph.add_edge_with_graph("posts", "comments", {
29//!     let mut sub = EntityGraph::new();
30//!     sub.add_edge("comments", "author");
31//!     sub
32//! });
33//!
34//! // 2. 使用 BatchLoader 批量加载用户
35//! fn load_users(ids: &[i64]) -> HashMap<i64, String> {
36//!     ids.iter().map(|id| (*id, format!("user_{}", id))).collect()
37//! }
38//!
39//! let loader = BatchLoader::new(100, Box::new(load_users));
40//! let users = loader.load_many(&[1, 2, 3, 4, 5]);
41//! assert_eq!(users.len(), 5);
42//! assert_eq!(users.get(&3), Some(&"user_3".to_string()));
43//! ```
44
45use std::collections::HashMap;
46use std::hash::Hash;
47use std::sync::RwLock;
48
49// ============================================================================
50// EntityGraph — 实体图
51// ============================================================================
52
53/// 实体图边(关联关系)
54#[derive(Debug, Clone)]
55pub struct GraphEdge {
56    /// 父字段名
57    pub parent_field: String,
58    /// 关联名(如 "posts"、"comments")
59    pub relation: String,
60    /// 嵌套子图(用于递归抓取)
61    pub sub_graph: Option<Box<EntityGraph>>,
62}
63
64/// 实体图
65///
66/// 描述一组关联关系的抓取计划。
67///
68/// # 示例
69///
70/// ```
71/// use sz_orm_core::entity_graph::EntityGraph;
72///
73/// let mut graph = EntityGraph::new();
74/// graph.add_edge("user", "profile");
75/// graph.add_edge("user", "posts");
76/// ```
77#[derive(Debug, Clone, Default)]
78pub struct EntityGraph {
79    /// 边列表
80    edges: Vec<GraphEdge>,
81}
82
83impl EntityGraph {
84    /// 创建空实体图
85    pub fn new() -> Self {
86        Self { edges: Vec::new() }
87    }
88
89    /// 添加一条边
90    pub fn add_edge(
91        &mut self,
92        parent_field: impl Into<String>,
93        relation: impl Into<String>,
94    ) -> &mut Self {
95        self.edges.push(GraphEdge {
96            parent_field: parent_field.into(),
97            relation: relation.into(),
98            sub_graph: None,
99        });
100        self
101    }
102
103    /// 添加一条带子图的边(嵌套抓取)
104    pub fn add_edge_with_graph(
105        &mut self,
106        parent_field: impl Into<String>,
107        relation: impl Into<String>,
108        sub_graph: EntityGraph,
109    ) -> &mut Self {
110        self.edges.push(GraphEdge {
111            parent_field: parent_field.into(),
112            relation: relation.into(),
113            sub_graph: Some(Box::new(sub_graph)),
114        });
115        self
116    }
117
118    /// 返回所有边
119    pub fn edges(&self) -> &[GraphEdge] {
120        &self.edges
121    }
122
123    /// 返回边的数量
124    pub fn edge_count(&self) -> usize {
125        self.edges.len()
126    }
127
128    /// 查询某个父字段的所有关联
129    pub fn relations_of(&self, parent_field: &str) -> Vec<&GraphEdge> {
130        self.edges
131            .iter()
132            .filter(|e| e.parent_field == parent_field)
133            .collect()
134    }
135
136    /// 收集图中所有关联名(去重)
137    pub fn all_relations(&self) -> Vec<String> {
138        let mut rels: Vec<String> = self.edges.iter().map(|e| e.relation.clone()).collect();
139        rels.sort();
140        rels.dedup();
141        rels
142    }
143
144    /// 收集图中所有父字段(去重)
145    pub fn all_parent_fields(&self) -> Vec<String> {
146        let mut fields: Vec<String> = self.edges.iter().map(|e| e.parent_field.clone()).collect();
147        fields.sort();
148        fields.dedup();
149        fields
150    }
151
152    /// 是否为空图
153    pub fn is_empty(&self) -> bool {
154        self.edges.is_empty()
155    }
156
157    /// 递归收集图中所有关联(含子图)
158    pub fn all_relations_recursive(&self) -> Vec<String> {
159        let mut result = Vec::new();
160        for edge in &self.edges {
161            result.push(edge.relation.clone());
162            if let Some(sub) = &edge.sub_graph {
163                result.extend(sub.all_relations_recursive());
164            }
165        }
166        result.sort();
167        result.dedup();
168        result
169    }
170}
171
172// ============================================================================
173// BatchStrategy — 批量策略
174// ============================================================================
175
176/// 批量抓取策略
177#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
178pub enum BatchStrategy {
179    /// 使用 `WHERE id IN (?, ?, ...)` 子句批量加载
180    ///
181    /// 适用场景:关联数量较少、目标表无索引时的备选方案
182    #[default]
183    In,
184    /// 使用 `LEFT JOIN` 一次性加载所有关联
185    ///
186    /// 适用场景:关联数量较少、需要原子性读取
187    Join,
188    /// 使用 `WHERE id IN (SELECT ... FROM ...)` 子查询批量加载
189    ///
190    /// 适用场景:子查询可被数据库优化器优化时
191    Subquery,
192}
193
194impl BatchStrategy {
195    /// 策略名称
196    pub fn name(&self) -> &'static str {
197        match self {
198            BatchStrategy::In => "in",
199            BatchStrategy::Join => "join",
200            BatchStrategy::Subquery => "subquery",
201        }
202    }
203
204    /// 生成 IN 子句的 SQL 片段
205    ///
206    /// 返回形如 `"id IN (?, ?, ?)"` 的字符串(占位符数量与 values 一致)。
207    pub fn render_in_clause(column: &str, placeholders: usize) -> String {
208        if placeholders == 0 {
209            return format!("{} IN ()", column);
210        }
211        let marks: Vec<&str> = vec!["?"; placeholders];
212        format!("{} IN ({})", column, marks.join(", "))
213    }
214}
215
216// ============================================================================
217// BatchSizeConfig — 批量大小配置
218// ============================================================================
219
220/// 批量大小配置
221///
222/// 对应 Hibernate `@BatchSize(size = 100)` 注解。
223#[derive(Debug, Clone, Copy)]
224pub struct BatchSizeConfig {
225    /// 每批数量
226    pub size: usize,
227    /// 抓取策略
228    pub strategy: BatchStrategy,
229}
230
231impl Default for BatchSizeConfig {
232    fn default() -> Self {
233        Self {
234            size: 100,
235            strategy: BatchStrategy::In,
236        }
237    }
238}
239
240impl BatchSizeConfig {
241    /// 创建配置
242    pub fn new(size: usize, strategy: BatchStrategy) -> Self {
243        Self { size, strategy }
244    }
245
246    /// 创建默认策略的配置(IN)
247    pub fn with_size(size: usize) -> Self {
248        Self {
249            size,
250            strategy: BatchStrategy::In,
251        }
252    }
253
254    /// 计算给定总数需要分多少批
255    ///
256    /// # 示例
257    ///
258    /// ```
259    /// use sz_orm_core::entity_graph::BatchSizeConfig;
260    ///
261    /// let config = BatchSizeConfig::with_size(100);
262    /// assert_eq!(config.batch_count(0), 0);
263    /// assert_eq!(config.batch_count(1), 1);
264    /// assert_eq!(config.batch_count(100), 1);
265    /// assert_eq!(config.batch_count(101), 2);
266    /// assert_eq!(config.batch_count(250), 3);
267    /// ```
268    pub fn batch_count(&self, total: usize) -> usize {
269        if total == 0 {
270            0
271        } else {
272            total.div_ceil(self.size)
273        }
274    }
275
276    /// 返回第 `batch_index` 批的范围(start..end,end 不超过 total)
277    ///
278    /// # 示例
279    ///
280    /// ```
281    /// use sz_orm_core::entity_graph::BatchSizeConfig;
282    ///
283    /// let config = BatchSizeConfig::with_size(100);
284    /// assert_eq!(config.batch_range(0, 250), 0..100);
285    /// assert_eq!(config.batch_range(1, 250), 100..200);
286    /// assert_eq!(config.batch_range(2, 250), 200..250);
287    /// ```
288    pub fn batch_range(&self, batch_index: usize, total: usize) -> std::ops::Range<usize> {
289        let start = batch_index * self.size;
290        let end = (start + self.size).min(total);
291        start..end
292    }
293}
294
295// ============================================================================
296// BatchLoader — 通用批量加载器
297// ============================================================================
298
299/// 批量加载函数类型
300pub type BatchLoaderFn<K, V> = Box<dyn Fn(&[K]) -> HashMap<K, V> + Send + Sync>;
301
302/// 批量加载器
303///
304/// 将 N 个单条加载请求合并为 ⌈N/batch_size⌉ 次批量加载,避免 N+1 查询问题。
305///
306/// # 泛型参数
307///
308/// - `K`:主键类型(必须实现 `Hash + Eq + Clone`)
309/// - `V`:值类型
310///
311/// # 示例
312///
313/// ```
314/// use sz_orm_core::entity_graph::BatchLoader;
315/// use std::collections::HashMap;
316///
317/// fn load_users(ids: &[i64]) -> HashMap<i64, String> {
318///     ids.iter().map(|id| (*id, format!("user_{}", id))).collect()
319/// }
320///
321/// let loader = BatchLoader::new(100, Box::new(load_users));
322/// let users = loader.load_many(&[1, 2, 3]);
323/// assert_eq!(users.len(), 3);
324/// ```
325pub struct BatchLoader<K, V>
326where
327    K: Hash + Eq + Clone + Send + Sync,
328    V: Clone + Send + Sync,
329{
330    /// 每批数量
331    batch_size: usize,
332    /// 实际加载函数
333    loader: BatchLoaderFn<K, V>,
334    /// 缓存(避免重复加载相同的 key)
335    cache: RwLock<HashMap<K, V>>,
336}
337
338impl<K, V> BatchLoader<K, V>
339where
340    K: Hash + Eq + Clone + Send + Sync,
341    V: Clone + Send + Sync,
342{
343    /// 创建批量加载器
344    ///
345    /// # 参数
346    /// - `batch_size`:每批数量
347    /// - `loader`:实际加载函数,接收一批 key,返回 key→value 的 HashMap
348    pub fn new(batch_size: usize, loader: BatchLoaderFn<K, V>) -> Self {
349        Self {
350            batch_size,
351            loader,
352            cache: RwLock::new(HashMap::new()),
353        }
354    }
355
356    /// 批量加载多个 key
357    ///
358    /// - 自动跳过缓存中已有的 key
359    /// - 按 batch_size 分批调用 loader
360    /// - 返回所有 key 对应的 value(包含缓存与新加载的)
361    pub fn load_many(&self, keys: &[K]) -> HashMap<K, V> {
362        let mut result: HashMap<K, V> = HashMap::new();
363
364        // 1. 从缓存读取
365        let cached = self.cache.read().unwrap();
366        let mut to_load: Vec<K> = Vec::new();
367        for k in keys {
368            if let Some(v) = cached.get(k) {
369                result.insert(k.clone(), v.clone());
370            } else {
371                to_load.push(k.clone());
372            }
373        }
374        drop(cached);
375
376        if to_load.is_empty() {
377            return result;
378        }
379
380        // 2. 分批加载
381        let batch_size = self.batch_size.max(1);
382        let mut all_loaded: HashMap<K, V> = HashMap::new();
383        for chunk in to_load.chunks(batch_size) {
384            let loaded = (self.loader)(chunk);
385            all_loaded.extend(loaded);
386        }
387
388        // 3. 写入缓存
389        let mut cache = self.cache.write().unwrap();
390        for (k, v) in &all_loaded {
391            cache.insert(k.clone(), v.clone());
392        }
393        drop(cache);
394
395        // 4. 合并结果
396        result.extend(all_loaded);
397        result
398    }
399
400    /// 加载单个 key(便捷方法)
401    pub fn load_one(&self, key: &K) -> Option<V> {
402        let result = self.load_many(std::slice::from_ref(key));
403        result.get(key).cloned()
404    }
405
406    /// 清空缓存
407    pub fn clear_cache(&self) {
408        self.cache.write().unwrap().clear();
409    }
410
411    /// 返回当前缓存大小
412    pub fn cache_size(&self) -> usize {
413        self.cache.read().unwrap().len()
414    }
415
416    /// 返回 batch_size
417    pub fn batch_size(&self) -> usize {
418        self.batch_size
419    }
420}
421
422// ============================================================================
423// 单元测试
424// ============================================================================
425
426#[cfg(test)]
427mod tests {
428    use super::*;
429
430    // ===== EntityGraph 测试 =====
431
432    #[test]
433    fn test_new_graph_is_empty() {
434        let g = EntityGraph::new();
435        assert!(g.is_empty());
436        assert_eq!(g.edge_count(), 0);
437    }
438
439    #[test]
440    fn test_add_edge() {
441        let mut g = EntityGraph::new();
442        g.add_edge("user", "posts");
443        assert_eq!(g.edge_count(), 1);
444        assert!(!g.is_empty());
445    }
446
447    #[test]
448    fn test_add_multiple_edges() {
449        let mut g = EntityGraph::new();
450        g.add_edge("user", "posts")
451            .add_edge("user", "profile")
452            .add_edge("user", "comments");
453        assert_eq!(g.edge_count(), 3);
454    }
455
456    #[test]
457    fn test_add_edge_with_sub_graph() {
458        let mut sub = EntityGraph::new();
459        sub.add_edge("comments", "author");
460
461        let mut g = EntityGraph::new();
462        g.add_edge_with_graph("user", "posts", sub);
463
464        assert_eq!(g.edge_count(), 1);
465        assert!(g.edges()[0].sub_graph.is_some());
466        assert_eq!(g.edges()[0].sub_graph.as_ref().unwrap().edge_count(), 1);
467    }
468
469    #[test]
470    fn test_relations_of() {
471        let mut g = EntityGraph::new();
472        g.add_edge("user", "posts")
473            .add_edge("user", "profile")
474            .add_edge("post", "comments");
475
476        let user_relations = g.relations_of("user");
477        assert_eq!(user_relations.len(), 2);
478        assert_eq!(user_relations[0].relation, "posts");
479        assert_eq!(user_relations[1].relation, "profile");
480
481        let post_relations = g.relations_of("post");
482        assert_eq!(post_relations.len(), 1);
483
484        let none = g.relations_of("nonexistent");
485        assert!(none.is_empty());
486    }
487
488    #[test]
489    fn test_all_relations() {
490        let mut g = EntityGraph::new();
491        g.add_edge("user", "posts")
492            .add_edge("user", "profile")
493            .add_edge("post", "comments");
494
495        let rels = g.all_relations();
496        assert_eq!(rels, vec!["comments", "posts", "profile"]);
497    }
498
499    #[test]
500    fn test_all_parent_fields() {
501        let mut g = EntityGraph::new();
502        g.add_edge("user", "posts")
503            .add_edge("user", "profile")
504            .add_edge("post", "comments");
505
506        let fields = g.all_parent_fields();
507        assert_eq!(fields, vec!["post", "user"]);
508    }
509
510    #[test]
511    fn test_all_relations_recursive() {
512        let mut sub = EntityGraph::new();
513        sub.add_edge("comments", "author")
514            .add_edge("comments", "likes");
515
516        let mut g = EntityGraph::new();
517        g.add_edge("user", "posts")
518            .add_edge_with_graph("user", "comments", sub);
519
520        let all = g.all_relations_recursive();
521        assert!(all.contains(&"posts".to_string()));
522        assert!(all.contains(&"comments".to_string()));
523        assert!(all.contains(&"author".to_string()));
524        assert!(all.contains(&"likes".to_string()));
525        assert_eq!(all.len(), 4);
526    }
527
528    #[test]
529    fn test_default_graph_is_empty() {
530        let g = EntityGraph::default();
531        assert!(g.is_empty());
532    }
533
534    // ===== BatchStrategy 测试 =====
535
536    #[test]
537    fn test_strategy_name() {
538        assert_eq!(BatchStrategy::In.name(), "in");
539        assert_eq!(BatchStrategy::Join.name(), "join");
540        assert_eq!(BatchStrategy::Subquery.name(), "subquery");
541    }
542
543    #[test]
544    fn test_strategy_default_is_in() {
545        assert_eq!(BatchStrategy::default(), BatchStrategy::In);
546    }
547
548    #[test]
549    fn test_render_in_clause_empty() {
550        let sql = BatchStrategy::render_in_clause("id", 0);
551        assert_eq!(sql, "id IN ()");
552    }
553
554    #[test]
555    fn test_render_in_clause_single() {
556        let sql = BatchStrategy::render_in_clause("id", 1);
557        assert_eq!(sql, "id IN (?)");
558    }
559
560    #[test]
561    fn test_render_in_clause_multiple() {
562        let sql = BatchStrategy::render_in_clause("user_id", 3);
563        assert_eq!(sql, "user_id IN (?, ?, ?)");
564    }
565
566    // ===== BatchSizeConfig 测试 =====
567
568    #[test]
569    fn test_default_config() {
570        let config = BatchSizeConfig::default();
571        assert_eq!(config.size, 100);
572        assert_eq!(config.strategy, BatchStrategy::In);
573    }
574
575    #[test]
576    fn test_with_size() {
577        let config = BatchSizeConfig::with_size(50);
578        assert_eq!(config.size, 50);
579        assert_eq!(config.strategy, BatchStrategy::In);
580    }
581
582    #[test]
583    fn test_new_with_strategy() {
584        let config = BatchSizeConfig::new(200, BatchStrategy::Join);
585        assert_eq!(config.size, 200);
586        assert_eq!(config.strategy, BatchStrategy::Join);
587    }
588
589    #[test]
590    fn test_batch_count_zero() {
591        let config = BatchSizeConfig::with_size(100);
592        assert_eq!(config.batch_count(0), 0);
593    }
594
595    #[test]
596    fn test_batch_count_exact_multiple() {
597        let config = BatchSizeConfig::with_size(100);
598        assert_eq!(config.batch_count(100), 1);
599        assert_eq!(config.batch_count(200), 2);
600        assert_eq!(config.batch_count(500), 5);
601    }
602
603    #[test]
604    fn test_batch_count_with_remainder() {
605        let config = BatchSizeConfig::with_size(100);
606        assert_eq!(config.batch_count(1), 1);
607        assert_eq!(config.batch_count(99), 1);
608        assert_eq!(config.batch_count(101), 2);
609        assert_eq!(config.batch_count(150), 2);
610        assert_eq!(config.batch_count(201), 3);
611    }
612
613    #[test]
614    fn test_batch_range() {
615        let config = BatchSizeConfig::with_size(100);
616
617        assert_eq!(config.batch_range(0, 250), 0..100);
618        assert_eq!(config.batch_range(1, 250), 100..200);
619        assert_eq!(config.batch_range(2, 250), 200..250);
620    }
621
622    #[test]
623    fn test_batch_range_exact() {
624        let config = BatchSizeConfig::with_size(100);
625
626        assert_eq!(config.batch_range(0, 100), 0..100);
627        assert_eq!(config.batch_range(1, 100), 100..100); // 空范围
628    }
629
630    #[test]
631    fn test_batch_range_small_batch() {
632        let config = BatchSizeConfig::with_size(10);
633
634        assert_eq!(config.batch_range(0, 25), 0..10);
635        assert_eq!(config.batch_range(1, 25), 10..20);
636        assert_eq!(config.batch_range(2, 25), 20..25);
637    }
638
639    // ===== BatchLoader 测试 =====
640
641    fn make_loader() -> BatchLoader<i64, String> {
642        let loader = Box::new(|ids: &[i64]| -> HashMap<i64, String> {
643            ids.iter().map(|id| (*id, format!("user_{}", id))).collect()
644        });
645        BatchLoader::new(2, loader)
646    }
647
648    #[test]
649    fn test_batch_loader_load_many_single_batch() {
650        let loader = make_loader();
651        let result = loader.load_many(&[1, 2]);
652        assert_eq!(result.len(), 2);
653        assert_eq!(result.get(&1), Some(&"user_1".to_string()));
654        assert_eq!(result.get(&2), Some(&"user_2".to_string()));
655    }
656
657    #[test]
658    fn test_batch_loader_load_many_multiple_batches() {
659        let loader = make_loader();
660        // batch_size=2, 5 keys → 3 batches
661        let result = loader.load_many(&[1, 2, 3, 4, 5]);
662        assert_eq!(result.len(), 5);
663        for id in 1..=5 {
664            assert_eq!(
665                result.get(&id),
666                Some(&format!("user_{}", id)),
667                "missing user {}",
668                id
669            );
670        }
671    }
672
673    #[test]
674    fn test_batch_loader_load_one() {
675        let loader = make_loader();
676        let result = loader.load_one(&42);
677        assert_eq!(result, Some("user_42".to_string()));
678    }
679
680    #[test]
681    fn test_batch_loader_load_one_missing() {
682        // loader 返回的 map 没有 key 100
683        let loader: BatchLoader<i64, String> =
684            BatchLoader::new(10, Box::new(|_ids: &[i64]| HashMap::new()));
685        let result = loader.load_one(&100);
686        assert_eq!(result, None);
687    }
688
689    #[test]
690    fn test_batch_loader_caches_results() {
691        let call_count = std::sync::Arc::new(std::sync::Mutex::new(0u32));
692        let call_count_clone = call_count.clone();
693
694        let loader = Box::new(move |ids: &[i64]| -> HashMap<i64, String> {
695            *call_count_clone.lock().unwrap() += 1;
696            ids.iter().map(|id| (*id, format!("user_{}", id))).collect()
697        });
698
699        let batch_loader = BatchLoader::new(100, loader);
700
701        // 第一次加载
702        batch_loader.load_many(&[1, 2, 3]);
703        assert_eq!(*call_count.lock().unwrap(), 1);
704
705        // 第二次加载相同 key,应命中缓存
706        batch_loader.load_many(&[1, 2, 3]);
707        assert_eq!(*call_count.lock().unwrap(), 1); // 未增加
708
709        // 加载新 key,应触发新的 loader 调用
710        batch_loader.load_many(&[4, 5]);
711        assert_eq!(*call_count.lock().unwrap(), 2);
712    }
713
714    #[test]
715    fn test_batch_loader_partial_cache_hit() {
716        let call_count = std::sync::Arc::new(std::sync::Mutex::new(0u32));
717        let call_count_clone = call_count.clone();
718
719        let loader = Box::new(move |ids: &[i64]| -> HashMap<i64, String> {
720            *call_count_clone.lock().unwrap() += 1;
721            ids.iter().map(|id| (*id, format!("user_{}", id))).collect()
722        });
723
724        let batch_loader = BatchLoader::new(100, loader);
725
726        // 加载 1, 2, 3
727        batch_loader.load_many(&[1, 2, 3]);
728        assert_eq!(*call_count.lock().unwrap(), 1);
729
730        // 加载 1, 2, 3, 4, 5(前 3 个命中缓存)
731        let result = batch_loader.load_many(&[1, 2, 3, 4, 5]);
732        assert_eq!(result.len(), 5);
733        assert_eq!(*call_count.lock().unwrap(), 2); // 只为 4, 5 调用一次
734
735        // 缓存大小应为 5
736        assert_eq!(batch_loader.cache_size(), 5);
737    }
738
739    #[test]
740    fn test_batch_loader_clear_cache() {
741        let loader = make_loader();
742        loader.load_many(&[1, 2]);
743        assert_eq!(loader.cache_size(), 2);
744
745        loader.clear_cache();
746        assert_eq!(loader.cache_size(), 0);
747    }
748
749    #[test]
750    fn test_batch_loader_empty_input() {
751        let loader = make_loader();
752        let result = loader.load_many(&[]);
753        assert!(result.is_empty());
754    }
755
756    #[test]
757    fn test_batch_loader_batch_size_attribute() {
758        let loader = make_loader();
759        assert_eq!(loader.batch_size(), 2);
760    }
761
762    #[test]
763    fn test_batch_loader_with_size_1() {
764        let loader = BatchLoader::new(
765            1,
766            Box::new(|ids: &[i64]| ids.iter().map(|id| (*id, *id * 10)).collect()),
767        );
768        let result = loader.load_many(&[1, 2, 3]);
769        assert_eq!(result.len(), 3);
770        assert_eq!(result.get(&1), Some(&10));
771        assert_eq!(result.get(&2), Some(&20));
772        assert_eq!(result.get(&3), Some(&30));
773    }
774
775    // ===== 集成场景测试 =====
776
777    #[test]
778    fn test_workflow_graph_and_batch_loader() {
779        // 模拟 User → Posts → Comments 的批量加载场景
780        let mut graph = EntityGraph::new();
781        graph.add_edge_with_graph("user", "posts", {
782            let mut sub = EntityGraph::new();
783            sub.add_edge("posts", "comments");
784            sub
785        });
786        assert_eq!(graph.all_relations_recursive().len(), 2);
787
788        // 模拟批量加载用户
789        let user_loader = BatchLoader::new(
790            50,
791            Box::new(|ids: &[i64]| ids.iter().map(|id| (*id, format!("User#{}", id))).collect()),
792        );
793
794        // 加载 123 个用户(应分 3 批)
795        let user_ids: Vec<i64> = (1..=123).collect();
796        let users = user_loader.load_many(&user_ids);
797        assert_eq!(users.len(), 123);
798        assert_eq!(user_loader.cache_size(), 123);
799    }
800
801    #[test]
802    fn test_n_plus_1_problem_solved() {
803        // 经典 N+1 问题演示:
804        // - 错误做法:N 个用户各发 1 次查询加载 posts → N+1 次查询
805        // - 正确做法:用 BatchLoader 一次批量加载 → ⌈N/batch⌉+1 次查询
806
807        let query_count = std::sync::Arc::new(std::sync::Mutex::new(0u32));
808        let query_count_clone = query_count.clone();
809
810        let post_loader = BatchLoader::new(
811            100,
812            Box::new(move |user_ids: &[i64]| {
813                *query_count_clone.lock().unwrap() += 1;
814                // 模拟为每个 user_id 返回 posts
815                user_ids
816                    .iter()
817                    .map(|uid| (*uid, format!("posts_for_user_{}", uid)))
818                    .collect()
819            }),
820        );
821
822        // 250 个用户
823        let user_ids: Vec<i64> = (1..=250).collect();
824        let _posts = post_loader.load_many(&user_ids);
825
826        // 应分 3 批(100+100+50),调用 loader 3 次
827        assert_eq!(*query_count.lock().unwrap(), 3);
828    }
829}