Наверх

Знакомство с xPDO ч. 2 или xPDO для «гиков»

В разделе «Репосты» расположены чужие статьи, которые мне понравились или показались полезными.

После написания первой вводной статьи по xPDO, где я разобрал только базовые понятия и простейшие запросы/операции, обещался я вам (и себе), что обязательно копну глубже. Потребовало это достаточное кол-во времени, ибо «почва через чур каменистая», и похоже кроме самого Jason`а Coward`а никто не знает точно как сделать с xPDO все «правильно и красиво». Надеюсь недостаток документации восполнится после выхода Революшн (а на данный момент добрая половина функционала xPDO не задокументированна вобще :( ).
Посему за сеансом «просветления в Дао» я обратился лично у вышеупомянутому Джэйсону, и получил ответы на некоторые вопросы. (Хотя один из у меня разрешить так и не удалось, но об этом чуть позже).
Данная статья покрывает такие функции xPDO:
→ $xpdo->newQuery();
→ new xPDOCriteria();
→ $xpdo->getOne;
→ $xpdo->getMany;
→ $xpdo->getObjectGraph;
→ $xpdo->getCollectionGraph;
→ $xpdo->getCount;
→ незадокументированные методы создания и отладки запросов.
→ а также не упомянутые ранее вопросы «облегченного» удаления записей в БД.

Итак, в первом топике мы на «УРА» отработали простейшие запросы. Да вот незадача — они покрывают лишь частные случаи и зачастую неэффективны в плане расхода ресурсов.
В обычном SQL мы привыкли делать ограничения по выборке и т.д. Здесь на сцену выходит функция $xpdo->newQuery

// Простой пример newQuery 
<?php
$query= $xpdo->newQuery('Actor', array('actor_id:>' => 2));
$query->limit(10);
$actors = $xpdo->getCollection('Actor',$query);
foreach ($actors as $actor)
{
   echo $actor->first_name."
";
}
?>

Объясняю, что здесь произошло. Мы выбрали 10 записей актеров с ИД больше 2.
Функция newQuery (офф. документация по функции) это по сути конструктор запроса в объектно ориентированной форме. Первым параметром обязательно принимает имя объекта модели (имя таблицы БД — если проще), а вторым некий [mixed $criteria].

КСТАТИ данный объект под общим именованием $criteria принимают в себя почти все функции xPDO. Очень важным для понимания я считаю тот факт, что $criteria это обширный круг понятий и сущностей.

Это как минимум может быть:
— прямое указание на значение первичного ключа (например getObject('Actor', 1);
)
— пары значений/ключей (например array('actor_id:>' => 2))
— объект newQuery (который сам может содержать вхождение критерии)
— объект xPDOCriteria (который сам может входить в newQuery)
— ассоциативный массив привязок (bindings)
— … и ещё… сам Ковард знает что (вобщем полная свобода действий)
Так вот $xpdo->newQuery('Actor', array('actor_id:>' => 2));
это укороченная запись равносильно эту строку можно записать двумя строками:

<?php
$query= $xpdo->newQuery('Actor');
$query->where(array(
                        'actor_id:>' => 2)
                         );
?>

Не томя вам душу выкладываю, что у xPDO::newQuery есть следующие функции конструкторы запроса:
where
select
limit
sortby
groupby
innerJoin
leftJoin
rightJoin
andCondition
orCondition
setClassAlias

Так давайте же используем всё это на полную катушку… в диком, беспощадном и бессмысленном запросе :-)

<?php
// сложный пример 
$query= $xpdo->newQuery('Actor');
$query->select('Actor.actor_id, Actor.last_name');  
$query->where(array('actor_id:<=' => 20));
$query->andCondition(array('actor_id:!=' => 8));
$query->orCondition(array('actor_id:!=' => 7));
$query->sortby('actor_id','DESC'); 
$query->groupby('first_name'); 
$query->limit(10);

$actors = $xpdo->getCollection('Actor',$query);
foreach ($actors as $actor) {
    print_r($actor);
}
?>

!NB Здесь хочу отметить тот, неразрешенный мною до сих пор глюк, у меня почему то отказывается работать getCollection при использовании $query->select, при $query->innerJoin (left и right тоже), и попытке засунуть в него сырой запрос с xPDOCriteria. Почему-то обрывается соединение с сервером по нехватке памяти. Консультации по вопросу мне не помогли, пробовал на двух локальных серверах. Есть подозрения, что это какой-то глюк моей тестовой Sakila DB. На форуме меня заверили, что это должно работать, я склонен этому верить :) на днях дойдёт дело до сложных запросов в одном проекте, который я начал делать на xPDO — попробую на нём и отпишусь ещё по этой теме.

