Создание CMS Модель данных (3) Поиск

В предыдущей статье на примере создания объектной модели простого сайта производились одиночные загрузки сущностей из базы данных по их идентификаторам конструкцией Object::Create($id), при этом мы знали, у какой сущности (чаще всего класса), какой идентификатор, так как сами создавали эти сущности и в крайнем случаи могли просто заглянуть в базу данных. На практике загружать сущности по идентификатору проблематично, если нас интересуют сущности, о существовании которых можно только догадываться, то есть, не имея информации об их идентификаторах. Более того существует необходимость загружать несколько сущностей разом, отвечающих некоторым условиям.

В магазине, например, мы не выбираем товар по его серийному номеру или штрих-коду, не зная при этом, что он означает – мы смотрим на свойства товара интересующие нас. На главной странице сайта, опять же для примера, необходимо выводить последние новости, что сводится к выборке из базы данных (объектной модели данных) 10 объектов класса «Новость» с сортировкой по дате их создания. Для осуществления подобных запросов необходим гибкий способ описания условий выборки сущностей – условий поиска с учетом особенностей объектной модели. На основе условия необходимо создавать SQL код для непосредственной выборки из БД идентификаторов сущностей удовлетворяющих условию, имея идентификаторы – имеем сущности.

Для создания условия поиска используется объектный подход. Из объектов представляющих логические и другие функции создаётся иерархия, теоретически её можно интерпретировать в подобие математической функции. Ниже пример условия для получения сущностей класса «news». Расшифровывается следующим образом: выбрать сущности (Q::Entity), принадлежащие классу (Q::IsClass), у которого атрибут ’sys_name’ (Q::Attrib) равен значению ‘news’ (Q::Comp).

$cond = Q::Entity(
		Q::IsClass(
			Q::Comp(Q::Attrib('sys_name'), '=', 'news')
	)
);

Условие используется для создания объекта запроса Query. Объектом Query выполняется интерпретация условия в SQL код и его выполнение при вызове метода Execute(). Результатом выполнения запроса является массив найденных сущностей либо, если условие начиналось с функции Count, результатом будет целое число – количество сущностей удовлетворяющих условию. Поиск сущностей можно ограничить по количеству и выполнить смещение как это делается параметром LIMIT в SQL, ограничение осуществляется вторым и третьим аргументом при создании запроса, либо использованием методов SetCount() и SetStart() объекта запроса.

// Объект запроса с условием $cond и ограничением результата количеством не более 10
// начиная с первой (0) сущности
$q = new Query($cond, 10, 0);

// Выполнение запроса. Результат – массив объектов данных Object
$list = $q->Execute();

if (sizeof($list)>0){
	echo $list[0]->getP('head')->getA('value'); // заголовок первой новости
}

Условие создается из объектов классов CondAttrib, CondClass, CondComp, CondCount, CondEntity, CondIsExist, CondLink, CondLog, CondNotExist и CondParam, но с целью упрощения синтаксиса вместо непосредственного использование оператора new и классов Cond* используется статический класс Q. С помощью его статических методов (фабричных методов) создаются объекты условия.

Условие включает в себя аргументы (сущности, атрибуты, связи), функции проверки наличия/отсутствия атрибутов или свойств у сущности, функцию подсчета количества свойств у сущности или самих сущностей, логические функции Or и And, выполняющие ещё роль скобок, и функцию сравнения значений атрибутов или результата функции подсчета количества с использованием операций ‘=’, ‘>’, ‘<','<=', '>=’,’<>‘,’like’.

В отличие от SQL, при выборке не нужно указывать, откуда выбирать (из каких таблиц), не нужны также объединения и группировка результатов. В запросе с помощью условия просто указывается ЧТО выбирать. Так как в объектной модели абсолютно всё является объектами, то и выбирать кроме объектов нечего, единственное можно вместо поиска объектов узнать количество объектов удовлетворяющих условию. Поэтому условие всегда начинается с аргумента сущности или функции подсчета сущностей.

//Условие - все сущности
$cond1 = Q::Entity( );
//Условие - количество сущностей
$cond2 = Q::Count(Q::Entity( ));

