Overview
在本指南中,您可以学习如何使用 MongoDB Entity Framework Core 框架提供商建模 MongoDB 数据库中的文档之间的关系。EF Core 提供商支持以下关系类型:
嵌入式关系:直接嵌入父文档中的子文档。这是 MongoDB 中一对一和一对多关系的推荐方法。
手动引用:引用另一个文档的文档,通过将其 ID 存储在字段中,而不是将文档嵌入为子文档。这种方法非常适合建模多对多关系,或在需要独立访问文档时使用。
提示
根据查询模式设计模式
为 MongoDB 建模数据时,首先确定应用程序最常运行的操作。然后,选择文档结构以高效支持这些操作。MongoDB 中最高效的模式可能与您用于关系数据库的模式不匹配。取而代之,请考虑哪些数据通常会被一起检索,哪些值会频繁变化,以及哪些数据关系会随时间的推移而变化。
要了解有关数据建模以及从关系数据库迁移到 MongoDB Server 的更多信息,请参阅 MongoDB Server 文档中的 SQL 到 MongoDB 映射图表和 MongoDB 中的数据建模。
嵌入式关系
要在父文档中嵌入子文档,请将嵌入式文档的模型指定为所有的实体。嵌入式关系是 MongoDB 原生的关系建模方式,可为经常一起访问的文档提供更好的性能。
何时嵌入式文档
当相关数据通常在其父级的上下文中显示且嵌入数据的大小保持在合理范围内时,请使用嵌入式文档。这包括少量的子值,例如地址、设置或有限的最近项目列表。嵌入式文档时,MongoDB Server 可以通过在单个数据库操作中检索相关数据以及在单个文档中原子更新相关数据来提高性能。
在以下情况下考虑嵌入数据:
您的应用程序通常会同时读取父和子数据。在一个查询中获取所有内容比单独调用更有效。
相关数据的增长没有明确的上限。有界数据不会导致文档无限增长或达到 MongoDB 的 16MB 文档大小限制。
相关数据不会频繁变化,也不会独立于父级变化。这样可以减少重写父级文档的开销。
注意
嵌套层次结构
当您更新嵌套所有实体的任何属性时,EF Core 提供商会重写整个根文档。对于嵌套层次为三个或更多个级别的实体层次结构,此写入成本会复合,因为树中的深层次的小改变会触发完整的父文档重写。如果您的模型使用深层次嵌套,并且您的应用程序频繁执行写入,请考虑将层次结构平展或将内部所有实体替换为手动引用。
要了解更多信息,请参阅 MongoDB Server 文档中的嵌入数据。
如何嵌入式文档
您可以通过以下方式指定所有的实体:
[所有] 属性:将
[Owned]数据注释属性应用于子文档的模型类。这种方法是声明性的和简洁的,但灵活性也较差,因为它全局应用于所有所有类型的用法。流畅 API:在
DbContext配置中调用OwnsOne()或OwnsMany()方法。这种方法提供了更多的控制和灵活性,允许您为特定实体配置不同的关系,而无需修改模型类。
以下部分提供了这些方法的示例,用于建模单个和多个所有实体。
一个拥有实体
要在父文档中嵌入单个文档,请在 DbContext 配置中调用 OwnsOne() 方法,或将 [Owned] 属性应用于嵌入类。
以下示例显示一个包含嵌入式 Address 文档的 Customer 实体。选择 [Owned] Attribute 或 Fluent API 标签页查看对应的语法:
[] public class Address { public string Street { get; set; } = null!; public string City { get; set; } = null!; public string Country { get; set; } = null!; } public class Customer { public ObjectId Id { get; set; } public string Name { get; set; } = null!; public Address Address { get; set; } = null!; }
public class CustomerDbContext : DbContext { public DbSet<Customer> Customers { get; set; } = null!; public CustomerDbContext(DbContextOptions options) : base(options) { } protected override void OnModelCreating(ModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder); modelBuilder.Entity<Customer>(c => { c.OwnsOne(c => c.Address); c.ToCollection("customers"); }); } }
保存 Customer 实体时,MongoDB 会将 Address 作为嵌入式文档存储在 Customer 文档中。以下 JSON 显示了该关系在 MongoDB 中的显示方式:
{ "_id": ObjectId("..."), "Name": "John Doe", "Address": { "Street": "123 Main St", "City": "New York", "Country": "USA" } }
多个所有实体
要在父文档中嵌入多个文档,请调用 OwnsMany() 方法或将 [Owned] 属性应用于嵌入类并定义集合属性。
以下示例显示一个包含多个嵌入式 Order 文档的 Customer 实体。选择 [Owned] Attribute 或 Fluent API 标签页查看对应的语法:
[] public class Order { public string Product { get; set; } = null!; public int Quantity { get; set; } } public class CustomerWithOrders { public ObjectId Id { get; set; } public string Name { get; set; } = null!; public List<Order> Orders { get; set; } = new(); }
public class OrderDbContext : DbContext { public DbSet<CustomerWithOrders> Customers { get; set; } = null!; public OrderDbContext(DbContextOptions options) : base(options) { } protected override void OnModelCreating(ModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder); modelBuilder.Entity<CustomerWithOrders>(c => { c.OwnsMany(c => c.Orders); c.ToCollection("customers"); }); } }
保存 CustomerWithOrders 实体时,MongoDB 会将 Orders 作为嵌入式文档数组存储,如以下 JSON 所示:
{ "_id": ObjectId("..."), "Name": "Jane Smith", "Orders": [ { "Product": "Laptop", "Quantity": 1 }, { "Product": "Mouse", "Quantity": 2 } ] }
手动引用
要建模未嵌入的实体之间的关系,可以通过将一个文档的 ID 保存在另一个文档的字段中来手动存储引用。此方法要求您在应用程序代码中管理关系。
何时使用引用
当您经常独立于其父级查询相关数据、子集可以变得非常大或相关数据经常变更时,请使用引用。
在以下情况下考虑使用引用:
相关数据在没有明确上限的情况下增长。无界数组会导致性能问题,并可能达到 MongoDB 的 16MB 文档大小限制。
您需要独立查询相关实体。您可以直接查询和索引子集合,而无需扫描父文档。
相关数据经常变化,并且独立于父级。对嵌入数据的频繁更新需要每次重写整个父级文档,这是低效的。
应用程序通常不会同时读取父数据和子数据。通过使用引用,您可以避免获取不需要的子数据。
关系是多对多或其他复杂关系。
复制数据会产生过多的更新开销。
要了解更多信息,请参阅 MongoDB Server 文档中的 参考数据。
如何使用引用
以下示例展示了如何存储对相关实体的引用:
public class Author { public ObjectId Id { get; set; } public string Name { get; set; } = null!; } public class Book { public ObjectId Id { get; set; } public string Title { get; set; } = null!; // Store reference to Author by storing the Author's Id public ObjectId AuthorId { get; set; } }
要检索相关实体,必须单独查询引用集合,如以下示例所示:
// Query a book and its author var book = db.Books.FirstOrDefault(b => b.Title == "My Book"); var author = db.Authors.FirstOrDefault(a => a.Id == book!.AuthorId);
对于大型一对多集合使用子集模式
如果父文档包含大型数组,但您的应用程序通常仅使用最新或最常访问的项目,请考虑子集模式。
在子集模式中,您将最常访问的项保留在主文档中,并将其余项移动到单独的集合中。这种方法可以减少文档大小,并将更多工作集保留在 RAM 中,这可以提高查询性能。
当您想获取嵌入的性能优势,但完整的嵌入历史会导致文档过大或加载成本过高时,此模式很有用。
更多信息
要了解 MongoDB 中模式设计的最佳实践,请参阅 MongoDB Server 文档中的MongoDB 中数据建模的最佳实践和设计模式。
要学习有关MongoDB中数据建模的更多信息,请参阅MongoDB Server文档中的数据建模。
要了解有关 Entity Framework Core 中所有实体类型的更多信息,请参阅 Microsoft Entity Framework Core 文档中的所有实体类型。
API 文档
要进一步了解本指南所讨论的方法和类型,请参阅以下 Microsoft API 文档: