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