C# MongoDB如何实现文档引用而非嵌入?原LiteDB用户咨询
在MongoDB C#驱动中实现跨集合引用与关联加载
需求回顾
需要将Phonebook和Category分别存储在独立集合中,Phonebook的Categories属性仅保存Category集合条目的引用,且支持类似LiteDB的关联加载能力(不加载时仅返回引用ID,加载时获取完整对象),同时Category需独立管理、多处复用,不能嵌入到Phonebook文档中。
原实体类定义:
public class Phonebook { public Guid Id {get; set;} public string Name {get; set;} public List<Category> Categories {get; set;} } public class Category { public Guid Id {get; set;} public string Name {get; set;} }
原LiteDB实现方式:
// 指定Categories为引用类型 BsonMapper.Global.Entity<PhoneBook>() .DbRef(x => x.Categories, GetCollectionName<Category>()); // 加载关联数据 database.GetCollection<Phonebook>() .Include(x => x.Categories).FindOne(x => x.Id == myId);
MongoDB C#驱动实现方案
1. 调整实体模型(核心步骤)
MongoDB中没有原生的"DbRef自动映射"机制,需要手动将Phonebook的Categories拆分为持久化的ID引用列表和非持久化的对象集合,或者直接存储ID列表:
方案1:拆分属性(代码可读性更高)
using MongoDB.Bson.Serialization.Attributes; public class Phonebook { public Guid Id { get; set; } public string Name { get; set; } // 存储到MongoDB的Category ID引用列表 [BsonElement("categoryIds")] public List<Guid> CategoryIds { get; set; } = new List<Guid>(); // 非持久化属性,用于加载后填充完整Category对象 [BsonIgnore] public List<Category> Categories { get; set; } = new List<Category>(); } public class Category { public Guid Id { get; set; } public string Name { get; set; } }
方案2:直接存储ID列表(简化模型)
如果不需要在代码中保留Categories属性名称,可直接使用CategoryIds存储引用,加载时手动关联:
public class Phonebook { public Guid Id { get; set; } public string Name { get; set; } public List<Guid> CategoryIds { get; set; } = new List<Guid>(); } public class Category { public Guid Id { get; set; } public string Name { get; set; } }
2. 实现关联加载(类似LiteDB的Include)
MongoDB C#驱动没有内置的自动关联加载方法,需要手动完成"查询主文档→收集引用ID→查询关联文档→映射关联"的流程:
单个Phonebook加载关联数据
// 假设已初始化MongoDB集合 var phonebookCollection = _database.GetCollection<Phonebook>("phonebooks"); var categoryCollection = _database.GetCollection<Category>("categories"); var targetPhonebookId = new Guid("你的目标ID"); // 1. 查询目标Phonebook var phonebook = await phonebookCollection.Find(p => p.Id == targetPhonebookId) .FirstOrDefaultAsync(); if (phonebook != null && phonebook.CategoryIds.Any()) { // 2. 根据ID列表查询对应的Category var categories = await categoryCollection.Find(c => phonebook.CategoryIds.Contains(c.Id)) .ToListAsync(); // 3. 填充关联属性(方案1适用) phonebook.Categories = categories; }
批量Phonebook加载关联数据(高效版)
避免N+1查询,先批量获取所有Phonebook,再一次性查询所有需要的Category:
// 1. 查询所有Phonebook var phonebooks = await phonebookCollection.Find(FilterDefinition<Phonebook>.Empty) .ToListAsync(); // 2. 收集所有需要的Category ID并去重 var allCategoryIds = phonebooks.SelectMany(p => p.CategoryIds) .Distinct() .ToList(); if (allCategoryIds.Any()) { // 3. 批量查询Category并建立ID映射 var categories = await categoryCollection.Find(c => allCategoryIds.Contains(c.Id)) .ToListAsync(); var categoryMap = categories.ToDictionary(c => c.Id); // 4. 为每个Phonebook填充关联数据 foreach (var pb in phonebooks) { pb.Categories = pb.CategoryIds .Select(id => categoryMap.TryGetValue(id, out var cat) ? cat : null) .Where(cat => cat != null) .ToList(); } }
3. 可选:使用MongoDB DBRef(不推荐)
MongoDB提供了DBRef类型用于跨集合引用,但官方不推荐使用(仅适合跨数据库引用场景),因为它需要额外存储集合名称,且查询流程更繁琐:
实体模型调整
using MongoDB.Bson.Serialization.Attributes; using MongoDB.Driver; public class Phonebook { public Guid Id { get; set; } public string Name { get; set; } public List<DBRef> Categories { get; set; } = new List<DBRef>(); } public class Category { public Guid Id { get; set; } public string Name { get; set; } }
存储与加载示例
// 存储时创建DBRef var category = await categoryCollection.Find(c => c.Name == "Work").FirstOrDefaultAsync(); var newPhonebook = new Phonebook { Id = Guid.NewGuid(), Name = "Office Contacts", Categories = new List<DBRef> { new DBRef("categories", category.Id) } }; await phonebookCollection.InsertOneAsync(newPhonebook); // 加载时解析DBRef var phonebook = await phonebookCollection.Find(p => p.Id == newPhonebook.Id).FirstOrDefaultAsync(); if (phonebook != null && phonebook.Categories.Any()) { var categories = new List<Category>(); foreach (var dbRef in phonebook.Categories) { var category = await categoryCollection.Find(c => c.Id == (Guid)dbRef.Id).FirstOrDefaultAsync(); if (category != null) categories.Add(category); } // 使用categories集合 }
方案选择建议
优先选择手动存储ID列表+关联查询的方案,原因如下:
- 更符合MongoDB的文档设计理念,避免不必要的复杂结构
- 查询性能更可控,可灵活优化批量查询逻辑
- 代码可读性更高,维护成本低
内容的提问来源于stack exchange,提问作者Stephan Steiner
相关产品推荐
相关产品推荐