В первом варианте будет возвращен массив с абсолютно всеми сущностями объектной модели (все объекты, классы и связи), конечно, если не будет ограничений по количеству искомых сущностей в запросе Query. Во втором случаи будет возвращено число – общее количество сущностей.

Нас редко интересуют абсолютно все сущности, поэтому аргумент-условие сущности Q::Entity($cond) дополняется условием $cond, его ещё можно назвать фильтром. В нём определяется, какие атрибуты и свойства должны быть или отсутствовать у искомых сущностей, как бы продолжая условие: «Сущности, у которых…»

Атрибуты

Условие можно поставить на значение атрибута сущности. Для этого используется аргумент-условие обозначающий атрибут Q::Attrib и функция сравнения Q::Comp. Нижеприведенное условие означает: «Сущности, у которых атрибут sys_name равен значению link»

$cond = Q::Entity(
		Q::Comp(Q::Attrib('sys_name'),'=','link')
);

Условие можно дополнить логическими функциями «И» и «ИЛИ» (Q::LogAnd и Q::LogOr), если проверка на значение или общее условие на сущность неоднозначны. Логические функции «И» и «ИЛИ» играют также роль скобок, с помощью которых можно создавать сложные условия любой вложенности.

Запрос с нижеприведенным условием вернет сущности, у которых атрибут sys_name равен значению link или если атрибут sys_name равен значению label, а атрибут is_define равен 0.

$cond = Q::Entity(
		Q::LogOr(
			Q::Comp(Q::Attrib('sys_name'),'=','link'),
			Q::LogAnd(
				Q::Comp(Q::Attrib('sys_name'),'=','label'),
				Q::Comp(Q::Attrib('is_define'),'=',0)
			)
		)
);

В объектной модели, создание которой было рассмотрено в предыдущей статье, атрибут sys_name есть у классов и связей, поэтому результатом приведенного условия будет класс link и связи с системными именами label, не определяющие свойства. Результатом будут разнотипные сущности. Хотя приведенное условие вряд ли имеет практическое применение, но оно хорошо демонстрирует безразличность к типам сущностей при поиске, заметьте, в условии нет уточнений о существовании атрибутов, значения которых сравниваются. Поиск выполняется практически так же, как это делает человек. Например, найти всё, что красное среди разнотипных объектов: фонарь, закат, трактор, мяч, кровь, молоко, флаг CCСР :) – поиск осуществляется без проблем, главное, чтоб красными были.

Параметр вместо значения

В функции сравнения Q::Comp вместо скалярного значения, с которым сравнивается атрибут можно использовать параметр Q::Param, что позволяет в дальнейшем уже при сформированном запросе устанавливать значение параметра. Таким образом, можно использовать один запрос несколько раз с возможностью переназначения значений его параметров.

$cond = Q::Entity(
		Q::Comp(Q::Attrib('sys_name'),'=', Q::Param('par1'))
);
// Создаем запрос
$q = new Query($cond);
// Устанавливаем значение параметра
$q->SetValue('par1', 'news');
// Выполняем запрос
$list1 = $q->Execute();
// Устанавливаем новое значение параметра
$q->SetValue('par1', 'name');
// Выполняем запрос
$list2 = $q->Execute();

Кроме сравнения значения атрибутов, можно просто поставить условие на их наличие функцией-условием Q::IsExist(). Нижеприведенное условие определяет сущности, у которых есть атрибут value, результатом будут все объекты строк и чисел.

$cond = Q::Entity(
		Q::IsExist(Q::Attrib('value'))
);

Свойства

К атрибутам ещё вернемся, когда речь пойдет о сортировке, а сейчас про условия на свойства сущности. Свойство – это совокупность связи и сущности, с которой выполнена связь. Условие в запросе можно просто поставить на отсутствие/наличие свойства или на количество свойств у сущности. Но самое интересное от того что свойство – это объекты (повторяюсь) и поэтому условие можно поставить на атрибуты и свойства самой связи или сущности с которой выполнена связь. Только вдумайтесь, ведь связь и объект, с которым выполняется связь, могут иметь такие же условия на свои атрибуты и самое важное на свои свойства, как у искомой сущности, что позволяет создавать всеохватывающие условия. Например, можно искать сущности, у которых есть некое свойство, при этом объект являющийся свойством должен иметь связь (свойство) с объектом, который в свою очередь должен иметь атрибут value содержащий некий фрагмент текста. При всём этом в условии даже можно не уточнять что за свойство – его системное имя, в частности. Поиск будет оперировать только тем, что известно из условия. В общем, абстрактность, универсальность и гибкость.

