...

понедельник, 20 января 2014 г.

Подобие 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"




В качестве условий можно использовать строки '=', '!=', '>=', '', '

Метод 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 довольно сложные запросы, гибким его можно назвать только с натяжкой. Поэтому сейчас формируется концепция более гибкого генератора, который позволит ещё больше сократить количество символов при задании условия запроса, но это уже совсем другая история.

This entry passed through the Full-Text RSS service — if this is your content and you're reading it on someone else's site, please read the FAQ at http://ift.tt/jcXqJW.


Комментариев нет:

Отправить комментарий