从 PHP 到 AI + Golang,程序员自救转型手记(五十一):管理员和角色组的关联 📅 2026/8/5 1:23:50 这是一个系列 Blog作者将以一个 PHP 全栈工程师的身份利用 AI 工具claude code、codex、deepseek、豆包等从零开始学习 golang 语言并最终完成 ai-go-admingithub | gitee开源项目的制作感谢 star ~全程记录分享。在上一期我们进行了 “增加管理员角色组管理”本期将完成管理员和角色组的关联管理员和角色组的关联目前系统内还没有一处使用了关联表这里就以管理员和角色组来跑路关联的流程先直接让 AI 提供建议切换为plan模式接下来需要完成Admin和AdminGroupAccess模型的关联它们定义于internal/model/admin.go用什么方式关联比较好同时我需要为基类增加快速配置关联的功能在什么地方配置配置名叫什么比较好GORM 预加载复习以上提示词说的比较笼统甚至使用什么关联模式都是让 AI 提供方案在它分析的时候这里我也是赶紧打开 GORM 的学习笔记查看一下各种关联方式的预加载写法。我要做不只是写好管理员和角色组的预加载这直接重写仓储层就行我的目的是在控制器配置一个字段仓储层就能给我加载好供我使用即 基类 对关联预加载的支持。预加载的写法有PreloadtypeUserstruct{gorm.Model UsernamestringOrders[]Order}typeOrderstruct{gorm.Model UserIDuintPricefloat64}// 预加载 Order 数据参数二可以传递一个 func(db gorm.PreloadBuilder) error 闭包函数里边拼接查询条件、排序方式等.Preload(Order,nil)// 条件预加载.Preload(Orders,state NOT IN (?),cancelled)// 嵌套.Preload(Orders.OrderItems.Product)Joins 预加载还是上面的模型定义使用 Joins 预加载是这样写的// 参数二同样可以传递一个闭包签名是: func(db gorm.JoinBuilder, joinTable clause.Table, curTable clause.Table) error.Joins(clause.JoinTarget{Association:Order},nil)Joins 预加载的性能无疑是最好的因为它在单条 SQL 中使用左连接来加载关联数据。AI 初版完成提供的方案一般般在刚刚复习完笔记的我来看甚至有点low首先是控制器加了个可选参数函数WithPreloads然后将设置的Preloads传递到仓储层于仓储层直接// 预加载关联for_,p:rangeopts.Preloads{qq.Preload(p,func(pb gorm.PreloadBuilder)error{returnnil})}Preload写法然后加载是加载了闭包函数是固定的return nil以下是找到的问题或改善点。参数和 GORM 的 Preloads 同步控制器层定义预加载选项Preloads时支持到第二个参数其类型和GORM的Preloads第二个参数同步。// Preload 预加载关联配置typePreloadstruct{Namestring// 关联名称Queryfunc(gorm.PreloadBuilder)error// 可选的自定义查询条件为 nil 时仅按名称预加载}AI 上来就定义了一个type Preload struct第一个字段为Name string我对比了一下Preload的签名它第一个参数是association string这个 AI 不严谨啊让它先改正。控制器层关联配置优化// PreloadFields 声明对应方法的预加载关联配置typePreloadFieldsstruct{Get[]repository.Preload List[]repository.Preload Update[]repository.Preload Create[]repository.Preload}这里是 AI 模仿的ActionFields给Get、List、Update、Create各提供了一个定义Preload的位置其中在Update、Create中这东西基本没用直接去掉List几乎是主要使用方Get使用频率低且就算额外查了List的Preload也没关系。总而言之简化为单个Preloads []repository.Preload配置即可不需要区分方法。仓储层 Preload 调用优化AI 原本是这样写的for_,p:rangeopts.Preloads{query:p.Queryifquerynil{queryfunc(pb gorm.PreloadBuilder)error{returnnil}}qq.Preload(p.Association,query)}实际上直接写q q.Preload(p.Association, nil)都是可以的并不需要nil守卫直接去除。关联筛选查询我们的表格是支持多字段筛选的比如使用关联模型group的name字段模糊查询前端几乎无需改动表格组件和表格管家的公共搜索组件自然就能传递这种数据至服务端{wheres:[{field:group.name,operator:ILIKE,value:1,},{field:group.title,operator:ILIKE,value:1,},],or:true}问题在于服务端在何处应用这些筛选数据首先肯定是考虑使用Preload(association string, query func(db gorm.PreloadBuilder)的第二个闭包函数但这个理解是错误的gorm.G[Admin](db).Preload(Group,func(pb PreloadBuilder)error{pb.Where(name LIKE ?,%foo%)returnnil}).Find(ctx)产生两条 SQL-- 1. 主查询所有 Admin全部返回SELECT*FROMadmins;-- 2. 预加载只加载 name LIKE foo 的 groupSELECT*FROMadmin_groupsWHEREidIN(?,?,?)ANDnameLIKE%foo%;结果是所有Admin都会在结果里只是部分行不匹配的的Group数据将被置为nil。我们要的筛选语义显然是只返回group.name匹配的Admin的行。这需要主查询过滤Preload帮不上。唯一干净的方式子查询顺着关联链层层套EXISTS/IN子查询即可。SELECT*FROMadminsWHEREadmins.group_idIN(SELECTidFROMadmin_groupsWHEREnameLIKE%foo%);就按子查询的方式让 AI 实现出来在已有的BuildWhereScopes、BuildWhereExpr完成关联表字段名检查等这里实际上的实现比较复杂而且人工review了很久不过好在是实现出来了而且很优雅。现在前端可以直接发送任意表的筛选数据最终组装好对应的 SQL 进行查询比如主表的name LIKE %foo%关联表的group.name LIKE %foo%。