Отношение многие ко многим 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}"
}
Это загрузит контакт, а затем загрузит данные реляционных документов.
Я не тестировал этот точный пример, но он основан на рабочем примере, который я реализовал в своем собственном проекте. Возможно, вам придется сделать некоторые настройки.
Вам, конечно, нужно настроить его так, чтобы он включал много документов, но он должен дать вам базовое представление о том, как использовать отношения.
Вы также должны оформить документацию для отношений документа.
Надеюсь, это поможет.