Итак, начнем с условия проверки наличия свойства у искомых сущностей. На самом деле использовать функцию-условие Q::IsExist не нужно, так сам факт указания свойства в условие устанавливает наличие свойства.

$cond = Q::Entity(
		Q::Property( )
);

То же самое:

$cond = Q::Entity(
		Q::IsExist(Q::Property())
);

Вышеприведенным условием определено наличие свойства, а какого именно не указано, поэтому результатом запроса с данным условием будут все сущности, у которых есть хотя бы одно свойство, то есть условие звучит так: «Сущности, у которых есть свойство».

Обратным условием является проверка отсутствия свойства. Для определения отсутствия свойства используется функция-условие Q::NotExist. Результатом нижеприведенного условия будут все сущности, не имеющие свойств.

$cond = Q::Entity(
		Q::NotExist(Q::Property())
);

Как поставить условие на наличие конкретного свойства? Q::Property($link_cond, $entity_cond) имеет два аргумента для определения соответственно условия на связь и условия на сущность, с которой выполнена связь. Опять же, так как связь тоже является объектом, условия для неё возможны точно такие же, как и для сущности. Например, условие на атрибут.

Нижеприведенное условие использует условие на атрибут у связи свойства. Запрос с этим условием возвратит сущности, имеющие множественное свойство (size=0 означает множественность), а оно есть только у новостей. Если быть точнее сама связь с атрибутом size равным нулю есть у класса новости («news»), как определяющей комментарии, и у первой новости, имеющей два комментария, вторая новость комментариев не имеет (смотрите схему в предыдущей статье).

$cond = Q::Entity(
		Q::Property(
			Q::Comp(Q::Attrib('size'),'=',0))
);

Теперь добавим условие на сущность, с которой выполнена связь – поставим условие на свойство сущности, с которой выполнена связь. Нижеприведенное условие возвратит сущности, у которых есть множественное свойство, в свою очередь сущность являющаяся свойством имеет своё свойство без уточнений какое, но связанное с сущностью, атрибут value которой содержит фрагмент текста «Первый». Условие получилось абстрактным. Результатом его на самом деле будет первая новость, так как объект новости имеет комментарий, заголовок которого начинается со слова «Первый». Взаимосвязь между объектом новости и комментария как раз подходит приведенному условию

$cond = Q::Entity(
	Q::Property(
		Q::Comp(Q::Attrib('size'),'=',0), //множественное свойство
		Q::Entity( // условие на сущность, с которой выполнена связь
			Q::Property(
				null, // условия на связь нет
				Q::Entity(Q::Comp(Q::Attrib('value'),'like','%Первый%'))
			)
		)
	)
);

Продемонстрируем пример условия с подсчетом количества свойств. Нижеприведенное условие возвратит сущности, у которых более 4 свойств. Ими будут первая новость и класс категории. У новости имеется связь на категорию, заголовок, текст и два комментария – всего 5 свойств. У класса категории имеется название, описание и четыре связи, определяющие свойства для объектов – всего 6. В условии мы не уточняли, какие связи учитывать.

$cond = Q::Entity(
		Q::Comp(Q::Count(Q::Property()), '>', 4)
);

Ничто не ограничивает нас добавить условия свойству, можно даже выполнять подсчет свойств у сущностей, которые играю роль свойства у искомой сущности.

Сущность, являющаяся свойством

На практике часто необходимо выполнять поиск сущностей, являющимися свойствами других сущностей. Например, поиск комментариев, принадлежащих конкретной новости. Для этих целей используется аргумент-условие Q::IsProperty подобное аргументу-условию свойства, разница только в том что, им определяется связь и сущность-владелец связи. В остальном все также – условия на связь, условия на сущность. Можно даже также посчитать, сколько сущностей ссылаются на искомую и поставить условие, что должно быть не мене 2 владельцев.

