Задача хранения в базах данных схемы типа Объект — Множество Атрибутов — Значения атрибутов давно стала «классической».
В рамках реляционных СУБД, простейшее решение выглядит как-то так (anti-pattern detected!):
public class Product
{
public int Id { get; set; }
public List<PropertyValue> PropertyValues { get; set; }
public string ProductTitle { get; set; }
public decimal ProductPrice { get; set; }
}
public class Property
{
public int Id { get; set; }
public string Title { get; set; }
}
public class PropertyValue
{
public int Id { get; set; }
public Property Property { get; set; }
public string Value { get; set; }
}
И это не учитывая потенциальной типизации значений свойств (некоторые могут быть числовыми, другие — датой/временем и т.п.) и полагаясь на ORM для генерации таблицы связи много-ко-многим (Product/PropertyValue).
Помимо сложности самой модели, построение SQL-запросов к ней также становится непростой задачей. Например, задача поиска Продукта по двум свойствам (например, Высота=10 && Мощность=20) может вылиться в SQL-запрос вроде такого:
SELECT * FROM Products WHERE Products.Id IN (
SELECT distinct Products.Id
FROM Products
JOIN ProductPropertyValues ON ProductPropertyValues.ProductId = Products.Id
JOIN PropertyValues ON PropertyValues.Id = ProductPropertyValues.PropertyValueId
JOIN Properties ON Properties.Id = ProductPropertyValues.PropertyId
WHERE Properties.Title = 'Height'
AND PropertyValues.Value = "10"
INTERSECT
SELECT distinct Products.Id
FROM Products
JOIN ProductPropertyValues ON ProductPropertyValues.ProductId = Products.Id
JOIN PropertyValues ON PropertyValues.Id = ProductPropertyValues.PropertyValueId
JOIN Properties ON Properties.Id = ProductPropertyValues.PropertyId
WHERE Properties.Title = 'Torque'
AND PropertyValues.Value = "20"
)
Можно представить, насколько непросто будет и динамическое построение такого запроса.
Не зря подобное решение очень часто относят к анти-паттернам в реляционных БД, заранее предупреждая, что высокой эффективности запросов на больших объемах данных добиться будет очень непросто.
В рамках научного эксперимента по внедрению-RavenDb-везде-где-только-можно, появилось желание посмотреть, как аналогичная задача решается в NoSQL-базах.
«Схема» БД:
public class Product
{
public string Id { get; set; }
public string ProductTitle { get; set; }
public decimal ProductPrice { get; set; }
public Dictionary<string, object> Attributes { get; set; }
}
Всё просто и очевидно. Запрос к БД (аналогично, поиск по значению двух свойств):
var products = DocumentSession.Query<EavProduct>()
.Where(x => (int)x.Attributes["Height"] == 10 &&
(int)x.Attributes["Torque"] >= 20)
.ToList();
Стоит отметить, что проблем с типизацией свойств здесь абсолютно не возникает: операции «больше»/»меньше» будут отлично работать как для числовых и строковых значений, так и для дат.
В случае, если у некоторых свойств возможно несколько значений, можно слегка поменять схему для атрибутов:
public class Product
{
public string Id { get; set; }
public string ProductTitle { get; set; }
public decimal ProductPrice { get; set; }
public List<KeyValuePair<string, object>> Attributes { get; set; }
}
Запрос в этом случае станет чуть сложнее, но нисколько не потеряет в своей читаемости:
var products = DocumentSession.Query<Product>()
.Where(x =>
x.Attributes.Any(z => z.Key == "Height" && (int)z.Value == 20) &&
x.Attributes.Any(z => z.Key == "Torque" && (int)z.Value >= 10)
)
.ToList();
Конечно, сравнивать одну из сильнейших сторон NoSQL с признанно слабым местом реляционных баз — это не совсем честно. Но удобство и элегантность решения на базе RavenDB способно поистине удивить.
P.S. Было бы очень интересно сравнить и производительность этих двух решений (и при случае я обязательно постараюсь это сделать), но что-то подсказывает, что и в этом аспекте реляционные хранилища проиграют.