Пишем код

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

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

without comments

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

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

Written by Shaddix

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

Posted in .net,EF,ravendb

Leave a Reply