6

这个问题在 Entity Framework 或 NHibernate 等 ORM 中很容易解决,但我在 MongoDb 的 c# 驱动程序中没有看到任何现成的解决方案。假设我有 A 类型的对象的集合,它引用了 B 类型的对象,我需要将它们存储在单独的集合中,这样一旦特定的对象 B 发生更改,所有引用它的 A 都需要知道更改。换句话说,我需要规范化这个对象关系。同时,我需要在类中由 A 引用 B,而不是通过 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,它解决了很多问题,并且在大多数情况下,我需要为我的项目存储非规范化的对象,但有时我们可能会需要在单个存储内混合设计。

4

3 回答 3

6

这主要是一个架构问题,它可能取决于个人品味。我会尝试检查利弊(实际上只有缺点,这很自以为是):

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

public class DBObject 
{
    public ObjectId Id {get;set;}
}

public class Department : DBObject 
{
  // ...
}

public class EmployeeDB : DBObject
{
    public ObjectId DepartmentId {get;set;}
}

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

现在,您是否想使用更多“智能”对象是一个偏好问题。事实上,很多人喜欢使用强类型的自动激活访问器,例如

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'and employeeJohn.Save()。这是否更新了部门?如果是这样,对 john 的更改是先序列化,还是在对依赖对象的更改之后序列化?版本控制和锁定呢?
  • 许多语义很难实现:employeJohn.Department.Employees.Clear()可能很棘手。

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

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

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

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

所以,我的意见是:坚持薄层,在服务层执行大部分数据库操作。移动 DTO,而不是设计一个复杂的域模型,该模型在您需要添加锁定、mvcc、级联更新等时立即分解。

于 2013-09-30T17:06:02.803 回答
3

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

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 中,您通常会使用 a string,但只需int稍作调整即可使用 a 。其他文档数据库可能允许其他类型。

在文档数据库中无法正常工作的部分是您在第二个示例中显示的如何A保留引用而不B将其保留为文档的一部分。这种模式可以在 Entity Framework 或 NHibernate 等 ORM 中工作,但它往往是通过虚拟属性和代理类来实现的。这些在文档数据库环境中并不适用。

因此,如果它们是单独的文档,而不是加载A和使用a.RefBget to B,您只需单独加载A和使用B。例如,您可能会加载A,并使用BId来确定如何加载B

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

于 2013-09-30T16:55:54.477 回答
1

文档数据库基于与关系数据库完全不同的架构概念。NoSQL 数据库的主要原则是聚合而不是关系。所以你不应该期望在你描述的这样的数据库中进行标准化。

您的问题只能手动跟踪。NoSQL 中没有参照完整性这样的东西。

于 2013-10-06T20:45:26.633 回答