Задача хранения в базах данных схемы типа Объект — Множество Атрибутов — Значения атрибутов давно стала «классической».
В рамках реляционных СУБД, простейшее решение выглядит как-то так (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. Было бы очень интересно сравнить и производительность этих двух решений (и при случае я обязательно постараюсь это сделать), но что-то подсказывает, что и в этом аспекте реляционные хранилища проиграют.