beego orm 的 rel(fk) 与 reverse(many) 需双向正确配对才生效:rel(fk) 写在含外键方(字段为 parent),reverse(many) 写在被引用方(字段为 []child),且 column 名、tag 拼写、类型必须严格匹配,否则 relatedsel() 和 deleteall() 均无效。

Beego ORM 的 rel(fk) 与 reverse(many) 怎么配才生效
Beego ORM 的级联行为不是自动开启的,必须靠 struct tag 显式声明外键和反向关系,且两边都要对得上。只写一边、字段类型错、tag 拼写错误(比如写成 rel(fk 少了右括号),都会让 QueryTable().Filter("Field", id).All() 返回空切片。
常见错误现象:查出主对象没问题,但 .RelatedSel().All() 拿不到子数据,日志里也没有 SQL JOIN 或额外 SELECT;或者手动调用 Delete 后,关联表记录还在。
-
rel(fk)必须写在「拥有外键」的一方,字段类型是 *ParentStruct(一对一)或 *ParentStruct(一对多时也用指针,但实际存的是外键值) -
reverse(many)写在「被引用」的一方,字段类型必须是[]*ChildStruct(切片指针),不能是[]ChildStruct - 外键字段名要和数据库真实列名一致,比如
orm:"column(user_id);rel(fk)",否则 ORM 构建 SQL 时会用默认名(如user_id),但表里可能是uid
示例:
type User struct {
Id int64 `orm:"pk;auto"`
Name string `orm:"size(100)"`
}
type Article struct {
Id int64 `orm:"pk;auto"`
Title string `orm:"size(200)"`
UserId int64 `orm:"column(user_id);rel(fk)"`
User *User `orm:"rel(fk)"`
}
type Comment struct {
Id int64 `orm:"pk;auto"`
Content string `orm:"size(500)"`
Article *Article `orm:"column(article_id);rel(fk)"`
ArticleId int64 `orm:"column(article_id)"`
}
// 反向关系写在被引用方(User 和 Article)
func (u *User) TableName() string { return "user" }
func (a *Article) TableName() string { return "article" }
func (c *Comment) TableName() string { return "comment" }
级联删除要自己写,o.Delete() 不会自动删子表
Beego ORM 的 o.Delete(&obj) 默认只删当前对象对应行,不触发表间约束或触发器,也不递归调用子对象 Delete。所谓“级联删除”必须手动实现:先查子集,再逐个删,或用原生 SQL + 事务包裹。
容易踩的坑:以为加了 rel(fk) 就能级联删;或在事务中漏了 o.Begin()/o.Commit(),导致部分删除成功、部分失败,数据不一致。
- 用
QueryTable().Filter("foreign_key_field", id).DeleteAll()是最稳妥的批量删法,它生成 DELETE 语句,不加载对象到内存 - 如果需要删前校验(比如检查子记录是否可删),就得先
.All()加载,再循环调o.Delete(),但要注意性能——1000 条子记录意味着 1000 次 DB round-trip - MySQL 开启了
ON DELETE CASCADE的外键约束时,可以直接用原生 SQL:o.Raw("DELETE FROM user WHERE id = ?", uid).Exec(),依赖数据库层级联,ORM 不干预
推荐做法(带事务):
o := orm.NewOrm()
err := o.Begin()
if err != nil {
return err
}
_, err = o.QueryTable("comment").Filter("article_id", aid).DeleteAll()
if err != nil {
o.Rollback()
return err
}
_, err = o.QueryTable("article").Filter("id", aid).DeleteAll()
if err != nil {
o.Rollback()
return err
}
return o.Commit()
RelatedSel() 查关联数据时,为什么 SQL 没有 JOIN 而是发了 N+1 查询
Beego ORM 的 RelatedSel() 默认走的是“懒加载 + 多次查询”模式,不是 SQL JOIN。它先查主表,再对每条主记录,单独发一条 SELECT 去查关联表。当主结果有 100 行,关联表又没加索引时,就是 101 次查询,很容易拖垮 DB。
这不是 bug,是设计选择:JOIN 在复杂条件或深嵌套时 SQL 难写难调,而分步查更可控。但如果你明确知道要一次性拉取全部关联数据,就得换策略。
- 用
Raw()手写 JOIN 查询,然后RowsToStruct()或Values()映射,适合固定结构 - 用
QueryBuilder构建多表 SELECT,再手动组装结构体,适合动态字段 - 保持
RelatedSel(),但给外键字段加数据库索引(比如article.user_id),把单次子查控制在毫秒级
例如避免 N+1:
// ❌ 容易 N+1
var users []*User
o.QueryTable("user").All(&users)
for _, u := range users {
var articles []*Article
o.QueryTable("article").Filter("user_id", u.Id).RelatedSel().All(&articles) // 每次都查一次
}
// ✅ 改成一次 JOIN 查询
var results []map[string]interface{}
sql := `SELECT u.id, u.name, a.title, a.id as article_id
FROM user u
LEFT JOIN article a ON u.id = a.user_id`
o.Raw(sql).Values(&results)
事务中混合使用 QueryTable 和 Raw 会丢连接
Beego ORM 的事务对象 Orm 是有状态的,一旦调用 o.Begin(),后续所有操作(包括 QueryTable 和 Raw)都必须用同一个 o 实例,否则会回退到默认连接池,导致事务失效。
典型错误:在事务块里调 orm.NewOrm() 新建一个句柄去执行 Raw,或者把 o 传进某个工具函数后,在函数里又调了 orm.NewOrm()。
- 所有事务内操作,统一用开始事务时的那个
o句柄,不要重新 New -
QueryTable("table")必须基于该o,即o.QueryTable("table"),而不是全局orm.QueryTable("table") -
Raw()同理,必须是o.Raw(...),且参数绑定要一致(比如都用?占位符)
最容易被忽略的点:Beego 的 Controller 里默认用的是全局 orm.NewOrm(),你在 Get() 方法里开了事务,但中间调了一个封装好的 service 函数,而那个函数内部又用了 orm.NewOrm() —— 此时事务就断了,删了一半数据,另一半没删,还 commit 成功了。











