如何在 RavenDB 等面向文档的数据库系统中对分层和关系数据进行建模?

Posted

技术标签:

【中文标题】如何在 RavenDB 等面向文档的数据库系统中对分层和关系数据进行建模?【英文标题】:How would I model data that is heirarchal and relational in a document-oriented database system like RavenDB? 【发布时间】:2011-09-11 06:12:14 【问题描述】:

面向文档的数据库(尤其是 RavenDB)真的很吸引我,我想尝试一下它们。然而,作为一个非常习惯关系映射的人,我一直在思考如何在文档数据库中正确建模数据。

假设我的 C# 应用程序中有一个包含以下实体的 CRM(省略了不需要的属性):

public class Company

    public int Id  get; set; 
    public IList<Contact> Contacts  get; set; 
    public IList<Task> Tasks  get; set; 


public class Contact

    public int Id  get; set; 
    public Company Company  get; set; 
    public IList<Task> Tasks  get; set; 


public class Task

    public int Id  get; set; 
    public Company Company  get; set; 
    public Contact Contact  get; set; 

我正在考虑将这一切都放在Company 文档中,因为联系人和任务在公司之外没有目的,并且大多数时候查询任务或联系人也会显示有关关联公司的信息.

问题与Task 实体有关。假设业务要求任务始终与公司相关联,但也可选择与任务相关联。

在关系模型中,这很容易,因为您只有一个Tasks 表,并且Company.Tasks 与公司的所有任务相关,而Contact.Tasks 仅显示特定任务的任务。

为了在文档数据库中建模,我想到了以下三个想法:

    将任务建模为单独的文档。这似乎是一种反文档数据库,因为大多数时候您查看公司或联系人,您会希望查看任务列表,因此必须对文档执行很多操作。

    将与联系人无关的任务保留在Company.Tasks 列表中,并将与联系人关联的任务放入每个联系人的列表中。不幸的是,这意味着如果您想查看公司的所有任务(可能很多),您必须将公司的所有任务与每个联系人的所有任务结合起来。当您想解除任务与联系人的关联时,我还认为这很复杂,因为您必须将其从联系人移至公司

    将所有任务保留在Company.Tasks 列表中,并且每个联系人都有一个与其关联的任务的 id 值列表。除了必须手动获取 id 值并且必须为联系人创建 Task 实体的子列表之外,这似乎是一个不错的方法。

在面向文档的数据库中对这些数据建模的推荐方法是什么?

【问题讨论】:

【参考方案1】:

使用非规范化引用:

http://ravendb.net/faq/denormalized-references

本质上你有一个 DenormalizedReference 类:

public class DenormalizedReference<T> where T : INamedDocument

    public string Id  get; set; 
    public string Name  get; set; 

    public static implicit operator DenormalizedReference<T> (T doc)
    
        return new DenormalizedReference<T>
        
            Id = doc.Id,
            Name = doc.Name
        
    

你的文档看起来像 - 我已经实现了 INamedDocument 接口 - 这可以是你需要的任何东西:

public class Company : INamedDocument

    public string Nameget;set;
    public int Id  get; set; 
    public IList<DenormalizedReference<Contact>> Contacts  get; set; 
    public IList<DenormalizedReference<Task>> Tasks  get; set; 


public class Contact : INamedDocument

    public string Nameget;set;
    public int Id  get; set; 
    public DenormalizedReference<Company> Company  get; set; 
    public IList<DenormalizedReference<Task>> Tasks  get; set; 


public class Task : INamedDocument

    public string Nameget;set;
    public int Id  get; set; 
    public DenormalizedReference<Company> Company  get; set; 
    public DenormalizedReference<Contact> Contact  get; set; 

现在保存任务的工作方式与以前完全一样:

var task = new Task
    Company = myCompany,
    Contact = myContact
;

但是,将所有这些拉回来意味着您只会获得子对象的非规范化引用。为了水合这些,我使用了一个索引:

public class Tasks_Hydrated : AbstractIndexCreationTask<Task>

    public Tasks_Hydrated()
    
        Map = docs => from doc in docs
                      select new
                                 
                                     doc.Name
                                 ;

        TransformResults = (db, docs) => from doc in docs
                                         let Company = db.Load<Company>(doc.Company.Id)
                                         let Contact = db.Load<Contact>(doc.Contact.Id)
                                         select new
                                                    
                                                        Contact,
                                                        Company,
                                                        doc.Id,
                                                        doc.Name
                                                    ;
    

使用您的索引来检索水合任务是:

var tasks = from c in _session.Query<Projections.Task, Tasks_Hydrated>()
                    where c.Name == "taskmaster"
                    select c;

我觉得很干净:)

作为设计对话 - 一般规则是,如果您曾经 需要加载子文档单独 - 不是父文档的一部分。无论是用于编辑还是查看 - 您都应该使用它自己的 Id 作为它自己的文档对其进行建模。使用上面的方法使这变得非常简单。

【讨论】:

好吧,所以我想我在非规范化方面做得太过分了,但是拆分这些是否会放弃基于文档的数据库的优势,因为我必须经常在文档之间进行连接? 你不会因为这些索引快如闪电,并且 db.Load 发生在服务器上,因此成本最低。您应该考虑您的事务边界在哪里,并仅在您真正需要时使用此方法 - 但这确实意味着您可以真正享受两全其美的好处。我忘了提到更新非规范化引用(例如,如果名称更改)您需要运行补丁来更新引用。这同样非常简单 - 但您需要管理一个过程。我发现这是一个很小的成本,而无模式数据库的好处大大超过了它:) 这是有道理的:)。我真的很喜欢文档(更重要的是无模式)数据库的想法。谢谢!【参考方案2】:

我也是记录 dbs 的新手......所以有点盐......

作为一个对比示例...如果您在 Twitter 上并且您有一个您关注的人的列表,其中包含他们的推文列表...您不会为了阅读而将他们的推文移动到您的推特帐户中他们,如果你重新发推文,你只会得到一份副本,而不是原件。

因此,同样地,我的观点是,如果任务属于一家公司,那么它们将留在公司内。公司是任务的聚合根。联系人只能保存引用(id)或任务的副本,不能直接修改它们。如果您的联系人持有任务的“副本”,那很好,但为了修改任务(例如,将其标记为完成),您将通过其聚合根(公司)修改任务。由于副本可能很快就会过时,因此您似乎只希望在内存中存在副本,并且在保存联系人时,您只需要保存对任务的引用。

【讨论】:

以上是关于如何在 RavenDB 等面向文档的数据库系统中对分层和关系数据进行建模?的主要内容,如果未能解决你的问题,请参考以下文章

为同一个 RavenDB 应用程序创建两个文档存储

#yyds干货盘点# RavenDB 文档建模--琐碎的注意事项--处理无限增长的文档

RavenDB 全文搜索

.NET 文档数据库 RavenDB 4.0 发布

面向对象数据库和文档数据库有啥区别?

如何创建返回字符串列表的 RavenDB 索引?