Разделы

Прямой эфир

Весь эфир | RSS

Знакомство с 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, там не очень много, но помогает лучше понять)
  • +12
  • 17 февраля 2010, 02:02
  • iJack

Комментарии (14)

RSS свернуть / развернуть
0
Ого. На вид вкусная вещь, только какая-то мозготравмирующая.

Как самочувствие после использования?

Ну и хотелось бы пару слов услышать про применение в жизни. Как оно?
avatar

pitbull

  • 17 февраля 2010, 08:55
0
Да, мозготравмирующая — не то слово :) Но коль уж твердо решил разобраться, то отступить не посмел. Основная проблема — это местами отрывочная документация. Скажем про ГРАФы я бы вобще долго еще гадал, не напиши мне Ковард на форуме вариант такого запроса.

Про применение в жизни. Вобще довольно неплохо — разнобразные варианты критерии, способ указания логических операторов — к этому очень быстро привыкаешь. Использовать их удобно. Сегодня буду как раз воплощать «сложные» запросы в одном «реальном» проекте — надеюсь getCollection() все же заработает :)
Одно неудобство — во время разработки меняется структура БД и часто приходится перегенерировать схему+модель. От чего слетают указанные связи «agregate/composite» — тут уже появилась задумка сделать GUI`шный интерфейс для генерации с возможностью сохранения связей.

Кстати еще есть возможность указать способы валидации записей перед сохранением (по простым логическим признакам типа мин-длина, макс-длина, не пустое и по вызову кастомной функции).
И вызывается просто $item->validate(); но об этому наверно будет 3-я статья :))) + еще может чего узрею к тому времени.

P/S Вобще ORM — это не мелочь по карманам тырить, с той же Doctrine разве что документации по более будет, а мороки подготовительной, наверно, не меньше. Зато xPDO — по размерам легка и реализует только необходимый минимум, что зачастую и требуется.
avatar

iJack

  • 17 февраля 2010, 11:48
0
Спасибо, очень интересно. Попробуем как оно на вкус!
avatar

bullder

  • 17 февраля 2010, 09:29
0
Огромное спасибо за этот титанический труд! :-)

xPDO меня буквально наполняет благоговейным трепетом. С одной стороны он кажется монстрообразным, а с другой его гибкость поражает.

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


Это уже на Revo или отдельно?
avatar

Carw

  • 17 февраля 2010, 13:21
+2
Не, стоит самому начать разбираться и его монстрообразность развеивается :) Остаются некоторые странности, но надеюсь они будут отшлифовываться по ходу разработки Революшна. Я кстати решился взяться именно за xPDO как раз по той причине, что он изначально разработан для того, чтобы писать на нем CMF. И люди реально пишут на нем мега-CMF :)

А тот проект что я говорю, это совершенно отдельно от MODx, просто надоело писать ручками запросы и решил использовать ОРМ фрэймворк, вот и влез на свою голову, решил типа убить одним выстрелом двух зайцев (параллельно глубже поняв как работает MODx Revo) :)

avatar

iJack

  • 17 февраля 2010, 13:39
0
Слава первопроходцам! :-))
avatar

Carw

  • 17 февраля 2010, 13:42
0
Начинал делать один проект (PHP daemon, CLI mode), активно использующий MySQL. Работал с PDO и чистым PHP. Вы меня переубедили, спасибо (чем сильно облегчили жизнь).
avatar

atma

  • 2 марта 2010, 19:51
0
Вобще, положа руку на сердце — xPDO не идеален и не оптимален и… наверно можно найти ОРМ фрэймворк по удобнее и более функциональный. Но чего не отнять так то, что после работы даже с ним писать скрипты без ОРМ стало очень лень, т.к. разобравшись раз он уже сильно облегчает жизнь :)

З.Ы. Кстати про баги с getCollection что я писал в посте — их похоже реально нет, это были проблемы какие-то с конкретной тестовой БД. С другой БД все работает хорошо.
avatar

iJack

  • 2 марта 2010, 23:14
+1
Вот натолкнулся снова на вашу статью и вспомнил, что у меня возникла небольшая проблема с IN statement, может кому пригодится:
$c->where(array(
    "id IN (" . implode(',', $ids) . ")"
    //"id IN (1,3,5,7,11)"
));

Другими словами AND, OR, LIKE, etc PDO понимает как экспрешн, а тут надо raw, но все же array
avatar

atma

  • 8 мая 2010, 11:57
+1
занимательная штука xpdo и спасибо за статью. все это можно было бы понять прочитав и мануал по xpdo но у тебя получилось по-хорошому смешно и более ближе к правде. только начал разбираться с xpdo… у тебя нет идей как получить ссылку на сам родной pdo экземпляр имея xpdo? нужно выполнить пару запросов в обход xpdo (не хотелось тревожить таким вопросом самого мистера Коварда).
avatar

kzawner

  • 26 июня 2010, 20:01
0
Про экземпляр PDO точно не знаю, но в коде класса xPDO сразу видно
* A PDO instance used by xPDO for database access.
     * @var PDO
     * @access protected
     */
    protected $pdo= null;

Никакого публичного класса для возврата объекта PDO я не увидел, значит как я понимаю нужно самому дописать в класс xPDO что-то типа
public function getPDO() {
          return $this->pdo;
    }
может будет работать :)

А кстати основная сложность и непонимание в xPDO у меня была как раз в том, как сгенерировать и распарсить модели классов, что-то документация абсолютно невнятно освещала эти вопросы. Да и кстати использовал его в разработке двух не маленьких проектов — всё классно, экономит время и нервы (когда знаешь все его приколы :) ), но всё же понял, что без полноценного фрэймворка с АР не обойтись, так что дальше использовать xPDO врядли буду.
avatar

iJack

  • 27 июня 2010, 17:20
+1
getPDO()… я тут подумал, xpdo делает эмуляцию pdo для php4 — по этому должно быть можно его самого юзать как pdo… в больших проектах — может понадобиться интеграция с формами, урлами, генераторами и еще чем то (www.symfony-project.org)… мне как раз нравиться что xpdo легкий и его можно использовать как бы между прочим и не уходя от SQL, его легко усвоить. кроме того он прошел проверку на прочность с modx cms… я думаю.
avatar

kzawner

  • 30 июня 2010, 20:50
0
Спасибо — c xPDO познакомились, а про то как эти же методы использовать но в MODX тоже где то упоминалось? Если нет и это кому нибудь нужно — оставляйте зайявочки тут — готов сделать описание действий для добавления совего пакета в MODX ;-)
avatar

dmitry_modx_customize

  • 6 января 2012, 20:50
0
Хорошая статья. Спасибо. Есть вопрос. Почему запрос составленный при помощи xPDOCriteria является «сырым»? Вы хотите сказать, что в окончательном коде лучше не использовать его? Если «да», то почему? По моему разумению, лучше пользоваться синтаксисом стандартного SQL, любому программисту все будет понятно.
С уважением. Елена.
avatar

defele

  • 17 января 2012, 12:13

Только зарегистрированные и авторизованные пользователи могут оставлять комментарии.