
本文详解如何使用 mgo 在 go 中正确建模 mongodb 的引用关系,使 parent 文档仅保存 child 的 objectid 引用,而 child 作为独立文档完整存入 children 集合,避免嵌入式序列化陷阱。
在使用 mgo(现已归档,但仍在维护项目中广泛使用)进行 MongoDB 开发时,一个常见误区是误用 BSON 标签导致数据持久化行为不符合预期。如问题所示,开发者希望 Parent 结构体中只保存对 Child 的引用(即 Child.Id),而 Child 本身作为完整文档独立存储于 children 集合中——这属于典型的「引用式关系(Referenced Relationship)」,而非「嵌入式关系(Embedded Relationship)」。
关键在于:不能将 Child 类型直接作为 Parent 的字段值并期望它自动“降级”为 ObjectId 引用。mgo 默认会尝试序列化整个 Child 结构体(包括 C 字段),除非显式控制字段的 BSON 序列化行为。
✅ 正确做法:使用 bson:",omitempty" 或专用引用类型
方案一:调整 BSON 标签(推荐初学者使用)
修改 Child 定义,保留结构体完整性,但通过标签控制其在不同上下文中的序列化行为:
type Child struct {
Id bson.ObjectId `json:"_id,omitempty" bson:"_id,omitempty"`
C string `json:"c" bson:"c"` // 正常序列化字段
}
// 当 Child 作为独立文档插入 children 集合时:
err := session.DB("mydb").C("children").Insert(child) // ✅ 存储完整 { _id: ..., c: "panino" }
// 当 Child 作为 Parent 的字段时,我们不希望它被嵌入,而是仅存 ID ——
// 所以应在 Parent 中定义为 *bson.ObjectId 或自定义引用字段(见下文),而非 Child 类型!⚠️ 注意:原代码中 Parent.B 类型为 Child 是根本性错误。若你希望 B 仅表示引用,则它不应是 Child 实例,而应是 bson.ObjectId 或字符串:
type Parent struct {
Id bson.ObjectId `json:"_id,omitempty" bson:"_id,omitempty"`
A string `json:"a" bson:"a"`
ChildId bson.ObjectId `json:"child_id" bson:"child_id"` // ✅ 纯引用字段
}这样,插入 Parent 时只会写入 child_id 字段(如 "507f1f77bcf86cd799439011"),完全解耦两个集合。
方案二:定义专用引用类型(更清晰、类型安全)
为语义明确,建议为引用场景单独定义轻量类型:
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"` // 明确表示“这是引用”,非嵌入
}此时 Parent.B 仅序列化 Id 字段(因 ChildRef 无其他导出字段),且类型系统清晰表达了设计意图。
❌ 错误示例解析
原问题中尝试用 bson:"-" 忽略 C 字段,会导致:
- Child 插入 children 集合时丢失 C 值(仅存 _id),违背「Child 作为完整文档」的目标;
- Parent.B 仍是 Child 类型 → mgo 仍会尝试序列化整个结构体(即使 C 被忽略,_id 仍存在),造成冗余或逻辑混乱。
? 小贴士:bson:",omitempty" 仅在字段值为零值(如 "", 0, nil, ObjectId(""))时跳过序列化;而 bson:"-" 是强制忽略,无论值是否为空。二者语义截然不同。
总结与最佳实践
- 永远区分「实体」与「引用」:Child 是实体类型,用于操作 children 集合;引用应使用 bson.ObjectId 或专用 XXXRef 类型。
- 避免在父文档中嵌入子结构体,除非你明确需要嵌入式模型(此时子字段应存在于父文档 BSON 中)。
- 使用 session.DB("mydb").C("parents").Insert(parent) 和 session.DB("mydb").C("children").Insert(child) 分别写入,再通过应用层关联(如 FindId(parent.ChildId))实现 JOIN 语义。
- 如需服务端联查,可考虑 MongoDB 3.2+ 的 $lookup 聚合阶段,但需注意性能与分片兼容性。
通过合理设计类型与 BSON 标签,你就能在 mgo 中精准控制文档关系,兼顾数据一致性与查询灵活性。