Кстати для любопытных — тот мега конструктор создает такой запрос:

... WHERE  ( `Actor`.`actor_id` <= ? AND `Actor`.`actor_id` != ? OR `Actor`.`actor_id` != ? )

А сейчас, господа, придержите ваши шляпы… дабы не произошел вынос мозга :) пишем вот такую вот конструкцию (чисто в тренировочных целях, не ищите смысл :) )

<?php
$query= $xpdo->newQuery('Actor');
$query->where(array(  
     array(  
         'actor_id:<=' => '20',  
          array(  
                   'OR:last_name:LIKE' => '%a%',  
                   'AND:actor_id:>' => '5' 
                   )  
             ),  
     'actor_id:<=' => '30',
     'OR:actor_id:>=' => '6',
     ));

$query->limit(10);
?>

тоесть все Condition`ы можно запихнуть прямо в where() и получаем такой запрос на выходе

... WHERE  (  ( `Actor`.`actor_id` <= ? OR  ( `Actor`.`last_name` LIKE ? AND `Actor`.`actor_id` > ? )  )  AND `Actor`.`actor_id` <= ? OR `Actor`.`actor_id` >= ? )

Из этого следует, что все то что находится в первом входном массиве where() по умолчанию собирается вместе с AND (последний OR:actor_id: показывает, что «не по умолчанию» xPDO ставит то что вы укажите), вложенность запроса создается доп. вложением массива, двоеточия это разделители между именем столбца и оператором запроса. Ну думаю это интуитивно понятно.

Однако остановлюсь на логических операторах запросов.
Вложенность массивов здесь регулирует вложенность условий в запросе.
Вот такая запись 'actor_id:<=' => '30' обозначает все актёры с ИД меньше либо равно 30. И по аналогии элементарно. Пишем логический знак после двоеточия. По умолчанию, если ничего не указать считается знак "=".
Однако заметьте !
Если вы пишите логический указатель перед именем колонки типа 'OR:disabled:!=' => true, то нужно вручную указывать "=" обязательно.
Например 'AND:gender:=' => 'M'.

Как же узнать, какой запрос сгенерирован?
Вариант 1 — Вставить после формирования newQuery и перед самим запросом, например, getCollection вызов дебаггера.
$xpdo->setDebug(true);
и в куче инфы, что вывалится на экран найти запрос (он там есть… и ошибки там отображаются).
Вариант 2 — Который мне нравится больше. Но он куда менее явный.
В том же месте, вместо дебага пишем эти строки
$query->prepare();
echo "RAW SQL : ".$query->toSQL();
И получаем чистый SQL запрос, без кучи лишней информации.

Как задать произвольный «сырой» SQL запрос?
При помощи xPDOCriteria

$actorTable = $xpdo->getTableName('Actor');  
$query= new xPDOCriteria($xpdo,'  
     SELECT first_name, last_name FROM '.$actorTable.'  
     WHERE actor_id = :index  
     LIMIT 1  
',array(  
':index' => 2,
)); 

$actors = $xpdo->getObject('Actor',$query);  
print_r($actors);

Замечу сразу, что SELECT * не проходит. Нужно указывать поля явно! Здесь полный полёт фантазии запрос может быть абсолютно любой и соответсвенно вместо getObject может стоять getCollection.
Но здесь вы берёт всю ответственность за запрос и результаты на себя.

РАБОТА СО СВЯЗАННЫМИ ТАБЛИЦАМИ
Для того, чтобы делать запросы с JOIN`ами через объектные конструкторы (а не сырые запросы с помощью xPDOCriteria) вам нужно провести подготовительную работу со сгенерированной моделью базы данных. А именно указать связи. (документация по теме svn.modxcms.com/docs/display/xPDO20/Defining+Relationships).

Есть два типа зависимости Композиционная (Composite) и Агрегирующая (Aggregate). Подробно о них читайте в офф. доках. По идее agregate просто устанавливает «ни к чему не обязывающую связь», а composite — подразумевает автоматическое удаление связанных с ним строк, в случае удаления самого композициионной строки. Вобщем я в этих вопросах не специалист и делал всё по аналогии с мануалом — вроде работает :)

В нашем примере это будет так. Есть три таблицы Actor (с актёрами), Film (с фильмами), FilmActor (связующая таблица Актер_ИД = Фильм_ИД).
Откройте ваш файл со сгенерированной схемой БД (у меня это models/MyDBModel.mysql.schema.xml). И укажем связи. У меня это вышло так.

