Skip to content

Conversation

@LanceAdd
Copy link
Member

@LanceAdd LanceAdd commented Jan 26, 2026

解决With/WithAll的多层递归的查询次数N+1问题并且增加更加可控的颗粒度分批查询控制

前提

#4620 中提到了多层嵌套查询导致查询次数不可控暴涨问题,所以有了此次改进,允许逐层统一收集当前结果然后统一查询下一层数据,也可以控制每一层在达到指定数量后使用分批查询,也可以什么也不改使用原本的默认查询方式

提前说明

  1. WithWIthAll功能原本的设计我不清楚是不是故意设计成这种可能无限膨胀的模式,也有可能时希望开发者自己控制层数不要太深,所以我并不清楚我的这次改进是否有意义,需要大家来判断。
  2. 这次的改进有点绕并且使用了反射,当然了也加了缓存避免多余反射,只用模拟数据看来这次避免查询次数爆炸的优化(可能算是优化吧)算是正面提升,但是否符合大家的实际情况需要大家讨论。
  3. 如果你不主动启用优化使用原本查询方式也能因为缓存得到一定的提升,这个应该算是优化。
  4. 已知的情况包括:不使用优化那还保持原本查询方式;如果层数不多,数据量不大的情况下,统一查询的效果比默认查询和分批查询快,毕竟没有那么多次网络开销;如果数据量很大层数很深的情况下,合理划分分批查询确实可以有效平滑查询次数和查询速度,并且避免查询in条件的参数数量限制。
  5. 可能需要大家帮忙凑点合理的测试用例,我的数据环境过于单一了,而中文注释是为了方便大家review,如果最后需要merge我再删掉。

概述

本次改进在保留原有的 WithAll 功能基础上,新增了 WithBatch 功能,解决传统 ORM 关联查询中的 N+1 问题。

核心改进

1. 保留原有功能

  • WithAll 保持原有行为:启用所有带 "with" 标签的对象关联操作
  • With 保持原有行为:为指定对象启用关联操作
  • 向后兼容性得到保障

2. 批量查询优化

通过 WithBatch() 方法启用批量查询功能,将 N+1 查询优化为固定次数的批量查询:

  • 主表查询:1 次
  • 关联表批量查询:1 次(使用 WHERE IN 子句)

3. 分批处理机制

针对大数据量场景,引入分批处理机制:

  • batchSize:每批处理的最大记录数
  • batchThreshold:启用批量查询的最小记录数阈值

使用方法

1. 基础用法

定义关联结构体

type User struct {
    gmeta.Meta `orm:"table:user"`
    Id         int           `json:"id"`
    Name       string        `json:"name"`
    UserDetail *UserDetail   `orm:"with:uid=id"`      // 一对一关联
    UserScores []*UserScores `orm:"with:uid=id"`      // 一对多关联
}

type UserDetail struct {
    gmeta.Meta `orm:"table:user_detail"`
    Uid        int    `json:"uid"`
    Address    string `json:"address"`
}

type UserScores struct {
    gmeta.Meta   `orm:"table:user_scores"`
    Id           int `json:"id"`
    Uid          int `json:"uid"`
    Score        int `json:"score"`
}

普通关联查询(可能存在N+1问题)

var users []*User
err := db.Model("user").WithAll().Scan(&users)

启用批量优化查询

var users []*User
err := db.Model("user").WithBatch().WithAll().Scan(&users)

2. 高级用法

在结构体标签中配置分批参数

type User struct {
    gmeta.Meta `orm:"table:user"`
    Id         int           `json:"id"`
    Name       string        `json:"name"`
    // 配置分批参数:阈值为5,每批最多5条记录
    UserDetail *UserDetail   `orm:"with:uid=id, batch:threshold=5,batchSize=5"`
    // 配置分批参数:阈值为10,每批最多10条记录
    UserScores []*UserScores `orm:"with:uid=id,batch:threshold=10,batchSize=10"`
}

四层嵌套关联查询示例

// 四层数据结构定义
type DetailMeta struct {
    gmeta.Meta `orm:"table:detail_meta"`
    Id         int    `json:"id"`
    DetailId   int    `json:"detail_id"`
    MetaKey    string `json:"meta_key"`
    MetaValue  string `json:"meta_value"`
}

type UserScoreDetails struct {
    gmeta.Meta `orm:"table:user_score_details"`
    Id         int           `json:"id"`
    ScoreId    int           `json:"score_id"`
    DetailInfo string        `json:"detail_info"`
    // 第四层关联,配置批量参数
    DetailMeta []*DetailMeta `orm:"with:detail_id=id,batch:threshold=100,batchSize=200"`
}

type UserScores struct {
    gmeta.Meta   `orm:"table:user_scores"`
    Id           int                 `json:"id"`
    Uid          int                 `json:"uid"`
    Score        int                 `json:"score"`
    ScoreDetails []*UserScoreDetails `orm:"with:score_id=id"`
}

