Увидев пост о LINQ на PHP, я решил незамедлительно поделиться своими наработками в этой области.
Моей реализации далеко до полноценного LINQ, но в ней присутсвует наиболее заметная черта технологии — отсутвие инородной строки запроса.
Зачем?
Моя деятельность, как рабочая так и не очень, связана с БД, которая имеет EAV модель хранения данных. Это значит, что при увеличении количества сущностей, количество таблиц не увеличивается. Вся информация хранится всего в двух таблицах.
Таблицы с данными в 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