<?xml version="1.0" encoding="UTF-8"?>
<model package="MyDBModel" baseClass="xPDOObject" platform="mysql" defaultEngine="MyISAM">
        <object class="Actor" table="actor" extends="xPDOObject"><param name="wmode" value="opaque"></param>
                <field key="actor_id" dbtype="smallint" precision="5" attributes="unsigned" phptype="integer" null="false" index="pk"  generated="native" />
                <field key="first_name" dbtype="varchar" precision="45" phptype="string" null="false" />
                <field key="last_name" dbtype="varchar" precision="45" phptype="string" null="false" index="index" />
                <field key="last_update" dbtype="timestamp" phptype="timestamp" null="false" default="CURRENT_TIMESTAMP"  extra="on update current_timestamp" />
                <composite alias="FilmActor" class="FilmActor" local="actor_id" foreign="actor_id" cardinality="many" owner="local" />
        </object>
        <object class="Film" table="film" extends="xPDOObject"><param name="wmode" value="opaque"></param>
                <field key="film_id" dbtype="smallint" precision="5" attributes="unsigned" phptype="integer" null="false" index="pk"  generated="native" />
                <field key="title" dbtype="varchar" precision="255" phptype="string" null="false" index="index" />
                <field key="description" dbtype="text" phptype="string" null="true" />
                <field key="release_year" dbtype="year" precision="4" phptype="string" null="true" />
                <field key="language_id" dbtype="tinyint" precision="3" attributes="unsigned" phptype="integer" null="false" index="index" />
                <field key="original_language_id" dbtype="tinyint" precision="3" attributes="unsigned" phptype="integer" null="true" index="index" />
                <field key="rental_duration" dbtype="tinyint" precision="3" attributes="unsigned" phptype="integer" null="false" default="3" />
                <field key="rental_rate" dbtype="decimal" precision="4,2" phptype="float" null="false" default="4.99" />
                <field key="length" dbtype="smallint" precision="5" attributes="unsigned" phptype="integer" null="true" />
                <field key="replacement_cost" dbtype="decimal" precision="5,2" phptype="float" null="false" default="19.99" />
                <field key="rating" dbtype="enum" precision="'G','PG','PG-13','R','NC-17'" phptype="string" null="true" default="G" />
                <field key="special_features" dbtype="set" precision="'Trailers','Commentaries','Deleted Scenes','Behind the Scenes'" phptype="string" null="true" />
                <field key="last_update" dbtype="timestamp" phptype="timestamp" null="false" default="CURRENT_TIMESTAMP"  extra="on update current_timestamp" />
                <composite alias="FilmActor" class="FilmActor" local="film_id" foreign="film_id" cardinality="many" owner="local" />
        </object>
        <object class="FilmActor" table="film_actor" extends="xPDOObject"><param name="wmode" value="opaque"></param>
                <field key="actor_id" dbtype="smallint" precision="5" attributes="unsigned" phptype="integer" null="false" index="pk" />
                <field key="film_id" dbtype="smallint" precision="5" attributes="unsigned" phptype="integer" null="false" index="pk" />
                <field key="last_update" dbtype="timestamp" phptype="timestamp" null="false" default="CURRENT_TIMESTAMP"  extra="on update current_timestamp" />
                <aggregate alias="Actor" class="Actor" local="actor_id" foreign="actor_id" cardinality="one" owner="foreign" />
                <aggregate alias="Film" class="Film" local="film_id" foreign="film_id" cardinality="one" owner="foreign" />
        </object>
</model>

Теперь нужно запустить парсер схемы в модель. Если делать по моей прошлой стате, то это файл dbParser.php
Всё после того как вы пересобрали модель БД с нужными связями можно идти дальше.

