如何在RavenDB这样的文档导向型数据库系统中建模既具有层次结构又具有关系的数据?

12

面向文档的数据库(特别是RavenDB)让我非常着迷,我想玩弄一下这些数据库。 然而,作为一个非常习惯于关系映射的人,我正在思考如何在文档数据库中正确地建模数据。

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

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仅显示指定联系人的任务。
为了在文档数据库中建模,我想到了以下三个想法:
  1. 将任务建模为单独的文档。这似乎有点反文档数据库,因为大多数情况下,查看公司或联系人时您都会想要查看任务列表,因此必须经常在文档之间执行连接操作。
  2. 将未与联系人关联的任务保留在Company.Tasks列表中,并将与联系人关联的任务放在每个个人联系人的列表中。不幸的是,这意味着如果要查看公司的所有任务(这可能会很多),则必须将公司的所有任务与每个个人联系人的所有任务相结合。当您想要取消任务与联系人的关联时,我还看到这会很复杂,因为您必须将其从联系人移动到公司。
  3. 将所有任务保留在Company.Tasks列表中,每个联系人都有一个与其相关联的任务id值列表。这似乎是一种不错的方法,但是需要手动获取id值,并且必须为每个联系人制作一个子Task实体列表。
在文档导向的数据库中对此数据进行建模的推荐方法是什么?
2个回答

10

使用非规范化引用:

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

本质上,您有一个非规范化引用类:

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 Name{get;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 Name{get;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 Name{get;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并作为自己的文档。使用上述方法可以使此过程变得非常简单。


好的,我想我把去规范化做得太过了。但是,如果我把它们分开,是否会放弃文档型数据库的优势,因为我必须不断地在文档之间进行连接? - KallDrexx
你不会因为这些索引非常快,而且db.Load<T>发生在服务器上,所以成本很小。你应该考虑你的事务边界在哪里,并且只在真正需要时使用此方法 - 但这意味着你可以真正拥有两个世界的好处。我忘了提到更新反规范化的引用(例如名称更改)需要运行补丁来更新引用。这也非常简单 - 但是你需要管理这个过程。我认为这是一个小成本,远远超过了无模式DB的好处 :) - user156888
很有道理 :). 我真的很喜欢文档(更重要的是无模式)数据库的想法。谢谢! - KallDrexx

1

我对文档数据库也是新手,所以请谨慎参考...

举个相反的例子...如果你在Twitter上有一个关注列表,其中包含他们的推文列表...你不会将他们的推文移动到你的Twitter账户中阅读,如果你转发,你只会有一份副本,而不是原件。

因此,同样地,我的观点是,如果任务属于公司,则应该留在公司内。公司是任务的聚合根。联系人只能持有任务的引用(ID)或副本,并且不能直接修改它们。如果您的联系人持有任务的“副本”,那么没问题,但是为了修改任务(例如标记为完成),您将通过其聚合根(公司)修改任务。由于副本可能很快过时,因此似乎只想让副本存在于内存中,当保存联系人时,您只保存对任务的引用。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接