- PVSM.RU - https://www.pvsm.ru -

Подобие LINQ на PHP для EAV модели хранения данных

Увидев пост о LINQ на PHP [1], я решил незамедлительно поделиться своими наработками в этой области.
Моей реализации далеко до полноценного 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

Источник [2]


Сайт-источник PVSM.RU: https://www.pvsm.ru

Путь до страницы источника: https://www.pvsm.ru/programmirovanie/53148

Ссылки в тексте:

[1] пост о LINQ на PHP: http://habrahabr.ru/post/209514/

[2] Источник: http://habrahabr.ru/post/209682/