You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.22 21:52:04