必须显式两层preload并校验外键标签,否则permissions为nil;权限检查应转map实现o(1)查找,且预加载对象须注入context复用。

Preload 链路必须显式触达末端,否则 Permissions 为 nil
Buffalo 默认不自动预加载关联数据,user.Roles 可能非空,但 user.Roles.Permissions 很可能全是 nil —— 这不是 bug,是 gorm 的懒加载默认行为。直接遍历 role.Permissions 会静默跳过,最终权限检查永远失败。
正确做法是用 db.Preload 显式声明完整路径,并限制字段:
db.Preload("Roles").Preload("Roles.Permissions", func(db *gorm.DB) *gorm.DB {
return db.Select("code, description")
}).First(&user, userID)
- 必须写两层
Preload:先载角色,再载角色的权限;只写Preload("Roles.Permissions")会失效 -
Select()不只是优化,它还能防止因权限表含大字段(如long_description)拖慢整个查询 - 确保
Rolestruct 中Permissions字段的 gorm tag 外键名与数据库列完全一致,比如foreignKey:"role_id"而不是"RoleID"
中间件里别查库,用 c.MustGet("user") 注入已预加载对象
常见错误是在权限中间件里重复调用 Find(&user) + Preload,既浪费 DB 连接,又可能因并发导致 context 被污染(A 用户权限覆盖 B 用户)。
Buffalo 的生命周期允许你在认证中间件中一次性加载并注入:
Buffalo框架 1.0.1 版本源码包下载,适合需要错误处理改进、依赖更新、render.Download 注释和 request logger 调整的 v1 项目。
func AuthMiddleware(next buffalo.Handler) buffalo.Handler {
return func(c buffalo.Context) error {
userID := c.Session().Get("user_id")
var user models.User
err := models.DB.Preload("Roles.Permissions", func(db *gorm.DB) *gorm.DB {
return db.Select("code")
}).Find(&user, userID).Error
if err != nil {
return c.Error(401, err)
}
c.Set("user", user) // 后续中间件和 handler 直接用
return next(c)
}
}
- 后续所有 handler 用
c.MustGet("user").(*models.User)拿对象,不再查库 - 务必用
MustGet而非Get,避免空指针 panic;类型断言要带*,因为存的是指针 - 这个中间件必须在路由匹配之后、handler 执行之前注册,顺序错会导致
c.Session()不可用
权限码用 map[string]struct{} 做 O(1) 检查,别遍历 slice
即使预加载成功,如果每次检查都用 slice.Contains(permissions, "user:write"),等于把 N 条权限做 N 次线性扫描——100 个权限就要最多 100×100 次比较。
应在注入 user 对象后,立刻构建查找表:
func (u *User) PermissionMap() map[string]struct{} {
m := make(map[string]struct{})
for _, r := range u.Roles {
for _, p := range r.Permissions {
m[p.Code] = struct{}{}
}
}
return m
}
- 在 handler 或中间件里调用
user.PermissionMap()["user:delete"] != nil,性能稳定 - 不要把这个 map 存进 session 或 context,它不含敏感数据,但也不该跨请求复用(user 权限可能变更)
- 如果权限极少(如固定 5 个),用
switch也行,但动态角色场景下 map 更可靠
开发期用 DB.LogMode(true) 看真实 SQL,别信日志里的「预加载成功」
gorm 日志里显示 SELECT * FROM roles WHERE user_id IN (?) 并不表示 permissions 也被查了。真正决定是否发出第二条 JOIN 或子查询的,是 Preload 调用时机和链路完整性。
- 启动时加
models.DB.LogMode(true),观察是否有SELECT ... FROM permissions WHERE role_id IN (?) - 如果只看到 role 查询,说明 Preload 路径断了,回去核对 struct tag 和 Preload 字符串
- 线上禁用 LogMode,改用
DB.Debug()临时开启,避免日志 IO 拖垮吞吐
最常被忽略的是外键命名和 Preload 字符串大小写——Preload("roles.permissions")(小写)在 struct 字段是 Roles(大写)时必然静默失败。










