Пользователь
0,1
рейтинг
20 января 2014 в 12:49

Разработка → Подобие LINQ на PHP для EAV модели хранения данных

Увидев пост о LINQ на PHP, я решил незамедлительно поделиться своими наработками в этой области.
Моей реализации далеко до полноценного LINQ, но в ней присутсвует наиболее заметная черта технологии — отсутвие инородной строки запроса.

Зачем?

Моя деятельность, как рабочая так и не очень, связана с БД, которая имеет EAV модель хранения данных. Это значит, что при увеличении количества сущностей, количество таблиц не увеличивается. Вся информация хранится всего в двух таблицах.
image
Таблицы с данными в EAV модели
Естественно, что для того чтобы получить «запись» из такой структуры, необходимо написать запрос совершенно непохожий на аналогичный запрос при обычной структуре БД.
Например:
SELECT field_1, field_2, field_3 FROM object

и в EAV
SELECT f1.value_bigint, f2.value_bigint, f3.value_bigint 
FROM objects ob, attributes_values f1, attributes_values f2, attributes_values f3 
WHERE ob.ID_type="object" 
AND f1.ID_object = ob.ID_object AND f1.ID_attribute = 1
AND f2.ID_object = ob.ID_object AND f2.ID_attribute = 2 
AND f3.ID_object = ob.ID_object AND f3.ID_attribute = 3

Как говорится — почувствуйте задницу разницу.
Ситуация осложняется тем, что многие объекты связаны между собой отношениями, которые аналогично раздувают запрос.

Генератор запросов

В один прекрасный момент мне надоело писать плохочитаемую лапшу, которая содержит 50% — 70% вспомогательного кода. Тогда и появилась идея генерировать запрос автоматически. Так на свет появилася IQB — Irro Query Builder. Его концепция была навеяна тем, как устроено взаимодействие с БД в Drupal.
Вышеописанный запрос в IQB будет выглядеть следующим образом:
$q = new IQB();
$query = $q->from(array(new IQBObject('ob','object'), 
                         new IQBAttr('f1',1,INT), 
                         new IQBAttr('f2',2,INT), 
                         new IQBAttr('f3',3,INT)
                ))
        ->where('f1','with','ob')->where('f2','with','ob')->where('f3','with','ob')
        ->select('f1')->select('f2')->select('f3')
        ->build();

Количество кода не уменьшилось, но читаемость, как мне кажется, повысилась.
В этом запросе использованы все основные методы для генерации запроса.
Метод from() принимает класс или массив классов представляющих собой таблицы БД. Таблиц всего две, так что и классов такое же количество. Конструктор класса таблицы принимает псевдоним таблицы, её условный тип и тип данных, если это таблица атрибута.
Псевдоним таблицы используется во всех остальных методах генератора запросов. Условный тип, для таблицы объектов, является названием сущности, среди которых ведётся поиск, а для таблицы атрибутов, условный тип необходим просто чтобы различать атрибуты одного объекта. Тип данных, говорит из какого поля таблицы брать данные. Это необходимо т.к. атрибут объекта является структурой с 4 полями под данные, из которых используется только одно, и в каком именно поле хранятся данные надо указывать явно.
Метод where() накладывает условия на выборку. Принимает всегда 3 аргумента: псевдоним таблицы, условие, значение. В зависимости от условия, в качестве значения может быть передан псевдоним другой таблицы, значение или массив значенией с которым сравнивается поле таблицы.
Например:
$q->where('attr','with','object');

задаст условие
attr.ID_object = object.ID_object

из такого выражения
$q->where('attr','=','object');

получится похожее, но совсем другое выражение
attr.value_bigint = object.ID_object

а если таблица object не была объявлена во from(), то получится вот это (если ещё тип данных атрибута изменить на string)
attr.value_ntext = "object"

В качестве условий можно использовать строки '=', '!=', '>=', '<=', '>', '<', 'like' и 'with' — принадлежность атрибута конкретному объекту.
Метод select() указывает генератору, значения каких таблиц должны попасть в выборку. Кроме того можно «обернуть» это значение в функцию, передав в метод третьим аргументом строку вроде «SUM($)», и вместо доллара в функцию подставится поле таблицы. Вторым аргументом можно передать псевдоним поля в выборке.
Вместе с методами groupBy() и orderBy() этого хватает для построения среднестатистическиз запросов на чтение.

Однако не всё так просто.
Объекты, как и сущности в обычных БД, могут быть связаны отношениями.
Связь, как это ни странно — тоже объект. С атрибутами. И чтобы получить объект Б, который является дочерним у объекта А, необходимо проделать следующие манипуляции:
$q->from(array( 
        new IQBObject('b','B'),
        new IQBAttr('parent',23,INT),
        new IQBAttr('child',24,INT)
    ))
    ->where('parent','=',123456) // ID_object объекта А
    ->where('child','with','parent')
    ->where('child','=','b')

Многовато для простого «взять Б дочерний у А». Чтобы автоматизировать связывание объектов, в IQB сущетвует метод linked().
Метод принимает ID_object или псевдоним известного объекта, псевдоним дочернего/родительского и «флаг разворота» т.е. указание — искать дочерние объекты или родительские. Таким образом вышеизложенный код можно зписать так:
$q->from(new IQBObject('b','B'))->linked(123456,'b');// по умолчанию ищется дочерний объект.

