面向文档的数据库(尤其是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
个实体的子列表。
在面向文档的数据库中建模此数据的推荐方法是什么?
答案 0 :(得分:10)
使用非规范化引用:
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 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来建模,因为它是自己的文档。使用上面的方法可以很简单。
答案 1 :(得分:1)
我也是新手来记录dbs ......所以带着一粒盐...
作为一个对比的例子......如果你在Twitter上,并且你有一个你所关注的人的列表,其中包含他们的推文列表......你不会将他们的推文移到你的推特账户中以便阅读他们,如果你重新推文,你只会有一份副本,而不是原件。
所以,同样地,我的观点是,如果任务属于公司,那么他们就会留在公司内部。公司是任务的聚合根。联系人只能保存任务的引用(ids)或副本,不能直接修改它们。如果您的联系人持有任务的“副本”,那没关系,但为了修改任务(例如标记完成),您将通过其聚合根(公司)修改任务。由于副本很快就会过时,看起来你只想在内存中存在副本,而在保存联系人时,你只能保存对任务的引用。