Отношение многие ко многим RavenDb: структура и индекс документа

Как построить модель и индекс NoSQL (предпочтительно для RavenDb v4) для следующей реляционной схемы?

Тип документа Contactгде каждая запись может иметь несколько дополнительных свойств (тип свойства определяется в CustomField и значение в ContactCustomField) введите описание изображения здесь

Учитывая необходимость фильтровать / сортировать выделенные поля в одном запросе (все поля из контакта плюс настраиваемые поля).


Возможные варианты как я вижу:

Опция 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

Другим подходом будет сохранение нормализованной структуры (как показано на рисунке выше). Тогда это будет работать - я просто должен включить ContactCustomField в запросе для Contact,

Недостатком было бы не использование преимуществ NoSQL.

2 ответа

Решение

Обновленный ответ (29 июня 2018 г.)

Ключ к успеху лежит в одной недооцененной возможности Raven - индексах с динамическими полями. Это позволяет сохранить логическую структуру данных и избежать создания индекса разветвления.

Способ использования заключается в создании коллекций, как описано выше в варианте № 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 хранит значение для этого настраиваемого поля.

Для фильтрации / поиска по настраиваемым полям нам нужен следующий индекс:

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# с использованием обычного объекта Query (IRavenQueryable тип), а не RQL или же DocumentQuery, Это так, как мы назвали динамические поля - это составное имя в определенном формате: 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

Это недокументировано, и вот мое обсуждение с Ореном Эйни (он же Айенде Рахин), где вы можете узнать больше на эту тему.

PS Моя общая рекомендация - взаимодействовать с Raven через DocumentQuery а не обычный Query ( ссылка), поскольку интеграция с LINQ все еще довольно слабая, и разработчики могут постоянно сталкиваться с ошибками.


Первоначальный ответ (9 июня 2018 года)

Как было предложено Ореном Эйни (он же Айенде Рахьен), путь - это вариант № 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}"
}

Это загрузит контакт, а затем загрузит данные реляционных документов.

Я не тестировал этот точный пример, но он основан на рабочем примере, который я реализовал в своем собственном проекте. Возможно, вам придется сделать некоторые настройки.

Вам, конечно, нужно настроить его так, чтобы он включал много документов, но он должен дать вам базовое представление о том, как использовать отношения.

Вы также должны оформить документацию для отношений документа.

Надеюсь, это поможет.

Другие вопросы по тегам