多对多关系RavenDb:文档结构和索引

Many-to-many relationship RavenDb: Document structure and index

如何为以下关系模式构建 NoSQL 模型和索引(最好是 RavenDb v4)?

Document type Contact, where each record can have multiple additional properties (type of the property is defined in CustomField and the value in ContactCustomField)

考虑需要 filter/sort 在一个查询中突出显示的字段(联系人中的所有字段加上自定义字段)。


我认为可能的选择:

选项 #1

自然地,我会想象以下持久模型:

public class Contact
{
    public string Id      { get; set; }
    public string Name    { get; set; }
    public string Address { get; set; }
    public string Phone   { get; set; }
    // Where the key is CustomField.Id and the value is ContactCustomField.Value
    public Dictionary<string, string> CustomValues { get; set; }
}

public class CustomField
{
    public string Id          { get; set; }
    public string Code        { get; set; }
    public string DataType    { get; set; }
    public string Description { get; set; }
}

但是,为如下查询构建索引(抱歉混合语法)让我感到困惑:

SELECT Name, Address, Phone, CustomValues
FROM Contact
WHERE Name LIKE '*John*' AND CustomValues.Any(v => v.Key == "11" && v.Value == "student")

选项 #2

另一种方法是保持标准化结构(如上图所示)。然后它会起作用 - 我只需要在 Contact.

的查询中包含 ContactCustomField

缺点是没有利用 NoSQL 的好处。

更新答案(2018 年 6 月 29 日)

成功的关键在于 Raven 的一项被低估的功能 - Indexes with Dynamic Fields. It allows to keep logical data structure and avoids creating a fanout index

使用方法是像上面选项 #1 中描述的那样构建集合:

public class Contact
{
    public string Id      { get; set; }
    public string Name    { get; set; }
    public string Address { get; set; }
    public string Phone   { get; set; }
    public Dictionary<string, object> CustomFields { get; set; }
}

public class CustomField
{
    public string Id          { get; set; }
    public string Code        { get; set; }
    public string DataType    { get; set; }
    public string Description { get; set; }
}

其中 Contact.CustomFields.Key 是对 CustonField.Id 的引用,Contact.CustomFields.Value 存储该自定义字段的值。

为了在自定义字段上filter/search,我们需要以下索引:

public class MyIndex : AbstractIndexCreationTask<Contact>
{
    public MyIndex()
    {
        Map = contacts =>
            from e in contacts
            select new
            {
                _ = e.CustomFields.Select( x => CreateField ($"{nameof(Contact.CustomFields)}_{x.Key}", x.Value))
            };
    }
} 

该索引将涵盖字典的所有键值对,因为它们是 Contact.

的普通属性

陷阱

如果您在 C# 中使用常用的查询对象(IRavenQueryable 类型)而不是 RQLDocumentQuery 编写查询,则会遇到很大的问题。这是我们命名动态字段的方式 - 它是特定格式的复合名称:dictionary_name + underscore + key_name。它允许我们构建像

这样的查询
var q = s.Query<Person, MyIndex>()
                .Where(p => p.CustomFields["Age"].Equals(4));

在幕后转换为 RQL:

from index 'MyIndex' where CustomFields_Age = $p1

它没有记录,here 是我与 Oren Eini(又名 Ayende Rahien)的讨论,您可以在其中了解有关该主题的更多信息。

P.S。我的一般建议是通过 DocumentQuery 而不是通常的 Query (link) 与 Raven 交互,因为 LINQ 集成仍然很薄弱,开发人员可能会不断地发现错误。


初步回答(2018 年 6 月 9 日)

由于 Oren Eini(又名 Ayende Rahien)suggested,方法是选项 #2 - 在查询中包含一个单独的 ContactCustomField 集合。

所以尽管使用了 NoSQL 数据库,关系方法是唯一的方法。

为此,您可能希望使用 Map-Reduced 索引。

地图:

docs.Contacts.SelectMany(doc => (doc, next) => new{
// Contact Fields
doc.Id,
doc.Name,
doc.Address,
doc.Phone,
doc.CustomFieldLoaded = LoadDocument<string>(doc.CustomValueField, "CustomFieldLoaded"),
doc.CustomValues
});

减少:

from result in results
group result by {result.Id, result.Name, result.Address, result.Phone, result.CustomValues, result.CustomFieldLoaded} into g
select new{
g.Key.Id,
g.Key.Name,
g.Key.Address,
g.Key.Phone,
g.Key.CustomFieldLoaded = new {},
g.Key.CustomValues = g.CustomValues.Select(c=> g.Key.CustomFieldLoaded[g.Key.CustomValues.IndexOf(c)])
}

您的文档将如下所示:

{
"Name": "John Doe",
"Address": "1234 Elm St",
"Phone": "000-000-0000",
CustomValues: "{COLLECTION}/{DOCUMENTID}"
}

这将加载联系人,然后加载关系文档的数据。

我没有测试过这个确切的示例,但它基于我在自己的项目中实现的一个工作示例。您可能需要做一些调整。

您当然需要调整它以包含许多文档,但它应该让您对如何使用关系有一个基本的了解。

您还应该查看 document relationships 的文档。

希望对您有所帮助。