Как сделать запрос со связанными таблицами (тобишь JOIN`ами)?
1-ый вариант не самый оптимальный

$actor = $xpdo->getObject('Actor',1);  

echo "Actor - ".$actor->first_name."  ".$actor->last_name."
";
$actors = $actor->getMany('FilmActor'); 
 foreach ($actors as $actor) {  
    echo "Film ".$actor->film_id;
        echo " / ".$actor->getOne('Film')->title."
";
 }

Делаем выборку актёра с ИД = 1, потом выбираем «много» строк из таблицы-связки, по сути это выбор всех ИДшников фильмов с актером.
А потом уже пройдемся по всем Фильмам и получим инфу по каждому фильму.
Думаю вы поняли, почему это не оптимально в данном случае — куча запросов, по одному на каждый getOne.

Получение всех связанных объектов сразу
Вот здесь выходит на сцену «жадный загрузчик» $xpdo->getCollectionGraph

$items = $xpdo->getCollectionGraph('FilmActor', '{"Film":{},"Actor":{}}', array('FilmActor.actor_id' => 1,));
//print_r($items);
foreach($items as $item)
{
echo "FilmActor Actro_ID = ".$item->actor_id."
";
echo "Film Title = ".$item->Film->title."
";

За один запрос мы получаем всю инфу об актере, и фильмах.
Кстати, чтобы сделать более расширенный запрос можно использовать getCollectionGraph с newQuery, например.
Вот так
<?php
$c = $xpdo->newQuery('FilmActor');
$c->bindGraph('{"Film":{},"Actor":{}}');
$c->where(array(  
    'FilmActor.actor_id' => 1,
));
$items = $xpdo->getCollectionGraph('FilmActor', '{"Film":{},"Actor":{}}', $c);
?>
Новое здесь только bindGraph('{«Film»:{},«Actor»:{}}');
я честно говоря не понял как точно это работает, но такая запись в формате JSON нужна чтобы автоматически составить запрос со всеми связями. По аналогии сделать это легко для любых таблиц. Ну и понятное дело, что getObjectGraph() будет получать тоже самое, но лишь один экземпляр, то есть одну строку.

Подобные запросы можно сделать и без графов

$c = $xpdo->newQuery('FilmActor');  
$c->innerJoin('Film','Film','Film.film_id = FilmActor.film_id');
$c->innerJoin('Actor','Actor','Actor.actor_id = FilmActor.actor_id');
$с->select('Film.film_id, Film.title, FilmActor.film_id, FilmActor.actor_id');
$c->where(array(  
    'FilmActor.actor_id' => 1,
));
$items = $xpdo->getCollection('FilmActor',$c);

вот только если getCollectionGraph сам получает все связанные поля сразу и не реагирует на select(), то использование Join с newQuery требует использовать select() — иначе не будет выведено вобще ниодного поля.

Ну… и как маленькое дополнение и расслаление, после непонятных вещей :)
1) Функция считающая вхождения строк подходящих под данный критерий.
$total = $xpdo->getCount('Box',array(  
    'width' => 20,  
));  
}

2) Фишка, позволяющая полученный объект перевести в формат массива
$actor->toArray()
это антипод функции fromArray() (которая используется при создании объектов).
Кстати есть также функции toJSON() / fromJSON().

3) Также для удаления записей не обязательно их загружать и потом делать $item->remove();
Поидее можно делать $xpdo->removeObject('Table', $criteria) и $xpdo->removeCollection('Table', $criteria)

4) И напоследок конструкция, скорее памятка мне. Как можно обойтись без getCollection(), которая у меня глючит. За сие личный поклон гуру-отцу-основателю xPDO, которого я уже упоминал в этом посте :).

<?php
$actorTable = $xpdo->getTableName('Actor');  
$query= new xPDOCriteria($xpdo, "SELECT * FROM {$actorTable} WHERE `actor_id` < :index LIMIT 10", array(
    ':index' => 2,
));
if ($query->prepare() && $query->stmt->execute()) {
    while ($actor = $query->stmt->fetch(PDO::FETCH_ASSOC)) {
        print_r($actor);
    }
}
?>

Кто дочитал сей пост, писавшийся три дня, — тому пирожок :)

Ссылки:
Знакомство с xPDO ч.1
xPDO O/R Bridge 2.0 Documentation (очень советую прочитать, если вас заинтересовал xPDO, там не очень много, но помогает лучше понять).

Источник: community.modx-cms.ru/blog/documentation/448.html


5 комментариев

  1. Андер 19 августа 2016, 09:04 # 0
    Хорошая статья
    1. Mister N 04 апреля 2017, 11:42 # 0
      Илья, а почему первую часть не сохранил?
      web.archive.org/web/20160727034748/http://community.modx-cms.ru/blog/documentation/401.html
      1. Илья Уткин 08 апреля 2017, 08:25 # 0
        Да, надо будет перенести сюда эту статью =))…
      2. Евгений 01 августа 2017, 17:31(Комментарий был изменён) # 0
        Подскажите как реализовать на xPDO такой запрос?
        SELECT comment FROM Device, Remont WHERE remont.id=device.id
        1. Дмитрий 27 января 2022, 09:11 # 0
          Для тех, кто не знает ответа на этот вопрос:
          это реализуется через связи, нужно вернуться к 4 разделу (связи объектов) и ещё раз всё обдумать :)

        Авторизация

        через сервис Loginza:


        Шаблоны MODX

        1 2 Дальше »

        Объектная
        модель
        MODX