如何处理在像 Mongo 这样的文档数据库中引用单独存储的对象

本文关键字:文档数据库 引用 对象 存储 单独 Mongo 何处理 处理 | 更新日期: 2023-09-27 18:32:52

这个问题在Entity Framework或NHibernate等ORM中很容易解决,但我在MongoDB的c#驱动程序中没有看到任何现成的解决方案。假设我有引用 B 类型对象的 A 类型的对象的集合,我需要将其存储在单独的集合中,以便一旦特定对象 B 发生更改,所有引用它的 A 都需要知道该更改。换句话说,我需要规范化这个对象关系。同时,我需要 B 在类中由 A 引用,而不是由 Id 引用,而是通过类型引用引用,如下所示:

public class A
{
   public B RefB { get; set; }
}

我是否必须自己处理所有这些引用一致性?如果是这样,哪种方法最好使用?我是否必须在类中同时保留 B 的 Id 和 B 引用,并以某种方式处理同步它们的值,如下所示:

public class A
{
    // Need to implement reference consistency as well
    public int RefBId { get; set; }
    private B _refB;
    [BsonIgnore]
    public B RefB
    {
        get { return _refB; }
        set { _refB = value; RefBId = _refB.Id }
    }
}
我知道

有人可能会说关系数据库最符合这种情况,我知道,但我真的必须像 MongoDB 这样的文档 Db,它解决了很多问题,在大多数情况下,我需要为我的项目存储非规范化的对象,但有时我们可能需要在单个存储中混合设计。

如何处理在像 Mongo 这样的文档数据库中引用单独存储的对象

这主要是一个架构问题,它可能有点取决于个人品味。我将尝试检查利弊(实际上只有缺点,这是相当固执己见的(:

在数据库级别,MongoDB没有提供任何工具来强制执行引用完整性,所以是的,你必须自己做。我建议您使用如下所示的数据库对象:

public class DBObject 
{
    public ObjectId Id {get;set;}
}
public class Department : DBObject 
{
  // ...
}
public class EmployeeDB : DBObject
{
    public ObjectId DepartmentId {get;set;}
}

无论如何,我建议在数据库级别使用这样的普通 DTO。如果你想要额外的糖,把它放在一个单独的层里,即使这意味着一些复制。数据库对象中的逻辑需要很好地理解驱动程序冻结对象的方式,并且可能需要依赖实现详细信息。

现在,这是您是否要使用更"智能"对象的偏好问题。事实上,许多人喜欢使用强类型的自动激活访问器,例如

public class Employee
{
    public Department 
    { get { return /* the department object, magically, from the DB */ } }
}

这种模式带来了许多挑战:

  • 它需要Employee类(模型类(能够冻结数据库中的对象。这很棘手,因为它需要注入数据库,或者您需要一个静态对象来访问数据库,这也可能很棘手。
  • 访问Department看起来完全便宜,但实际上,它会触发数据库操作,它可能很慢,可能会失败。这对呼叫者完全隐藏。
  • 在1:n的关系中,事情变得更加复杂。例如,Department是否还会公开Employees列表?如果是这样,这真的是一个列表吗(即一旦你开始阅读第一个,所有员工都必须反序列化?还是懒MongoCursor
  • 更糟糕的是,通常不清楚应该使用哪种缓存。假设你得到myDepartment.Employee[0].Department.Name.显然,这段代码并不聪明,但想象一下有一个包含一些专用方法的调用堆栈。他们可能会像这样调用代码,即使它更隐藏。现在,一个朴素的实现实际上会再次反序列化 ref'd Department。这太丑了。另一方面,主动缓存是危险的,因为您可能实际上想要重新获取对象。
  • 最糟糕的是:更新。到目前为止,这些挑战在很大程度上是只读的。现在让我们说我打电话给employeeJohn.Department.Name = 'PixelPushers'employeeJohn.Save().这是否更新了部门?如果是这样,对 john 的更改是先序列化,还是在对依赖对象的更改之后序列化?版本控制和锁定呢?
  • 许多语义很难实现:employeJohn.Department.Employees.Clear()可能很棘手。

许多ORM使用一组复杂的模式来允许这些操作,因此这些问题并非不可能解决。但是ORM通常在10万到超过100万行代码(!(的范围内,我怀疑你有这样的时间。在RDBMS中,需要激活相关对象并使用sth。像ORM一样要严重得多,因为您无法在发票中嵌入例如行项目列表,因此每个1:n或m:n关系都必须使用连接来表示。这就是所谓的对象关系不匹配。

据我了解,文档数据库的想法是,您不需要像在RDBMS中那样不自然地分解模型。尽管如此,还是有"对象边界"。如果您将数据模型视为连接节点的网络,那么挑战在于了解您当前正在处理数据的哪一部分。

就个人而言,我宁愿不在此之上放置抽象层,因为该抽象是泄漏的,它向调用者隐藏了真实发生的事情,并且它试图用同一个锤子解决每个问题。

NoSQL的部分想法是,你的查询模式必须与数据模型仔细匹配,因为你不能简单地将JOIN锤子应用于任何可见的表。

所以,我的观点是:坚持薄层,在服务层中执行大部分数据库操作。移动 DTO,而不是设计一个复杂的域模型,一旦您需要添加锁定、MVCC、级联更新等,就会分解。

在文档数据库中,当您执行类似于第一个示例的操作时:

public class A
{
   public B RefB { get; set; }
}

您将B的值完全嵌入到 RefB 属性中。 换句话说,您的文档如下所示:

[a/1]
{
    AProp: "foo",
    RefB: {
        BProp: "bar"
    }
}

它有助于从领域驱动设计 (DDD( 的角度看待事物。 这种嵌入模式通常发生在B是"值对象"或"非聚合实体"(使用 DDD 术语(时。

如果要存储某个其他聚合实体的时间点快照,也会发生这种情况。 在这种情况下,如果 B 的值发生更改,则不希望更新这些值,否则它将不再表示该时间点。

另一种模式是将AB视为单独的聚合。 如果一个需要引用另一个,则仅通过引用其 ID 来指定。

public class A
{
   public string BId { get; set; }
}

然后,您的文档将被存储,例如:

[a/1]
{
    AProp: "foo",
    BId: "b/2"
}
[b/2]
{
    BProp: "bar",
}

注意:我相信MongoDB,你会使用ObjectId类型。 在RavenDB中,您通常会使用string,但是可以通过一些小的调整来int。 其他文档数据库可能允许其他类型的文档。

在文档数据库中效果不佳的部分是您在第二个示例中展示的方式A保留对B的引用而不将其保留为文档的一部分。 这种模式可能适用于实体框架或NHibernate等ORM,但它往往通过虚拟属性和代理类实现。 这些在文档数据库环境中不能很好地站稳脚跟。

因此,如果它们是单独的文档,而不是加载A并使用a.RefB到达B,您只需单独加载AB。 例如,您可以加载A ,然后使用BId来确定如何加载B

当然,问题仍然归结为是嵌入还是链接。 这是你必须弄清楚的事情,因为它通常可以以任何方式完成。 通常,对于特定的域问题,一种方法比另一种方法效果更好。 但你通常不会两者兼而有之。

基于与关系数据库完全不同的体系结构概念的文档数据库。NoSQL数据库的主要原则是聚合而不是关系。因此,您不应该期望在您所描述的此类数据库中进行规范化。

您的问题应仅手动跟踪。NoSQL中没有引用完整性这样的东西。