type UserDetail struct {
    gmeta.Meta `orm:"table:user_detail"`
    Uid        int    `json:"uid"`
    Address    string `json:"address"`
}

type User struct {
    gmeta.Meta `orm:"table:user"`
    Id         int           `json:"id"`
    Name       string        `json:"name"`
    UserDetail *UserDetail   `orm:"with:uid=id, batch:threshold=5,batchSize=5"`
    UserScores []*UserScores `orm:"with:uid=id,batch:threshold=10,batchSize=10"`
}

// 查询用法
func queryUsers() {
    var users []*User
    
    // 方式1:普通关联查询(可能有N+1问题)
    err := db.Model("user").WithAll().Scan(&users)
    
    // 方式2:启用批量优化查询
    err := db.Model("user").WithBatch().WithAll().Scan(&users)
    
    if err != nil {
        // 处理错误
    }
}

3. 性能优化建议

选择合适的批量参数

  • batchThreshold:对于小数据集(< 5条记录),可以不启用批量查询
  • batchSize:根据数据库性能和内存情况调整,一般建议 50-500

4. 实现原理

批量查询核心逻辑

  1. 收集所有需要关联查询的外键值
  2. 使用 WHERE IN 子句批量查询关联数据
  3. 将批量查询结果映射回主数据结构

分批处理策略

当关联数据量超过 batchSize 时,系统会自动将查询分批执行:

// 伪代码示例
var ids = gconv.SliceAny(relatedTargetValue)
for i := 0; i < len(ids); i += batchSize {
    end := i + batchSize
    if end > len(ids) {
        end = len(ids)
    }
    // 批量查询部分数据
    result, err := model.Where(relatedSourceName, ids[i:end]).All()
}

5. 注意事项

  1. 关联字段标签:确保关联字段有正确的 orm:"with:..." 标签
  2. 数据类型匹配:关联字段的类型必须匹配(如外键字段类型一致)
  3. 批量参数配置:合理设置 batchThresholdbatchSize 参数
  4. 嵌套层级:避免过深的嵌套层级,可能导致查询复杂度过高
  5. 内存管理:大数据量查询时注意内存使用情况

优势对比

查询方式 SQL查询次数 性能表现 内存使用
无优化(N+1) O(n)
WithBatch O(层数) 中等
WithBatch + 参数配置 可控 很好 可控

通过合理使用 WithBatch 功能和适当的参数配置,可以显著提升关联查询的性能,有效解决 N+1 查询问题。

LanceAdd and others added 15 commits January 25, 2026 22:19
- 在Model结构体中新增withBatchEnabled、withBatchOptions和withBatchDepth字段
- 实现WithBatch和WithBatchOption方法用于配置批量递归扫描参数
- 添加doScanListParseRelation和doScanListGetBindAttrInfo辅助函数
- 实现doScanListGetBatchRecursiveMap核心批量查询优化逻辑
- 新增doScanListAssignmentLoop最终赋值循环处理函数
- 添加针对slice、pointer、struct类型的分别处理逻辑
- 实现getOptionForCurrentLayer方法用于获取当前层级配置
- 在关联查询递归扫描中传递批量配置参数
- 新增完整的高级场景单元测试覆盖多层嵌套和边界情况
- 实现大数据量级复杂条件查询测试
- 验证软删除功能与unscoped参数的正确性
- 测试复杂关联查询中的where和order条件
- 添加大批量数据分批查询功能验证
- 实现边界和异常情况的测试场景
- 引入全局字段缓存管理器减少反射操作
- 使用缓存的字段索引替代 FieldByName 动态查找
- 优化指针类型判断逻辑提升访问效率
- 改进空值检查条件避免不必要的反射调用
- 保持嵌入字段的动态查找功能不变
- 删除 advanced test 和 bench comparison test 文件
- 删除 depth test 文件
- 新增四层关联查询的中等数据集测试
- 定义四层嵌套的数据结构和表关系
- 配置批量查询参数验证分批查询功能
- 设置中小型数据规模进行性能对比测试
- 移除不再使用的 fieldKeys 变量和相关反射字段提取逻辑
- 修改缓存键策略,使用 reflect.Type 直接作为缓存键而非字符串拼接
- 重构 fieldCacheItem 结构,移除标签解析和批处理设置的缓存存储
- 实现嵌入字段的完整索引路径支持,添加 bindToAttrIndexPath 和 relationAttrIndexPath
- 更新批处理递归扫描逻辑,修正从 DataMap 提取子记录的方式
- 调整字段访问逻辑,支持通过缓存的索引路径访问嵌入字段
- 优化字段名称映射,保持大小写不敏感查找功能
- 修正批量查询中的键值提取逻辑,直接从结构体字段获取关联值
- 更新注释说明,明确缓存用途和动态语义不缓存的原因
将 `reflect.Ptr` 替换为 `reflect.Pointer` 以使用更推荐的类型常量,并移除冗余空行。
@LanceAdd LanceAdd marked this pull request as draft January 28, 2026 05:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants