Skip to main content

sz_orm_query/
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_query::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_query::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    /// P2-7:检测实体图中的循环引用
172    ///
173    /// 使用 DFS 遍历图(含嵌套子图),检测是否存在循环路径。
174    /// 循环引用会导致递归 eager load 时栈溢出,必须在加载前检测。
175    ///
176    /// # 算法
177    ///
178    /// 1. **展平**:递归收集主图 + 所有子图的边到统一邻接表
179    /// 2. **三色标记 DFS**:
180    ///    - **白色(未访问)**:节点尚未访问
181    ///    - **灰色(访问中)**:节点正在当前 DFS 路径中,若再次遇到则发现回边(循环)
182    ///    - **黑色(已完成)**:节点及其所有子节点已访问完毕
183    ///
184    /// # 返回
185    ///
186    /// - `Ok(())`:无循环引用
187    /// - `Err(cycle_path)`:检测到循环,`cycle_path` 是循环路径上的节点列表
188    ///   (如 `["user", "posts", "user"]` 表示 user → posts → user 的循环)
189    ///
190    /// # 示例
191    ///
192    /// ```
193    /// use sz_orm_query::entity_graph::EntityGraph;
194    ///
195    /// // 无循环:user → posts → comments
196    /// let mut graph = EntityGraph::new();
197    /// graph.add_edge("user", "posts");
198    /// graph.add_edge("posts", "comments");
199    /// assert!(graph.detect_cycles().is_ok());
200    ///
201    /// // 有循环:user → posts → user
202    /// let mut graph = EntityGraph::new();
203    /// graph.add_edge_with_graph("user", "posts", {
204    ///     let mut sub = EntityGraph::new();
205    ///     sub.add_edge("posts", "user");
206    ///     sub
207    /// });
208    /// assert!(graph.detect_cycles().is_err());
209    /// ```
210    pub fn detect_cycles(&self) -> Result<(), Vec<String>> {
211        // 1. 展平:收集所有边(含子图)到邻接表
212        let mut adj: std::collections::HashMap<String, Vec<String>> =
213            std::collections::HashMap::new();
214        self.collect_edges_recursive(&mut adj);
215
216        // 1.1 对每个节点的邻接列表排序,保证 DFS 遍历顺序确定
217        for neighbors in adj.values_mut() {
218            neighbors.sort();
219        }
220
221        // 2. 三色标记 DFS
222        let mut visited = std::collections::HashSet::new();
223        let mut visiting = std::collections::HashSet::new();
224        let mut path = Vec::new();
225
226        // 2.1 按字典序排序节点,保证 DFS 起点确定
227        let mut sorted_nodes: Vec<String> = adj.keys().cloned().collect();
228        sorted_nodes.sort();
229        for node in &sorted_nodes {
230            if !visited.contains(node) {
231                dfs_cycle_detect(node, &adj, &mut visited, &mut visiting, &mut path)?;
232            }
233        }
234        Ok(())
235    }
236
237    /// P2-7:递归收集所有边到邻接表(含子图)
238    ///
239    /// 将主图和所有子图的边统一收集到 `adj` 中。
240    /// 子图的边也会被加入,因为子图定义了从 `edge.relation` 出发的额外边。
241    fn collect_edges_recursive(&self, adj: &mut std::collections::HashMap<String, Vec<String>>) {
242        for edge in &self.edges {
243            adj.entry(edge.parent_field.clone())
244                .or_default()
245                .push(edge.relation.clone());
246            if let Some(sub) = &edge.sub_graph {
247                sub.collect_edges_recursive(adj);
248            }
249        }
250    }
251
252    /// P2-7:检测并拒绝重复边(相同 parent_field + relation)
253    ///
254    /// 重复边不会导致栈溢出,但会产生冗余 SQL 查询,应检测并警告。
255    ///
256    /// 返回 `Ok(())` 表示无重复;返回 `Err(duplicates)` 表示有重复边。
257    pub fn detect_duplicate_edges(&self) -> Result<(), Vec<(String, String)>> {
258        let mut seen = std::collections::HashSet::new();
259        let mut duplicates = Vec::new();
260        for edge in &self.edges {
261            let key = (edge.parent_field.clone(), edge.relation.clone());
262            if !seen.insert(key.clone()) {
263                duplicates.push((edge.parent_field.clone(), edge.relation.clone()));
264            }
265        }
266        if duplicates.is_empty() {
267            Ok(())
268        } else {
269            Err(duplicates)
270        }
271    }
272
273    /// P2-7:综合校验(循环引用 + 重复边)
274    ///
275    /// 在 `load_eager` / `load_join` 前调用,确保图结构安全。
276    pub fn validate(&self) -> Result<(), String> {
277        // 1. 检测循环引用
278        if let Err(cycle) = self.detect_cycles() {
279            return Err(format!(
280                "EntityGraph 循环引用检测失败:{}",
281                cycle.join(" → ")
282            ));
283        }
284        // 2. 检测重复边
285        if let Err(duplicates) = self.detect_duplicate_edges() {
286            let dup_str: Vec<String> = duplicates
287                .iter()
288                .map(|(p, r)| format!("({}->{})", p, r))
289                .collect();
290            return Err(format!(
291                "EntityGraph 重复边检测失败:{}",
292                dup_str.join(", ")
293            ));
294        }
295        Ok(())
296    }
297}
298
299/// P2-7:DFS 循环检测(独立函数,避免 self 借用问题)
300///
301/// `node` 是当前访问节点,`adj` 是邻接表,`visiting` 是灰色标记集合,
302/// `visited` 是黑色标记集合,`path` 是当前路径。
303fn dfs_cycle_detect(
304    node: &str,
305    adj: &std::collections::HashMap<String, Vec<String>>,
306    visited: &mut std::collections::HashSet<String>,
307    visiting: &mut std::collections::HashSet<String>,
308    path: &mut Vec<String>,
309) -> Result<(), Vec<String>> {
310    // 灰色节点再次被访问 → 发现回边(循环)
311    if visiting.contains(node) {
312        let cycle_start = path.iter().position(|n| n == node).unwrap_or(0);
313        let mut cycle = path[cycle_start..].to_vec();
314        cycle.push(node.to_string());
315        return Err(cycle);
316    }
317    // 黑色节点已完全访问,跳过
318    if visited.contains(node) {
319        return Ok(());
320    }
321
322    // 标记为灰色(访问中)
323    visiting.insert(node.to_string());
324    path.push(node.to_string());
325
326    // 遍历所有邻接节点
327    if let Some(neighbors) = adj.get(node) {
328        for neighbor in neighbors {
329            dfs_cycle_detect(neighbor, adj, visited, visiting, path)?;
330        }
331    }
332
333    // 标记为黑色(已完成)
334    visiting.remove(node);
335    visited.insert(node.to_string());
336    path.pop();
337    Ok(())
338}
339
340// ============================================================================
341// BatchStrategy — 批量策略
342// ============================================================================
343
344/// 批量抓取策略
345#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
346pub enum BatchStrategy {
347    /// 使用 `WHERE id IN (?, ?, ...)` 子句批量加载
348    ///
349    /// 适用场景:关联数量较少、目标表无索引时的备选方案
350    #[default]
351    In,
352    /// 使用 `LEFT JOIN` 一次性加载所有关联
353    ///
354    /// 适用场景:关联数量较少、需要原子性读取
355    Join,
356    /// 使用 `WHERE id IN (SELECT ... FROM ...)` 子查询批量加载
357    ///
358    /// 适用场景:子查询可被数据库优化器优化时
359    Subquery,
360}
361
362impl BatchStrategy {
363    /// 策略名称
364    pub fn name(&self) -> &'static str {
365        match self {
366            BatchStrategy::In => "in",
367            BatchStrategy::Join => "join",
368            BatchStrategy::Subquery => "subquery",
369        }
370    }
371
372    /// 生成 IN 子句的 SQL 片段
373    ///
374    /// 返回形如 `"id IN (?, ?, ?)"` 的字符串(占位符数量与 values 一致)。
375    pub fn render_in_clause(column: &str, placeholders: usize) -> String {
376        if placeholders == 0 {
377            return format!("{} IN ()", column);
378        }
379        let marks: Vec<&str> = vec!["?"; placeholders];
380        format!("{} IN ({})", column, marks.join(", "))
381    }
382}
383
384// ============================================================================
385// BatchSizeConfig — 批量大小配置
386// ============================================================================
387
388/// 批量大小配置
389///
390/// 对应 Hibernate `@BatchSize(size = 100)` 注解。
391#[derive(Debug, Clone, Copy)]
392pub struct BatchSizeConfig {
393    /// 每批数量
394    pub size: usize,
395    /// 抓取策略
396    pub strategy: BatchStrategy,
397}
398
399impl Default for BatchSizeConfig {
400    fn default() -> Self {
401        Self {
402            size: 100,
403            strategy: BatchStrategy::In,
404        }
405    }
406}
407
408impl BatchSizeConfig {
409    /// 创建配置
410    pub fn new(size: usize, strategy: BatchStrategy) -> Self {
411        Self { size, strategy }
412    }
413
414    /// 创建默认策略的配置(IN)
415    pub fn with_size(size: usize) -> Self {
416        Self {
417            size,
418            strategy: BatchStrategy::In,
419        }
420    }
421
422    /// 计算给定总数需要分多少批
423    ///
424    /// # 示例
425    ///
426    /// ```
427    /// use sz_orm_query::entity_graph::BatchSizeConfig;
428    ///
429    /// let config = BatchSizeConfig::with_size(100);
430    /// assert_eq!(config.batch_count(0), 0);
431    /// assert_eq!(config.batch_count(1), 1);
432    /// assert_eq!(config.batch_count(100), 1);
433    /// assert_eq!(config.batch_count(101), 2);
434    /// assert_eq!(config.batch_count(250), 3);
435    /// ```
436    pub fn batch_count(&self, total: usize) -> usize {
437        if total == 0 {
438            0
439        } else {
440            total.div_ceil(self.size)
441        }
442    }
443
444    /// 返回第 `batch_index` 批的范围(start..end,end 不超过 total)
445    ///
446    /// # 示例
447    ///
448    /// ```
449    /// use sz_orm_query::entity_graph::BatchSizeConfig;
450    ///
451    /// let config = BatchSizeConfig::with_size(100);
452    /// assert_eq!(config.batch_range(0, 250), 0..100);
453    /// assert_eq!(config.batch_range(1, 250), 100..200);
454    /// assert_eq!(config.batch_range(2, 250), 200..250);
455    /// ```
456    pub fn batch_range(&self, batch_index: usize, total: usize) -> std::ops::Range<usize> {
457        let start = batch_index * self.size;
458        let end = (start + self.size).min(total);
459        start..end
460    }
461}
462
463// ============================================================================
464// BatchLoader — 通用批量加载器
465// ============================================================================
466
467/// 批量加载函数类型
468pub type BatchLoaderFn<K, V> = Box<dyn Fn(&[K]) -> HashMap<K, V> + Send + Sync>;
469
470/// 批量加载器
471///
472/// 将 N 个单条加载请求合并为 ⌈N/batch_size⌉ 次批量加载,避免 N+1 查询问题。
473///
474/// # 泛型参数
475///
476/// - `K`:主键类型(必须实现 `Hash + Eq + Clone`)
477/// - `V`:值类型
478///
479/// # 示例
480///
481/// ```
482/// use sz_orm_query::entity_graph::BatchLoader;
483/// use std::collections::HashMap;
484///
485/// fn load_users(ids: &[i64]) -> HashMap<i64, String> {
486///     ids.iter().map(|id| (*id, format!("user_{}", id))).collect()
487/// }
488///
489/// let loader = BatchLoader::new(100, Box::new(load_users));
490/// let users = loader.load_many(&[1, 2, 3]);
491/// assert_eq!(users.len(), 3);
492/// ```
493pub struct BatchLoader<K, V>
494where
495    K: Hash + Eq + Clone + Send + Sync,
496    V: Clone + Send + Sync,
497{
498    /// 每批数量
499    batch_size: usize,
500    /// 实际加载函数
501    loader: BatchLoaderFn<K, V>,
502    /// 缓存(避免重复加载相同的 key)
503    cache: RwLock<HashMap<K, V>>,
504}
505
506impl<K, V> BatchLoader<K, V>
507where
508    K: Hash + Eq + Clone + Send + Sync,
509    V: Clone + Send + Sync,
510{
511    /// 创建批量加载器
512    ///
513    /// # 参数
514    /// - `batch_size`:每批数量
515    /// - `loader`:实际加载函数,接收一批 key,返回 key→value 的 HashMap
516    pub fn new(batch_size: usize, loader: BatchLoaderFn<K, V>) -> Self {
517        Self {
518            batch_size,
519            loader,
520            cache: RwLock::new(HashMap::new()),
521        }
522    }
523
524    /// 批量加载多个 key
525    ///
526    /// - 自动跳过缓存中已有的 key
527    /// - 按 batch_size 分批调用 loader
528    /// - 返回所有 key 对应的 value(包含缓存与新加载的)
529    pub fn load_many(&self, keys: &[K]) -> HashMap<K, V> {
530        let mut result: HashMap<K, V> = HashMap::new();
531
532        // 1. 从缓存读取(锁毒化时视缓存为空,全部 key 重新加载)
533        let mut to_load: Vec<K> = Vec::new();
534        if let Ok(cached) = self.cache.read() {
535            for k in keys {
536                if let Some(v) = cached.get(k) {
537                    result.insert(k.clone(), v.clone());
538                } else {
539                    to_load.push(k.clone());
540                }
541            }
542        } else {
543            to_load.extend(keys.iter().cloned());
544        }
545
546        if to_load.is_empty() {
547            return result;
548        }
549
550        // 2. 分批加载
551        let batch_size = self.batch_size.max(1);
552        let mut all_loaded: HashMap<K, V> = HashMap::new();
553        for chunk in to_load.chunks(batch_size) {
554            let loaded = (self.loader)(chunk);
555            all_loaded.extend(loaded);
556        }
557
558        // 3. 写入缓存(锁毒化时跳过写入,不影响本次返回结果)
559        if let Ok(mut cache) = self.cache.write() {
560            for (k, v) in &all_loaded {
561                cache.insert(k.clone(), v.clone());
562            }
563        }
564
565        // 4. 合并结果
566        result.extend(all_loaded);
567        result
568    }
569
570    /// 加载单个 key(便捷方法)
571    pub fn load_one(&self, key: &K) -> Option<V> {
572        let result = self.load_many(std::slice::from_ref(key));
573        result.get(key).cloned()
574    }
575
576    /// 清空缓存
577    pub fn clear_cache(&self) {
578        if let Ok(mut cache) = self.cache.write() {
579            cache.clear();
580        }
581    }
582
583    /// 返回当前缓存大小
584    pub fn cache_size(&self) -> usize {
585        match self.cache.read() {
586            Ok(g) => g.len(),
587            Err(_) => 0,
588        }
589    }
590
591    /// 返回 batch_size
592    pub fn batch_size(&self) -> usize {
593        self.batch_size
594    }
595}
596
597// ============================================================================
598// N1QueryDetector — N+1 查询检测器(S-2:SeaORM 对标短板补全)
599// ============================================================================
600
601/// N+1 查询检测器
602///
603/// 通过统计相同表/关系在循环加载场景下的查询次数,识别潜在的 N+1 查询问题。
604///
605/// # 检测策略
606///
607/// 1. **次数阈值**:同一 `relation` 在一次"检测窗口"内被查询次数超过
608///    `threshold`(默认 5),即判定为 N+1 嫌疑;
609/// 2. **检测窗口**:以 `start_window()` / `end_window()` 显式划定窗口,
610///    便于在循环外包裹;
611/// 3. **批量命中识别**:如果一次 `record_batch_load` 调用就加载了多个 key,
612///    视为已通过批量加载规避 N+1,仅记 1 次批量查询;
613/// 4. **回调告警**:触发 N+1 时调用可选的 `on_alert` 回调(用于日志/指标上报)。
614///
615/// # 设计动机
616///
617/// SeaORM 的 `find_with_related` 仅在显式调用时才会批量加载,缺乏运行时
618/// 检测机制。本检测器提供运行时 introspection,可在开发/测试环境启用,
619/// 在生产环境关闭(zero-cost 抽象)。
620///
621/// # 线程安全
622///
623/// 内部使用 `RwLock<HashMap>` 维护计数,可在线程间共享(`Send + Sync`)。
624///
625/// # 示例
626///
627/// ```
628/// use sz_orm_query::entity_graph::{N1QueryDetector, N1DetectionConfig};
629///
630/// let detector = N1QueryDetector::new(N1DetectionConfig::default());
631/// detector.start_window();
632/// for _ in 0..10 {
633///     detector.record_single_load("posts");
634/// }
635/// detector.end_window();
636/// let alerts = detector.alerts();
637/// assert_eq!(alerts.len(), 1);
638/// assert_eq!(alerts[0].relation, "posts");
639/// assert_eq!(alerts[0].query_count, 10);
640/// ```
641pub struct N1QueryDetector {
642    /// 检测配置
643    config: N1DetectionConfig,
644    /// 当前窗口内各 relation 的单条查询计数
645    counts: RwLock<HashMap<String, u64>>,
646    /// 当前窗口内各 relation 的批量查询计数(每个 batch 记 1 次)
647    batch_counts: RwLock<HashMap<String, u64>>,
648    /// 当前窗口是否开启
649    window_active: RwLock<bool>,
650    /// 历史告警列表(最近一次窗口的结果)
651    alerts: RwLock<Vec<N1Alert>>,
652}
653
654/// N+1 检测配置
655#[derive(Debug, Clone)]
656pub struct N1DetectionConfig {
657    /// 触发告警的查询次数阈值(同一 relation 在一个窗口内的单条查询次数 ≥ threshold)
658    pub threshold: u64,
659    /// 是否启用检测(false 时所有 record_* 调用均为 no-op)
660    pub enabled: bool,
661}
662
663impl Default for N1DetectionConfig {
664    fn default() -> Self {
665        Self {
666            threshold: 5,
667            enabled: true,
668        }
669    }
670}
671
672impl N1DetectionConfig {
673    /// 创建默认配置(threshold=5, enabled=true)
674    pub fn new() -> Self {
675        Self::default()
676    }
677
678    /// 自定义阈值
679    pub fn with_threshold(mut self, threshold: u64) -> Self {
680        self.threshold = threshold.max(1);
681        self
682    }
683
684    /// 启用/禁用
685    pub fn with_enabled(mut self, enabled: bool) -> Self {
686        self.enabled = enabled;
687        self
688    }
689}
690
691/// N+1 查询告警
692#[derive(Debug, Clone, PartialEq, Eq)]
693pub struct N1Alert {
694    /// 触发告警的 relation 名称
695    pub relation: String,
696    /// 单条查询次数
697    pub query_count: u64,
698    /// 批量查询次数(0 表示完全没有使用批量加载)
699    pub batch_count: u64,
700    /// 配置的阈值
701    pub threshold: u64,
702}
703
704impl N1Alert {
705    /// 是否完全未使用批量加载
706    pub fn no_batch_used(&self) -> bool {
707        self.batch_count == 0
708    }
709
710    /// 建议的批量大小(query_count 向上取整到 10 的幂级,至少 50)
711    pub fn suggested_batch_size(&self) -> usize {
712        let n = self.query_count as usize;
713        if n <= 50 {
714            50
715        } else if n <= 100 {
716            100
717        } else if n <= 500 {
718            500
719        } else {
720            1000
721        }
722    }
723}
724
725impl N1QueryDetector {
726    /// 创建检测器
727    pub fn new(config: N1DetectionConfig) -> Self {
728        Self {
729            config,
730            counts: RwLock::new(HashMap::new()),
731            batch_counts: RwLock::new(HashMap::new()),
732            window_active: RwLock::new(false),
733            alerts: RwLock::new(Vec::new()),
734        }
735    }
736
737    /// 创建默认配置的检测器
738    pub fn with_defaults() -> Self {
739        Self::new(N1DetectionConfig::default())
740    }
741
742    /// 是否启用检测
743    pub fn is_enabled(&self) -> bool {
744        self.config.enabled
745    }
746
747    /// 当前阈值
748    pub fn threshold(&self) -> u64 {
749        self.config.threshold
750    }
751
752    /// 开启检测窗口(清空旧计数与旧告警)
753    ///
754    /// 重复调用 `start_window` 会重置窗口。
755    pub fn start_window(&self) {
756        if !self.config.enabled {
757            return;
758        }
759        if let Ok(mut counts) = self.counts.write() {
760            *counts = HashMap::new();
761        }
762        if let Ok(mut batch_counts) = self.batch_counts.write() {
763            *batch_counts = HashMap::new();
764        }
765        if let Ok(mut alerts) = self.alerts.write() {
766            *alerts = Vec::new();
767        }
768        if let Ok(mut window_active) = self.window_active.write() {
769            *window_active = true;
770        }
771    }
772
773    /// 结束检测窗口,分析并生成告警
774    ///
775    /// 结束后 `record_*` 调用会被忽略,直到下一次 `start_window`。
776    /// 返回本次窗口产生的告警列表。
777    pub fn end_window(&self) -> Vec<N1Alert> {
778        if !self.config.enabled {
779            return Vec::new();
780        }
781        if let Ok(mut window_active) = self.window_active.write() {
782            *window_active = false;
783        }
784
785        // 读锁毒化时返回空告警列表(graceful 降级)
786        let new_alerts: Vec<N1Alert> = match (self.counts.read(), self.batch_counts.read()) {
787            (Ok(counts), Ok(batch_counts)) => {
788                let mut alerts: Vec<N1Alert> = counts
789                    .iter()
790                    .filter_map(|(rel, &cnt)| {
791                        if cnt >= self.config.threshold {
792                            Some(N1Alert {
793                                relation: rel.clone(),
794                                query_count: cnt,
795                                batch_count: *batch_counts.get(rel).unwrap_or(&0),
796                                threshold: self.config.threshold,
797                            })
798                        } else {
799                            None
800                        }
801                    })
802                    .collect();
803                // 稳定排序便于断言
804                alerts.sort_by(|a, b| a.relation.cmp(&b.relation));
805                alerts
806            }
807            _ => Vec::new(),
808        };
809
810        if let Ok(mut alerts) = self.alerts.write() {
811            *alerts = new_alerts.clone();
812        }
813        new_alerts
814    }
815
816    /// 记录一次单条加载(典型的 N+1 来源:循环内 `find_by_id`)
817    pub fn record_single_load(&self, relation: &str) {
818        if !self.config.enabled {
819            return;
820        }
821        {
822            let active = self.window_active.read().map(|g| *g).unwrap_or(false);
823            if !active {
824                return;
825            }
826        }
827        if let Ok(mut counts) = self.counts.write() {
828            *counts.entry(relation.to_string()).or_insert(0) += 1;
829        }
830    }
831
832    /// 记录一次批量加载(已规避 N+1 的良好实践)
833    ///
834    /// - `keys_count`:本次批量加载的 key 数量
835    /// - 一个 batch 仅记 1 次批量查询,不论 keys_count 多少
836    pub fn record_batch_load(&self, relation: &str, _keys_count: usize) {
837        if !self.config.enabled {
838            return;
839        }
840        {
841            let active = self.window_active.read().map(|g| *g).unwrap_or(false);
842            if !active {
843                return;
844            }
845        }
846        if let Ok(mut batch_counts) = self.batch_counts.write() {
847            *batch_counts.entry(relation.to_string()).or_insert(0) += 1;
848        }
849    }
850
851    /// 获取最近一次 `end_window` 产生的告警(只读副本)
852    pub fn alerts(&self) -> Vec<N1Alert> {
853        self.alerts.read().map(|g| g.clone()).unwrap_or_default()
854    }
855
856    /// 当前窗口内某 relation 的单条查询次数
857    pub fn current_count(&self, relation: &str) -> u64 {
858        self.counts
859            .read()
860            .map(|g| g.get(relation).copied().unwrap_or(0))
861            .unwrap_or(0)
862    }
863
864    /// 当前窗口内某 relation 的批量查询次数
865    pub fn current_batch_count(&self, relation: &str) -> u64 {
866        self.batch_counts
867            .read()
868            .map(|g| g.get(relation).copied().unwrap_or(0))
869            .unwrap_or(0)
870    }
871
872    /// 窗口是否处于开启状态
873    pub fn is_window_active(&self) -> bool {
874        self.window_active.read().map(|g| *g).unwrap_or(false)
875    }
876
877    /// 是否已检测到 N+1(基于最近一次窗口的告警)
878    pub fn has_n_plus_one(&self) -> bool {
879        !self.alerts().is_empty()
880    }
881}
882
883impl Default for N1QueryDetector {
884    fn default() -> Self {
885        Self::with_defaults()
886    }
887}
888
889// ============================================================================
890// 单元测试
891// ============================================================================
892
893#[cfg(test)]
894mod tests {
895    use super::*;
896
897    // ===== EntityGraph 测试 =====
898
899    #[test]
900    fn test_new_graph_is_empty() {
901        let g = EntityGraph::new();
902        assert!(g.is_empty());
903        assert_eq!(g.edge_count(), 0);
904    }
905
906    #[test]
907    fn test_add_edge() {
908        let mut g = EntityGraph::new();
909        g.add_edge("user", "posts");
910        assert_eq!(g.edge_count(), 1);
911        assert!(!g.is_empty());
912    }
913
914    #[test]
915    fn test_add_multiple_edges() {
916        let mut g = EntityGraph::new();
917        g.add_edge("user", "posts")
918            .add_edge("user", "profile")
919            .add_edge("user", "comments");
920        assert_eq!(g.edge_count(), 3);
921    }
922
923    #[test]
924    fn test_add_edge_with_sub_graph() {
925        let mut sub = EntityGraph::new();
926        sub.add_edge("comments", "author");
927
928        let mut g = EntityGraph::new();
929        g.add_edge_with_graph("user", "posts", sub);
930
931        assert_eq!(g.edge_count(), 1);
932        assert!(g.edges()[0].sub_graph.is_some());
933        assert_eq!(g.edges()[0].sub_graph.as_ref().unwrap().edge_count(), 1);
934    }
935
936    #[test]
937    fn test_relations_of() {
938        let mut g = EntityGraph::new();
939        g.add_edge("user", "posts")
940            .add_edge("user", "profile")
941            .add_edge("post", "comments");
942
943        let user_relations = g.relations_of("user");
944        assert_eq!(user_relations.len(), 2);
945        assert_eq!(user_relations[0].relation, "posts");
946        assert_eq!(user_relations[1].relation, "profile");
947
948        let post_relations = g.relations_of("post");
949        assert_eq!(post_relations.len(), 1);
950
951        let none = g.relations_of("nonexistent");
952        assert!(none.is_empty());
953    }
954
955    #[test]
956    fn test_all_relations() {
957        let mut g = EntityGraph::new();
958        g.add_edge("user", "posts")
959            .add_edge("user", "profile")
960            .add_edge("post", "comments");
961
962        let rels = g.all_relations();
963        assert_eq!(rels, vec!["comments", "posts", "profile"]);
964    }
965
966    #[test]
967    fn test_all_parent_fields() {
968        let mut g = EntityGraph::new();
969        g.add_edge("user", "posts")
970            .add_edge("user", "profile")
971            .add_edge("post", "comments");
972
973        let fields = g.all_parent_fields();
974        assert_eq!(fields, vec!["post", "user"]);
975    }
976
977    #[test]
978    fn test_all_relations_recursive() {
979        let mut sub = EntityGraph::new();
980        sub.add_edge("comments", "author")
981            .add_edge("comments", "likes");
982
983        let mut g = EntityGraph::new();
984        g.add_edge("user", "posts")
985            .add_edge_with_graph("user", "comments", sub);
986
987        let all = g.all_relations_recursive();
988        assert!(all.contains(&"posts".to_string()));
989        assert!(all.contains(&"comments".to_string()));
990        assert!(all.contains(&"author".to_string()));
991        assert!(all.contains(&"likes".to_string()));
992        assert_eq!(all.len(), 4);
993    }
994
995    #[test]
996    fn test_default_graph_is_empty() {
997        let g = EntityGraph::default();
998        assert!(g.is_empty());
999    }
1000
1001    // ===== BatchStrategy 测试 =====
1002
1003    #[test]
1004    fn test_strategy_name() {
1005        assert_eq!(BatchStrategy::In.name(), "in");
1006        assert_eq!(BatchStrategy::Join.name(), "join");
1007        assert_eq!(BatchStrategy::Subquery.name(), "subquery");
1008    }
1009
1010    #[test]
1011    fn test_strategy_default_is_in() {
1012        assert_eq!(BatchStrategy::default(), BatchStrategy::In);
1013    }
1014
1015    #[test]
1016    fn test_render_in_clause_empty() {
1017        let sql = BatchStrategy::render_in_clause("id", 0);
1018        assert_eq!(sql, "id IN ()");
1019    }
1020
1021    #[test]
1022    fn test_render_in_clause_single() {
1023        let sql = BatchStrategy::render_in_clause("id", 1);
1024        assert_eq!(sql, "id IN (?)");
1025    }
1026
1027    #[test]
1028    fn test_render_in_clause_multiple() {
1029        let sql = BatchStrategy::render_in_clause("user_id", 3);
1030        assert_eq!(sql, "user_id IN (?, ?, ?)");
1031    }
1032
1033    // ===== BatchSizeConfig 测试 =====
1034
1035    #[test]
1036    fn test_default_config() {
1037        let config = BatchSizeConfig::default();
1038        assert_eq!(config.size, 100);
1039        assert_eq!(config.strategy, BatchStrategy::In);
1040    }
1041
1042    #[test]
1043    fn test_with_size() {
1044        let config = BatchSizeConfig::with_size(50);
1045        assert_eq!(config.size, 50);
1046        assert_eq!(config.strategy, BatchStrategy::In);
1047    }
1048
1049    #[test]
1050    fn test_new_with_strategy() {
1051        let config = BatchSizeConfig::new(200, BatchStrategy::Join);
1052        assert_eq!(config.size, 200);
1053        assert_eq!(config.strategy, BatchStrategy::Join);
1054    }
1055
1056    #[test]
1057    fn test_batch_count_zero() {
1058        let config = BatchSizeConfig::with_size(100);
1059        assert_eq!(config.batch_count(0), 0);
1060    }
1061
1062    #[test]
1063    fn test_batch_count_exact_multiple() {
1064        let config = BatchSizeConfig::with_size(100);
1065        assert_eq!(config.batch_count(100), 1);
1066        assert_eq!(config.batch_count(200), 2);
1067        assert_eq!(config.batch_count(500), 5);
1068    }
1069
1070    #[test]
1071    fn test_batch_count_with_remainder() {
1072        let config = BatchSizeConfig::with_size(100);
1073        assert_eq!(config.batch_count(1), 1);
1074        assert_eq!(config.batch_count(99), 1);
1075        assert_eq!(config.batch_count(101), 2);
1076        assert_eq!(config.batch_count(150), 2);
1077        assert_eq!(config.batch_count(201), 3);
1078    }
1079
1080    #[test]
1081    fn test_batch_range() {
1082        let config = BatchSizeConfig::with_size(100);
1083
1084        assert_eq!(config.batch_range(0, 250), 0..100);
1085        assert_eq!(config.batch_range(1, 250), 100..200);
1086        assert_eq!(config.batch_range(2, 250), 200..250);
1087    }
1088
1089    #[test]
1090    fn test_batch_range_exact() {
1091        let config = BatchSizeConfig::with_size(100);
1092
1093        assert_eq!(config.batch_range(0, 100), 0..100);
1094        assert_eq!(config.batch_range(1, 100), 100..100); // 空范围
1095    }
1096
1097    #[test]
1098    fn test_batch_range_small_batch() {
1099        let config = BatchSizeConfig::with_size(10);
1100
1101        assert_eq!(config.batch_range(0, 25), 0..10);
1102        assert_eq!(config.batch_range(1, 25), 10..20);
1103        assert_eq!(config.batch_range(2, 25), 20..25);
1104    }
1105
1106    // ===== BatchLoader 测试 =====
1107
1108    fn make_loader() -> BatchLoader<i64, String> {
1109        let loader = Box::new(|ids: &[i64]| -> HashMap<i64, String> {
1110            ids.iter().map(|id| (*id, format!("user_{}", id))).collect()
1111        });
1112        BatchLoader::new(2, loader)
1113    }
1114
1115    #[test]
1116    fn test_batch_loader_load_many_single_batch() {
1117        let loader = make_loader();
1118        let result = loader.load_many(&[1, 2]);
1119        assert_eq!(result.len(), 2);
1120        assert_eq!(result.get(&1), Some(&"user_1".to_string()));
1121        assert_eq!(result.get(&2), Some(&"user_2".to_string()));
1122    }
1123
1124    #[test]
1125    fn test_batch_loader_load_many_multiple_batches() {
1126        let loader = make_loader();
1127        // batch_size=2, 5 keys → 3 batches
1128        let result = loader.load_many(&[1, 2, 3, 4, 5]);
1129        assert_eq!(result.len(), 5);
1130        for id in 1..=5 {
1131            assert_eq!(
1132                result.get(&id),
1133                Some(&format!("user_{}", id)),
1134                "missing user {}",
1135                id
1136            );
1137        }
1138    }
1139
1140    #[test]
1141    fn test_batch_loader_load_one() {
1142        let loader = make_loader();
1143        let result = loader.load_one(&42);
1144        assert_eq!(result, Some("user_42".to_string()));
1145    }
1146
1147    #[test]
1148    fn test_batch_loader_load_one_missing() {
1149        // loader 返回的 map 没有 key 100
1150        let loader: BatchLoader<i64, String> =
1151            BatchLoader::new(10, Box::new(|_ids: &[i64]| HashMap::new()));
1152        let result = loader.load_one(&100);
1153        assert_eq!(result, None);
1154    }
1155
1156    #[test]
1157    fn test_batch_loader_caches_results() {
1158        let call_count = std::sync::Arc::new(std::sync::Mutex::new(0u32));
1159        let call_count_clone = call_count.clone();
1160
1161        let loader = Box::new(move |ids: &[i64]| -> HashMap<i64, String> {
1162            *call_count_clone.lock().unwrap() += 1;
1163            ids.iter().map(|id| (*id, format!("user_{}", id))).collect()
1164        });
1165
1166        let batch_loader = BatchLoader::new(100, loader);
1167
1168        // 第一次加载
1169        batch_loader.load_many(&[1, 2, 3]);
1170        assert_eq!(*call_count.lock().unwrap(), 1);
1171
1172        // 第二次加载相同 key,应命中缓存
1173        batch_loader.load_many(&[1, 2, 3]);
1174        assert_eq!(*call_count.lock().unwrap(), 1); // 未增加
1175
1176        // 加载新 key,应触发新的 loader 调用
1177        batch_loader.load_many(&[4, 5]);
1178        assert_eq!(*call_count.lock().unwrap(), 2);
1179    }
1180
1181    #[test]
1182    fn test_batch_loader_partial_cache_hit() {
1183        let call_count = std::sync::Arc::new(std::sync::Mutex::new(0u32));
1184        let call_count_clone = call_count.clone();
1185
1186        let loader = Box::new(move |ids: &[i64]| -> HashMap<i64, String> {
1187            *call_count_clone.lock().unwrap() += 1;
1188            ids.iter().map(|id| (*id, format!("user_{}", id))).collect()
1189        });
1190
1191        let batch_loader = BatchLoader::new(100, loader);
1192
1193        // 加载 1, 2, 3
1194        batch_loader.load_many(&[1, 2, 3]);
1195        assert_eq!(*call_count.lock().unwrap(), 1);
1196
1197        // 加载 1, 2, 3, 4, 5(前 3 个命中缓存)
1198        let result = batch_loader.load_many(&[1, 2, 3, 4, 5]);
1199        assert_eq!(result.len(), 5);
1200        assert_eq!(*call_count.lock().unwrap(), 2); // 只为 4, 5 调用一次
1201
1202        // 缓存大小应为 5
1203        assert_eq!(batch_loader.cache_size(), 5);
1204    }
1205
1206    #[test]
1207    fn test_batch_loader_clear_cache() {
1208        let loader = make_loader();
1209        loader.load_many(&[1, 2]);
1210        assert_eq!(loader.cache_size(), 2);
1211
1212        loader.clear_cache();
1213        assert_eq!(loader.cache_size(), 0);
1214    }
1215
1216    #[test]
1217    fn test_batch_loader_empty_input() {
1218        let loader = make_loader();
1219        let result = loader.load_many(&[]);
1220        assert!(result.is_empty());
1221    }
1222
1223    #[test]
1224    fn test_batch_loader_batch_size_attribute() {
1225        let loader = make_loader();
1226        assert_eq!(loader.batch_size(), 2);
1227    }
1228
1229    #[test]
1230    fn test_batch_loader_with_size_1() {
1231        let loader = BatchLoader::new(
1232            1,
1233            Box::new(|ids: &[i64]| ids.iter().map(|id| (*id, *id * 10)).collect()),
1234        );
1235        let result = loader.load_many(&[1, 2, 3]);
1236        assert_eq!(result.len(), 3);
1237        assert_eq!(result.get(&1), Some(&10));
1238        assert_eq!(result.get(&2), Some(&20));
1239        assert_eq!(result.get(&3), Some(&30));
1240    }
1241
1242    // ===== N1QueryDetector 测试(S-2)=====
1243
1244    #[test]
1245    fn test_n1_config_default() {
1246        let cfg = N1DetectionConfig::default();
1247        assert_eq!(cfg.threshold, 5);
1248        assert!(cfg.enabled);
1249    }
1250
1251    #[test]
1252    fn test_n1_config_builder() {
1253        let cfg = N1DetectionConfig::new()
1254            .with_threshold(10)
1255            .with_enabled(false);
1256        assert_eq!(cfg.threshold, 10);
1257        assert!(!cfg.enabled);
1258
1259        // threshold < 1 应被钳制为 1
1260        let cfg2 = N1DetectionConfig::new().with_threshold(0);
1261        assert_eq!(cfg2.threshold, 1);
1262    }
1263
1264    #[test]
1265    fn test_n1_detector_default() {
1266        let det = N1QueryDetector::default();
1267        assert!(det.is_enabled());
1268        assert_eq!(det.threshold(), 5);
1269        assert!(!det.is_window_active());
1270        assert!(!det.has_n_plus_one());
1271    }
1272
1273    #[test]
1274    fn test_n1_detector_disabled_is_noop() {
1275        let det = N1QueryDetector::new(N1DetectionConfig::new().with_enabled(false));
1276        det.start_window();
1277        for _ in 0..100 {
1278            det.record_single_load("posts");
1279        }
1280        // 禁用时计数不应增加
1281        assert_eq!(det.current_count("posts"), 0);
1282        let alerts = det.end_window();
1283        assert!(alerts.is_empty());
1284    }
1285
1286    #[test]
1287    fn test_n1_detector_records_outside_window_ignored() {
1288        let det = N1QueryDetector::with_defaults();
1289        // 未开启窗口时记录应被忽略
1290        det.record_single_load("posts");
1291        assert_eq!(det.current_count("posts"), 0);
1292    }
1293
1294    #[test]
1295    fn test_n1_detector_below_threshold_no_alert() {
1296        let det = N1QueryDetector::with_defaults(); // threshold=5
1297        det.start_window();
1298        for _ in 0..4 {
1299            det.record_single_load("posts");
1300        }
1301        assert_eq!(det.current_count("posts"), 4);
1302        let alerts = det.end_window();
1303        assert!(alerts.is_empty(), "below threshold should not alert");
1304        assert!(!det.has_n_plus_one());
1305    }
1306
1307    #[test]
1308    fn test_n1_detector_at_threshold_triggers_alert() {
1309        let det = N1QueryDetector::with_defaults(); // threshold=5
1310        det.start_window();
1311        for _ in 0..5 {
1312            det.record_single_load("posts");
1313        }
1314        let alerts = det.end_window();
1315        assert_eq!(alerts.len(), 1);
1316        assert_eq!(alerts[0].relation, "posts");
1317        assert_eq!(alerts[0].query_count, 5);
1318        assert_eq!(alerts[0].threshold, 5);
1319        assert_eq!(alerts[0].batch_count, 0);
1320        assert!(alerts[0].no_batch_used());
1321        assert!(det.has_n_plus_one());
1322    }
1323
1324    #[test]
1325    fn test_n1_detector_above_threshold_triggers_alert() {
1326        let det = N1QueryDetector::with_defaults();
1327        det.start_window();
1328        for _ in 0..10 {
1329            det.record_single_load("posts");
1330        }
1331        let alerts = det.end_window();
1332        assert_eq!(alerts.len(), 1);
1333        assert_eq!(alerts[0].query_count, 10);
1334        // 默认阈值为 5,10 次查询应建议 batch_size >= 50
1335        assert!(alerts[0].suggested_batch_size() >= 50);
1336    }
1337
1338    #[test]
1339    fn test_n1_detector_multiple_relations() {
1340        let det = N1QueryDetector::with_defaults();
1341        det.start_window();
1342        for _ in 0..6 {
1343            det.record_single_load("posts");
1344        }
1345        for _ in 0..3 {
1346            det.record_single_load("comments"); // 低于阈值
1347        }
1348        for _ in 0..8 {
1349            det.record_single_load("tags");
1350        }
1351        let alerts = det.end_window();
1352        // 仅 posts 与 tags 应触发告警(comments 低于阈值)
1353        assert_eq!(alerts.len(), 2);
1354        // 排序后应为 posts, tags
1355        assert_eq!(alerts[0].relation, "posts");
1356        assert_eq!(alerts[0].query_count, 6);
1357        assert_eq!(alerts[1].relation, "tags");
1358        assert_eq!(alerts[1].query_count, 8);
1359    }
1360
1361    #[test]
1362    fn test_n1_detector_batch_load_recorded_separately() {
1363        let det = N1QueryDetector::with_defaults();
1364        det.start_window();
1365        // 单条查询 6 次(触发 N+1)
1366        for _ in 0..6 {
1367            det.record_single_load("posts");
1368        }
1369        // 同时有 2 次批量加载(良好实践)
1370        det.record_batch_load("posts", 100);
1371        det.record_batch_load("posts", 50);
1372        let alerts = det.end_window();
1373        assert_eq!(alerts.len(), 1);
1374        assert_eq!(alerts[0].query_count, 6);
1375        assert_eq!(alerts[0].batch_count, 2);
1376        // batch_count != 0 表示已部分使用批量加载
1377        assert!(!alerts[0].no_batch_used());
1378    }
1379
1380    #[test]
1381    fn test_n1_detector_batch_only_does_not_trigger() {
1382        // 仅使用批量加载(无单条查询)不应触发告警
1383        let det = N1QueryDetector::with_defaults();
1384        det.start_window();
1385        for _ in 0..100 {
1386            det.record_batch_load("posts", 50);
1387        }
1388        assert_eq!(det.current_batch_count("posts"), 100);
1389        assert_eq!(det.current_count("posts"), 0);
1390        let alerts = det.end_window();
1391        assert!(alerts.is_empty());
1392    }
1393
1394    #[test]
1395    fn test_n1_detector_start_window_resets() {
1396        let det = N1QueryDetector::with_defaults();
1397        det.start_window();
1398        for _ in 0..10 {
1399            det.record_single_load("posts");
1400        }
1401        let _ = det.end_window();
1402        assert_eq!(det.alerts().len(), 1);
1403
1404        // 再次开启窗口应清空旧告警与计数
1405        det.start_window();
1406        assert_eq!(det.alerts().len(), 0);
1407        assert_eq!(det.current_count("posts"), 0);
1408        assert!(det.is_window_active());
1409    }
1410
1411    #[test]
1412    fn test_n1_detector_end_window_deactivates() {
1413        let det = N1QueryDetector::with_defaults();
1414        det.start_window();
1415        assert!(det.is_window_active());
1416        det.end_window();
1417        assert!(!det.is_window_active());
1418
1419        // 结束后 record_* 应被忽略
1420        det.record_single_load("posts");
1421        assert_eq!(det.current_count("posts"), 0);
1422    }
1423
1424    #[test]
1425    fn test_n1_detector_custom_threshold() {
1426        let det = N1QueryDetector::new(N1DetectionConfig::new().with_threshold(100));
1427        det.start_window();
1428        for _ in 0..50 {
1429            det.record_single_load("posts");
1430        }
1431        let alerts = det.end_window();
1432        assert!(alerts.is_empty(), "below custom threshold should not alert");
1433
1434        det.start_window();
1435        for _ in 0..100 {
1436            det.record_single_load("posts");
1437        }
1438        let alerts = det.end_window();
1439        assert_eq!(alerts.len(), 1);
1440        assert_eq!(alerts[0].threshold, 100);
1441        assert_eq!(alerts[0].query_count, 100);
1442    }
1443
1444    #[test]
1445    fn test_n1_alert_suggested_batch_size() {
1446        let mk = |cnt: u64| N1Alert {
1447            relation: "x".into(),
1448            query_count: cnt,
1449            batch_count: 0,
1450            threshold: 5,
1451        };
1452        assert_eq!(mk(5).suggested_batch_size(), 50);
1453        assert_eq!(mk(50).suggested_batch_size(), 50);
1454        assert_eq!(mk(51).suggested_batch_size(), 100);
1455        assert_eq!(mk(100).suggested_batch_size(), 100);
1456        assert_eq!(mk(101).suggested_batch_size(), 500);
1457        assert_eq!(mk(500).suggested_batch_size(), 500);
1458        assert_eq!(mk(501).suggested_batch_size(), 1000);
1459        assert_eq!(mk(10000).suggested_batch_size(), 1000);
1460    }
1461
1462    #[test]
1463    fn test_n1_detector_real_n_plus_one_scenario() {
1464        // 模拟真实场景:循环内查询用户 posts,触发 N+1
1465        let det = N1QueryDetector::with_defaults();
1466        det.start_window();
1467        let user_ids: Vec<i64> = (1..=20).collect();
1468        for _uid in &user_ids {
1469            // 每个用户都单条查询 posts —— 典型 N+1
1470            det.record_single_load("posts");
1471        }
1472        let alerts = det.end_window();
1473        assert_eq!(alerts.len(), 1);
1474        assert_eq!(alerts[0].query_count, 20);
1475        assert!(alerts[0].no_batch_used());
1476
1477        // 对比:使用 BatchLoader 后的批量加载场景
1478        det.start_window();
1479        det.record_batch_load("posts", 20); // 一次性批量加载 20 个用户的 posts
1480        let alerts2 = det.end_window();
1481        assert!(alerts2.is_empty(), "batch loading should not trigger N+1");
1482    }
1483
1484    // ===== 集成场景测试 =====
1485
1486    #[test]
1487    fn test_workflow_graph_and_batch_loader() {
1488        // 模拟 User → Posts → Comments 的批量加载场景
1489        let mut graph = EntityGraph::new();
1490        graph.add_edge_with_graph("user", "posts", {
1491            let mut sub = EntityGraph::new();
1492            sub.add_edge("posts", "comments");
1493            sub
1494        });
1495        assert_eq!(graph.all_relations_recursive().len(), 2);
1496
1497        // 模拟批量加载用户
1498        let user_loader = BatchLoader::new(
1499            50,
1500            Box::new(|ids: &[i64]| ids.iter().map(|id| (*id, format!("User#{}", id))).collect()),
1501        );
1502
1503        // 加载 123 个用户(应分 3 批)
1504        let user_ids: Vec<i64> = (1..=123).collect();
1505        let users = user_loader.load_many(&user_ids);
1506        assert_eq!(users.len(), 123);
1507        assert_eq!(user_loader.cache_size(), 123);
1508    }
1509
1510    #[test]
1511    fn test_n_plus_1_problem_solved() {
1512        // 经典 N+1 问题演示:
1513        // - 错误做法:N 个用户各发 1 次查询加载 posts → N+1 次查询
1514        // - 正确做法:用 BatchLoader 一次批量加载 → ⌈N/batch⌉+1 次查询
1515
1516        let query_count = std::sync::Arc::new(std::sync::Mutex::new(0u32));
1517        let query_count_clone = query_count.clone();
1518
1519        let post_loader = BatchLoader::new(
1520            100,
1521            Box::new(move |user_ids: &[i64]| {
1522                *query_count_clone.lock().unwrap() += 1;
1523                // 模拟为每个 user_id 返回 posts
1524                user_ids
1525                    .iter()
1526                    .map(|uid| (*uid, format!("posts_for_user_{}", uid)))
1527                    .collect()
1528            }),
1529        );
1530
1531        // 250 个用户
1532        let user_ids: Vec<i64> = (1..=250).collect();
1533        let _posts = post_loader.load_many(&user_ids);
1534
1535        // 应分 3 批(100+100+50),调用 loader 3 次
1536        assert_eq!(*query_count.lock().unwrap(), 3);
1537    }
1538}