Можно было бы на этом и закончить, но периодически попадаются задачи, для которых генератор запросов оказывается несколько ограниченным. Например, с некоторых пор начали попадаться объекты, у которых какой-то атрибут может отсутсвовать. Для решения этой проблемы был добавлен метод joinTo() который делает LEFT JOIN таблицы атрибута к таблице объекта.
А для совсем уж экзотических запросов есть rawWhere() и rawSelect() которые позволяют вводить произвольные куски запроса.

Заключение

Я не старался делать библиотеку для всеобщего пользования, поэтому новые возможности вводил только когда в этом появлялась необходимость. В связи с этим ошибки проектиования, допущенные на ранних этапах разработки, обросли парой слоёв костылей, необходимых для совместимости со старым кодом и для поддеражания новых функций.
Несморя на возможность реализовыть с помощью IQB довольно сложные запросы, гибким его можно назвать только с натяжкой. Поэтому сейчас формируется концепция более гибкого генератора, который позволит ещё больше сократить количество символов при задании условия запроса, но это уже совсем другая история.
@MadridianFox
карма
7,0
рейтинг 0,1
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Реклама

Самое читаемое Разработка

Комментарии (8)

  • +1
    У Вас немножечко совсем не то. Это построитель SQL запроса, а не методы для обработки коллекций. К тому же посмотрите,
    как бы просто писался Ваш запрос с использованием нативного LINQ

    var r = objects.
         Where(o => o.ID_type == "object").
         GroupJoin(attributes, o => o.ID_objects, o => o.ID_objects, (o,u) => {
              return u.Select(u => u.value_big_int).toArray();
         });
    

    или PHP (если бы эта библиотека уже умела конвертировать запрос в SQL)

    $r = Linq::from($objects)
         ->where(Lambda::ID_type()->eq("object"))
         ->groupJoin($attributes, 'id_object', 'id_object')
         ->select(Lambda::v(1)->linq()->select('value_bigint')->toArray());
    

    А по делу, посмотрите в направлении существующих ORM, тот же Doctrine. Мне кажется Вы пытаетесь сделать тоже самое.
    • 0
      А я и не говорил, что это методы обработки коллекций. Я с самого начала написал, что здесь от LINQ используется только факт написания запроса на «родном» языке. И далее я говорю именно о генерации строки запроса. Даже само название основного класса говорит об этом — Query Builder.
      И когда я его писал, я не пытался сделать его похожим на LINQ. Генератор запросов нужен чтобы упростить написание запросов.
      что проще:
      $r ->select(Lambda::v(1)->linq()->select('value_bigint')->toArray());
      

      или
      $q->select('attr');
      

      ?
      С другой стороны, у меня написано о применении такого подхода к EAV. Умеет ваш Doctrine работать с EAV? Буду вам очень признателен, если подскажете мне ORM для EAV.
      • 0
        Мапить, чтобы работать в стиле,

        $product->attributes["xxx"] = "yyy";

        Doctrine пока еще не умеет (см. Limitations). Но можно классически задать отношения «class Product» — один-к-многим — «class AttributesValues» — многие-к-одному — «class Attribute» и тогда работать в стиле

        foreach ($product->getAttributeValues() as $value) { if ($value->getAttribute()->getId() == 1) { ... } }

        При этом будет работать и редактирование, и удаление, и добавление новых свойств. Думаю, Doctrine не одна такая ORM, но она одна из популярных.
  • 0
    А не могли бы вы поподробней рассказать почему на проектах выбрали именно EAV модель данных? Несколько раз приходилось сталкиваться с подобным, но осознать плюсов подхода так и не смог.
    • 0
      В данном случае модель была выбрана, когда я ещё не участвовал в проекте.
      Модель EAV имеет один плюс и много минусов, и очень важно сразу понять — так ли необходим этот плюс.
      Достоинства EAV:
      — Управление сущностями используя DML (Data Manipulation Language), а именно — создание и редактирование сущностей без использования операций CREATE TABLE и ALTER TABLE. При грамотном подходе это даёт поразительную гибкость базы данных.
      Недостатки EAV:
      — Относительно низкая скорость работы
      — Каждый запрос несёт за собой большое количество рутинного кода для вытаскивания данных
      — Относительно высокий порог вхождения специалистов
      Таким образом EAV подходит для очень динамичных информационных систем, где скорость изменения структуры данных важнее скорости их получения.
      Из личного опыта могу сказать, что эта модель бессмысленна, когда нет инструментов её обслуживания — редакторов сущностей, унифицированных компонентов информационной системы для отображения объекта/списка объектов, универсальных форм заполнения объектов и т.д. Ведь если для любого изменения приходится лезть в код, то почему не сделать это хотя-бы традиционно?
      • 0
        Спасибо за развернутый ответ. Вопрос в догонку: почему бы не решать вопрос структуры базы ее отсутствием, а именно не использовать доментоориентированные базы?
        • 0
          Несмотря на то, что этот вопрос мне задаёт большинство людей, которые знают чем я занимаюсь, легче найти специалиста который хорошо разбирается в «традиционных» БД, чем человека который имеет опыт работы с документоориентированными базами данных. Ну а конкретно в этом случае, при проектировании ИС люди просто не знали о существовании таких БД.
  • 0
    Добавлю к предыдущему комментарию, что модель EAV очень хорошо использовать когда запросы направлены на получение информации об одном объекте. Например, медицинские карточки. Большая часть запросов идет только про одного человека, но при этом набор анализов и диагнозов у всех людей очень различается. Запросов, касающихся статистики по людям, болезням в общем в таких системах намного меньше, и они естественно будут медленнее. Но зато запросы про одного человека намного быстрее, чем если бы это было реализовано в традиционной нормализованной базе данных

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