Увидев пост о LINQ на PHP, я решил незамедлительно поделиться своими наработками в этой области.
Моей реализации далеко до полноценного LINQ, но в ней присутсвует наиболее заметная черта технологии — отсутвие инородной строки запроса.
Моя деятельность, как рабочая так и не очень, связана с БД, которая имеет EAV модель хранения данных. Это значит, что при увеличении количества сущностей, количество таблиц не увеличивается. Вся информация хранится всего в двух таблицах.
Таблицы с данными в EAV модели
Естественно, что для того чтобы получить «запись» из такой структуры, необходимо написать запрос совершенно непохожий на аналогичный запрос при обычной структуре БД.
Например:
и в EAV
Как говорится — почувствуйтезадницу разницу.
Ситуация осложняется тем, что многие объекты связаны между собой отношениями, которые аналогично раздувают запрос.
В один прекрасный момент мне надоело писать плохочитаемую лапшу, которая содержит 50% — 70% вспомогательного кода. Тогда и появилась идея генерировать запрос автоматически. Так на свет появилася IQB — Irro Query Builder. Его концепция была навеяна тем, как устроено взаимодействие с БД в Drupal.
Вышеописанный запрос в IQB будет выглядеть следующим образом:
Количество кода не уменьшилось, но читаемость, как мне кажется, повысилась.
В этом запросе использованы все основные методы для генерации запроса.
Метод from() принимает класс или массив классов представляющих собой таблицы БД. Таблиц всего две, так что и классов такое же количество. Конструктор класса таблицы принимает псевдоним таблицы, её условный тип и тип данных, если это таблица атрибута.
Псевдоним таблицы используется во всех остальных методах генератора запросов. Условный тип, для таблицы объектов, является названием сущности, среди которых ведётся поиск, а для таблицы атрибутов, условный тип необходим просто чтобы различать атрибуты одного объекта. Тип данных, говорит из какого поля таблицы брать данные. Это необходимо т.к. атрибут объекта является структурой с 4 полями под данные, из которых используется только одно, и в каком именно поле хранятся данные надо указывать явно.
Метод where() накладывает условия на выборку. Принимает всегда 3 аргумента: псевдоним таблицы, условие, значение. В зависимости от условия, в качестве значения может быть передан псевдоним другой таблицы, значение или массив значенией с которым сравнивается поле таблицы.
Например:
задаст условие
из такого выражения
получится похожее, но совсем другое выражение
а если таблица object не была объявлена во from(), то получится вот это (если ещё тип данных атрибута изменить на string)
В качестве условий можно использовать строки '=', '!=', '>=', '<=', '>', '<', 'like' и 'with' — принадлежность атрибута конкретному объекту.
Метод select() указывает генератору, значения каких таблиц должны попасть в выборку. Кроме того можно «обернуть» это значение в функцию, передав в метод третьим аргументом строку вроде «SUM($)», и вместо доллара в функцию подставится поле таблицы. Вторым аргументом можно передать псевдоним поля в выборке.
Вместе с методами groupBy() и orderBy() этого хватает для построения среднестатистическиз запросов на чтение.
Однако не всё так просто.
Объекты, как и сущности в обычных БД, могут быть связаны отношениями.
Связь, как это ни странно — тоже объект. С атрибутами. И чтобы получить объект Б, который является дочерним у объекта А, необходимо проделать следующие манипуляции:
Многовато для простого «взять Б дочерний у А». Чтобы автоматизировать связывание объектов, в IQB сущетвует метод linked().
Метод принимает ID_object или псевдоним известного объекта, псевдоним дочернего/родительского и «флаг разворота» т.е. указание — искать дочерние объекты или родительские. Таким образом вышеизложенный код можно зписать так:
Можно было бы на этом и закончить, но периодически попадаются задачи, для которых генератор запросов оказывается несколько ограниченным. Например, с некоторых пор начали попадаться объекты, у которых какой-то атрибут может отсутсвовать. Для решения этой проблемы был добавлен метод joinTo() который делает LEFT JOIN таблицы атрибута к таблице объекта.
А для совсем уж экзотических запросов есть rawWhere() и rawSelect() которые позволяют вводить произвольные куски запроса.
Я не старался делать библиотеку для всеобщего пользования, поэтому новые возможности вводил только когда в этом появлялась необходимость. В связи с этим ошибки проектиования, допущенные на ранних этапах разработки, обросли парой слоёв костылей, необходимых для совместимости со старым кодом и для поддеражания новых функций.
Несморя на возможность реализовыть с помощью IQB довольно сложные запросы, гибким его можно назвать только с натяжкой. Поэтому сейчас формируется концепция более гибкого генератора, который позволит ещё больше сократить количество символов при задании условия запроса, но это уже совсем другая история.
Моей реализации далеко до полноценного 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 довольно сложные запросы, гибким его можно назвать только с натяжкой. Поэтому сейчас формируется концепция более гибкого генератора, который позволит ещё больше сократить количество символов при задании условия запроса, но это уже совсем другая история.