$cond = Q::Entity(
		Q::Comp(Q::Count(Q::IsProperty()), '>=', 2)
);

Приведенное условие возвратит классы «string», «long_string», «category» и объект корневой категории с названием «Мероприятия» - все эти сущности имеют более одного владельца.

Принадлежность к классу

Ещё одним важным и часто используемым условием является принадлежность к классу Q::IsClass. Принадлежность к классу – это фактически свойство объекта, но оно требует особого способа проверки. Необходимо учитывать иерархию наследования, то есть проверка принадлежности к классу не сводится к банальной проверке сущности, с которым выполнена связь. Например, объект новости принадлежит к классу «news», но также принадлежит и классу «content» и базовому классу «id». Нижеприведенное условие возвратит все комментарии.

$cond = Q::Entity(
		Q::IsClass(
			Q::Comp(Q::Attrib('sys_name'), '=', 'comment')
		)
);

Класс тоже является объектом, поэтому можно уточнять условие класса, также как сущности – проверять атрибуты и свойства.

Применение логических функций Q::LogAnd и Q::LogOr продемонстрировано только на сравнении значений атрибутов, а ведь они могут включать в себя аргументы-условия свойств Q::Property, Q::IsProperty, принадлежности к классу Q::IsClass и функции Q::IsExist и Q::NotExist обеспечивая необходимую гибкость.

Сортировка

Поиск без сортировки никуда не годится. Используя сортировку можно находить актуальные темы, популярные товары и многое другое и применять с целью создания востребованного содержимого главных страниц сайта. Но как выполнять сортировку искомых сущностей, учитывая все возможные варианты условий, когда условие может даже ничего не уточнять о структуре сущностей? На самом деле определение сортировки осуществляется достаточно просто, а сама она выполняется непосредственно в СУБД при исполнении поискового SQL запроса.
Сортировать естественно можно только по скалярным значениям – по значениям атрибутов. Параметры сортировки указываются в аргументе-условии атрибута Q::Attrib. Таким образом, совмещается определение сортируемого атрибута и параметры сортировки (по убыванию или возрастанию, и порядок сортировки – если сортируемых атрибутов не один, то можно указать последовательность сортировки), и условие существования атрибута у искомых сущностей.

Сортировать можно по любому атрибуту, не обязательно принадлежащему искомым сущностям, можно хоть по атрибуту сущности, которая только косвенно связана чередой взаимосвязей с искомой. Помните, условие на атрибут можно поставить у связей и объектов являющимися свойствами? Так что можно даже сортировать по их атрибутам. Выходит, новости на сайте можно одновременно сортировать по дате создания и рейтингу автора новости. Или это все равно, что сортировать купленную продукцию по названию магазинов, в которых они были куплены – по свойству, не имеющего прямого отношения к продукции.

Теоретически возможна сортировка и по количеству свойств сущностей, например по количеству комментариев новости, но временно эта возможность не реализована.

Ещё следует заметить, что в параметрах сортировки указывается атрибут (его имя), но при этом уточнений о типе значения атрибута не требуются. Атрибут value, в частности, имеется у объектов разного класса и, самое важное, существуют различия в типах его значений у разных классов. Строки разной длины, числа целые и действительные. Но даже при этих обстоятельствах сортировка будет работать. Нижеприведенное условие возвратит отсортированный список объектов имеющих атрибут value. Сортировка будет происходить по атрибуту value по возрастанию. Третий аргумент (true) определят сортировку по данному атрибут, четвертый (1) – порядок сортировки (актуально при нескольких сортируемых атрибутах) и последних атрибут вид сортировки (false – по возрастанию, true – по убыванию)

$cond = Q::Entity(
		  Q::IsExist(Q::Attrib('value', null, true, 1, false))
);
// Запрос без ограничений количества искомых сущностей
$q = new Query($cond,0,0);
$list = $q->Execute();

Про второй аргумент, значение которого равно «null». С сортировкой разнотипных атрибутов система справляется за счет автоматического приведения значений к общему типу. При наличии хотя бы одного строкового типа все значения для сортировки преобразуются в строки. От этого сортировка чисел в строковом представлении видоизменяется и осуществляется посимвольно, а не по цельному значению, от чего нельзя с уверенностью сказать, что первое числовое значение больше остальных. В связи с этим применятся уточнение типа сортируемого атрибута – строковый или числовой. Уточнение типа повлияет и на общий список найденных сущностей. Будут возвращены соответственно сущности, обладающие только числовыми или только сроковыми атрибутами.

