
本文详解如何使用 mgo 在 go 中正确建模父子文档关系——既保持结构清晰(parent 中嵌入 child 类型),又实现物理分离存储(child 存于独立集合,parent 仅存其 objectid 引用)。
在 MongoDB 应用开发中,常需权衡「嵌入(embedding)」与「引用(referencing)」两种数据建模策略。当业务逻辑天然将 Child 视为独立实体(如需被多个 Parent 共享、单独查询或频繁更新),而 Parent 仅需持有其身份标识时,逻辑嵌入 + 物理引用是最佳实践。mgo(现已归档,但广泛用于遗留项目)本身不提供 ORM 式的关系管理,需通过结构体标签与类型设计显式控制序列化行为。
核心问题在于:你希望 Parent 结构体中字段 B Child 在保存到 parents 集合时只存 Child.Id(即 ObjectId 引用),而非整个 Child 文档;同时 Child 实例自身应能完整存入 children 集合(含 Id 和 C 字段)。错误地使用 bson:"-" 会彻底忽略该字段,导致 children 集合中 C 字段丢失。
✅ 正确解法是区分上下文,使用不同结构体或精准控制 BSON 标签:
方案一:利用 bson:",omitempty" + 零值设计(轻量推荐)
type Child struct {
Id bson.ObjectId `json:"_id,omitempty" bson:"_id,omitempty"`
C string `json:"c" bson:"c"` // 始终参与序列化
}
type Parent struct {
Id bson.ObjectId `json:"_id,omitempty" bson:"_id,omitempty"`
A string `json:"a" bson:"a"`
B Child `json:"b" bson:"b_id"` // 字段名映射为 b_id,且仅存 Id
}⚠️ 注意:Parent.B 是 Child 类型,但 mgo 序列化时默认会尝试写入整个结构。要使其仅存 Id,需确保 Child 的其他字段(如 C)在作为 Parent.B 使用时为零值,并配合 omitempty:
// 存储 Parent 时,显式构造一个仅含 Id 的 Child 引用
childRef := Child{Id: child.Id} // C 为 ""(零值)
parent := Parent{
Id: bson.NewObjectId(),
A: "Just a string",
B: childRef, // 序列化后 -> { "b_id": ObjectId("...") }
}此时 bson:"b_id,omitempty" 使 C 因为空字符串被忽略,最终 parents 集合中只存 b_id 字段。
方案二:定义专用引用类型(更清晰、推荐)
type Child struct {
Id bson.ObjectId `json:"_id,omitempty" bson:"_id,omitempty"`
C string `json:"c" bson:"c"`
}
// 专用于引用场景,无冗余字段
type ChildRef struct {
Id bson.ObjectId `json:"_id" bson:"_id"`
}
type Parent struct {
Id bson.ObjectId `json:"_id,omitempty" bson:"_id,omitempty"`
A string `json:"a" bson:"a"`
B ChildRef `json:"b" bson:"b_id"` // 明确语义:这是引用
}存储时:
parent := Parent{
Id: bson.NewObjectId(),
A: "Just a string",
B: ChildRef{Id: child.Id}, // 确保类型安全,无歧义
}此方案优势明显:
- 类型系统强制区分「完整文档」与「引用标识」;
- 避免零值陷阱,逻辑自解释;
- 同一 children 集合仍可直接用 Child 类型操作(c := Child{...}; session.DB("db").C("children").Insert(c))。
关键注意事项
- 不要在引用字段上滥用 bson:"-":它会完全跳过字段,导致目标集合缺失必要数据;
- ObjectId 是引用的黄金标准:MongoDB 官方推荐仅存储 ObjectId(而非字符串或复合键),配合集合名隐含语义(如 b_id 指向 children 集合);
- 手动处理关联查询:mgo 不支持自动 JOIN,需分两步:先查 Parent 获取 b_id,再用该 ObjectId 查询 children 集合;
- 考虑升级驱动:mgo 已停止维护,生产环境建议迁移到官方 mongo-go-driver,其 bson 标签行为更规范,且支持 MarshalBSON/UnmarshalBSON 方法实现精细控制。
总结:关系建模的本质是语义与存储的解耦。通过结构体拆分(Child vs ChildRef)或标签策略(omitempty + 零值约定),即可在 mgo 中优雅实现“代码中嵌入、数据库中分离”的设计目标——既保持 Go 代码的可读性与类型安全,又遵循 MongoDB 的文档存储范式。










