Пишем код

Заметки о .net разработке

NoSQL против реляционных СУБД в задаче Entity-Attribute-Value

without comments

Задача хранения в базах данных схемы типа Объект — Множество Атрибутов — Значения атрибутов давно стала «классической».
В рамках реляционных СУБД, простейшее решение выглядит как-то так (anti-pattern detected!):

<br />
    public class Product<br />
    {<br />
        public int Id { get; set; }<br />
        public List<PropertyValue> PropertyValues { get; set; }<br />
        public string ProductTitle { get; set; }<br />
        public decimal ProductPrice { get; set; }<br />
    }</p>
<p>    public class Property<br />
    {<br />
        public int Id { get; set; }<br />
        public string Title { get; set; }<br />
    }</p>
<p>    public class PropertyValue<br />
    {<br />
        public int Id { get; set; }<br />
        public Property Property { get; set; }<br />
        public string Value { get; set; }<br />
    }<br />

И это не учитывая потенциальной типизации значений свойств (некоторые могут быть числовыми, другие — датой/временем и т.п.) и полагаясь на ORM для генерации таблицы связи много-ко-многим (Product/PropertyValue).

Помимо сложности самой модели, построение SQL-запросов к ней также становится непростой задачей. Например, задача поиска Продукта по двум свойствам (например, Высота=10 && Мощность=20) может вылиться в SQL-запрос вроде такого:
<br />
	SELECT * FROM Products WHERE Products.Id IN (<br />
		SELECT distinct Products.Id<br />
			FROM Products<br />
			JOIN ProductPropertyValues ON ProductPropertyValues.ProductId = Products.Id<br />
			JOIN PropertyValues ON PropertyValues.Id = ProductPropertyValues.PropertyValueId<br />
			JOIN Properties ON Properties.Id = ProductPropertyValues.PropertyId</p>
<p>			WHERE Properties.Title = 'Height'<br />
			AND PropertyValues.Value = "10"</p>
<p>		INTERSECT</p>
<p>		SELECT distinct Products.Id<br />
			FROM Products<br />
			JOIN ProductPropertyValues ON ProductPropertyValues.ProductId = Products.Id<br />
			JOIN PropertyValues ON PropertyValues.Id = ProductPropertyValues.PropertyValueId<br />
			JOIN Properties ON Properties.Id = ProductPropertyValues.PropertyId</p>
<p>			WHERE Properties.Title = 'Torque'<br />
			AND PropertyValues.Value = "20"<br />
	)</p>
<p>

Можно представить, насколько непросто будет и динамическое построение такого запроса.
Не зря подобное решение очень часто относят к анти-паттернам в реляционных БД, заранее предупреждая, что высокой эффективности запросов на больших объемах данных добиться будет очень непросто.

В рамках научного эксперимента по внедрению-RavenDb-везде-где-только-можно, появилось желание посмотреть, как аналогичная задача решается в NoSQL-базах.
«Схема» БД:

<br />
    public class Product<br />
    {<br />
        public string Id { get; set; }<br />
        public string ProductTitle { get; set; }<br />
        public decimal ProductPrice { get; set; }<br />
        public Dictionary<string, object> Attributes { get; set; }<br />
    }<br />

Всё просто и очевидно. Запрос к БД (аналогично, поиск по значению двух свойств):
<br />
var products = DocumentSession.Query<EavProduct>()<br />
                .Where(x => (int)x.Attributes["Height"] == 10 &&<br />
                            (int)x.Attributes["Torque"] >= 20)<br />
                .ToList();<br />

Стоит отметить, что проблем с типизацией свойств здесь абсолютно не возникает: операции «больше»/»меньше» будут отлично работать как для числовых и строковых значений, так и для дат.

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

<br />
    public class Product<br />
    {<br />
        public string Id { get; set; }<br />
        public string ProductTitle { get; set; }<br />
        public decimal ProductPrice { get; set; }<br />
        public List<KeyValuePair<string, object>> Attributes { get; set; }<br />
    }<br />

Запрос в этом случае станет чуть сложнее, но нисколько не потеряет в своей читаемости:
<br />
var products = DocumentSession.Query<Product>()<br />
                .Where(x =><br />
                    x.Attributes.Any(z => z.Key == "Height" && (int)z.Value == 20) &&<br />
                    x.Attributes.Any(z => z.Key == "Torque" && (int)z.Value >= 10)<br />
                    )<br />
                .ToList();<br />

Конечно, сравнивать одну из сильнейших сторон NoSQL с признанно слабым местом реляционных баз — это не совсем честно. Но удобство и элегантность решения на базе RavenDB способно поистине удивить.

P.S. Было бы очень интересно сравнить и производительность этих двух решений (и при случае я обязательно постараюсь это сделать), но что-то подсказывает, что и в этом аспекте реляционные хранилища проиграют.

Опубликовать в Facebook
Опубликовать в Google Plus

Written by Shaddix

Август 6th, 2014 at 3:06 пп

Posted in .net,EF,ravendb

Leave a Reply