$cond = Q::Entity(
		  Q::IsExist(Q::Attrib('value', CondAttrib::TYPE_NUMBER, true, 1, false))
);

Заключение

Естественно, предложенный подход создания условия поиска может показаться сложным, но это только от того, что все мы привыкли к строковому описанию условий, как это делается в SQL и в языках программирования. Объектный же подход удобен, во-первых, для программного анализа и автоматического генерирования SQL запроса, во-вторых, для него будет нетрудно сделать осмысленный GUI интерфейс.

Ещё большую озабоченность должен вызвать вопрос о производительности и, какие SQL запросы получаются из этих мега гибких условий? Отвечаю. Запросы получаются не страшными, но применением нескольких LEFT JOIN-нов грешат и некоторыми другими конструкциями, без которых сложно достичь гибкости.

Вопросы производительности, способы оптимизации и реализации кэширования предлагаю обсудить на форуме в специально созданной теме. Здесь же прошу комментариев по теме статьи.

Комментарии

  1. Евгений пишет:

    С упоением читал всё - от и до
    Владимир - у меня к Вам вопрос
    Вы в данный момент трудоустроены?
    Над данным проектом один работаете?

    • Владимир пишет:

      Над проектом работаю пока один, но некоторые вопросы обсуждаются в маленьком, но постепенно растущем кругу. Присоединяйтесь - комментируйте, заходите на форум, стучитесь в аську ;) О личном по аське

  2. Хос пишет:

    EAV это очень гибкая штука, иногда достоинства такой модели перевешивают недостатки. Я некоторое время назад тоже был фанатом этой модели.

    Меня после прочтения один только вопрос мучает — а почему молоко красное? :)

    И еще замечание — запросы как-то более «человеческими» можно, наверное, сделать. Ну, это, наверное, со временем…

  3. Владимир пишет:

    Пожалуйста уточните, что именно для вас оказалось не в том виде как должно было быть? Насчет запросов полезней было бы увидеть пример/вариант удобный для вас, если речь, конечно, не идет о SQL. :) Над генерацией оптимизированного SQL я сейчас тружусь ;)

  4. X0c пишет:

    Нет-нет, в каком виде все “должно быть” - решать только вам :)

    Просто запросы вроде

        $cond = Q::Entity(
                Q::LogOr(
                    Q::Comp(Q::Attrib('sys_name'),'=','link'),
                    Q::LogAnd(
                        Q::Comp(Q::Attrib('sys_name'),'=','label'),
                        Q::Comp(Q::Attrib('is_define'),'=',0)
                    )
                )
        );
        $q = new Query($cond, 10, 0);
    

    на мой взгляд не очень хорошо воспринимаются, по крайней мере без некоторой тренировки. :)

    Да, я имел в виду использование языка запросов вроде SQL (или, может, вариации на тему XPath).

    Приведенный выше пример, как мне кажется, легче читался бы в таком виде:

    $q = new Query("sys_name = 'link' OR (sys_name = 'label' AND is_define = 0)", 10, 0);
    

    В результате парсинга должно формироваться точно то же самое, что создавалось “вручную” в примере, только читать, писать и изменять это проще.

    В принципе, написать парсер для разбора подобных строк несложно, но я понимаю, что сейчас есть более важные проблемы, которые нужно решать.

    А вообще было бы здорово, если бы можно было формулировать запросы на более высоком уровне абстракции, не концентрируясь на том, как организовано их хранение.

    Другой запрос из вашей статьи:

    $cond = Q::Entity(
         Q::Property(
             Q::Comp(Q::Attrib('size'),'=',0), //множественное свойство
             Q::Entity( // условие на сущность, с которой выполнена связь
                 Q::Property(
                     null, // условия на связь нет
                     Q::Entity(Q::Comp(Q::Attrib('value'),'like','%Первый%'))
                 )
             )
         )
     );
    

    Мне кажется, что мало кто сможет эффективно размышлять о предметной области (допустим, о тех же комментариях к новостям) таким образом.

    На самом деле на практике было бы удобно использовать что-то вроде:

      $q->where("comment.text LIKE '%Первый%'") // у сущности есть связь comment,  у того — text, содержащий строку 'Первый'
        ->limit(10)
        ->offset(0);
    

    (Да, я понимаю, что такой запрос значит не то же самое.)

    • Владимир пишет:

      Поймите ещё то, что потребуется дополнительное процессорное время для выполнения парсинга строкового условия.

      По второму условию, хоть вы и понимаете различия между своим “текстовом” вариантом. В объектном варианте не указывается конкретно, что комментарий у новости должен содержать текст %Первый%. Условие абстрактно, просто указывается, что любое из множественный свойств любой сущности должно содержать значение %Первый%. А уж так выходит, что пока только у новостей есть множественные свойство, называемое комментариями, и именно один из них удовлетворяет условию.

      Я не отказываюсь от возможности записи условий в текстовом виде, но сначала нужно тщательно проработать их синтаксис. Кстати, разработчикам сайта уже в готовой CMS не придется писать такие условия в объектном виде, так как будет реализовано нечто подобное модулю views у друпала :)

  5. egoholic пишет:

    Что-то долго нет новых постов. Жду их с нетерпением!!!!!!!!!

  6. Владимир пишет:

    Понимаю ;)
    Сейчас занимаюсь оптимизацией модуля Data. Рассказывать о проделанных изменениях в одной статье, не думаю, что будет интересно. Хотя для начала лучше узнать ваше мнение :)
    Посчитал лучше сейчас потратить время на сам движок, а потом уже по нему написать новый цикл статей, в который войдут темы разграничения прав, шаблонизация, кэширование и другое.

  7. Hivext Platform пишет:

    Мы ведем похожую разработку, похожа модель хранения данных на сервере, у нас это называется сервис структур http://www.hivext.ru/index.php/Структуры

    Думаю будет интересно ознакомиться с платформой и может даже объединить усилия. Простым языком, цель сделать платформу уровня win api, но для интернет приложений (любых приложений включая CMS).

  8. Андрей пишет:

    Я так понял данный класс работы с БД - это ваша разработка. Может вас заинтересует похожий абстрактный подход, реализованный ребятами из Zend, называемый LINQ for PHP

    http://devzone.zend.com/

    Изобретение велосипеда - не всегда оправданная задача, тем более для ЦМС, которой должны пользоваться другие программисты. А им далеко не всегда хочется учить новый для них синтаксис работы с БД. В связи с этим взять общепринятую библиотеку для ЦМС, на мой взгляд, гораздо перспективнее.

    Хотя, за 4 года профессионального программирования на php? я на самом деле, поклонник минимализма в коде. И писать кучу ненужных букв, чтобы создать запрос к БД я бы не стал.

    Например в моей реализации класса по работе с БД выборка имеет такой вид:
    $news = Dbs::select(’*', ‘news’, ‘name like “%Пример%”‘);
    просто, понятно и коротко)))))).

    • Владимир пишет:

      Существующие библиотеки не решают тех вопросов, что стоят передо мной. Работа направлена на достижение гибкости в управлении данными - создания любых информационных систем на уровне описания модели, а не создания таблиц и программирования классов. Да, с текущими доступными технологиями результат получается сложно применимым на реальных проектах (сайтах) из-за производительности, но сейчас важно, что желаемая гибкость достижима даже с использованием реляционных бд, причем без применения наворотов в SQL, без хранимых процедур и другого!!

      Спустя год, работа над движком Boolive перешла уже в практическое применение, и тут действительно лучше стало оперировать sql запросами, а гибкость организовывать на уровне логики, чтоб двиг был легким и быстрым, потому как на оптимизацию сложной программной логики модуля данных требуется невероятно много времени. Только итерационным путем эффективно двигаться к абсолютной гибкости, потому как даже со всех сторон проработанная модель данных поразит малейшее упущение. Как говорится, всё гениальное - просто :)) Сразу сообщу, что в скором времени сайт boolive.ru преобразится :)

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


(обязательно)


(обязательно) E-mail адрес не будет отображаться на сайте