Самые быстрые сниппеты с pdoTools

Давно изместно, что xPDO не нужен для выборки и вывода большого количества данных. Зачем его использовать, создавая кучу объектов, жрать процессор и память, если мы хотим просто выбрать 100 строк из БД и вывести их на экран?

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

Тогда я написал себе список хотелок:
— Быстрое создание готового сниппета.
— Любые выборки, из любых таблиц с любыми условиями и джоинами.
— Учет времени на каждую операцию, подробный лог для выявления узких мест.
— Итоговые сниппеты должны работать с getPage, автоматически.
— Лёгкая кастомизация, оно не должно меня ограничивать.
— Самый быстрый рендер чанков, быстрее только вообще без них.

Simple Dream дали добро на это дело, и в итоге вышла мини-библиотека pdoTools, которая уже входит в состав Tickets и войдёт в miniShop2.

Она отвечает всем моим требованиям и позволяет писать самые быстрые сниппеты для MODX Revolution, всего за 10 минут.

Принцип работы


Исходный код библиотеки находится тут и пока состоит из двух классов: pdoTools и pdoFetch.

Первый класс предоставляет общие инструменты, такие как логирование работы сниппета и рендер чанков, для полноценной работы его нужно расширять. Что и делает второй класс, pdoFetch — он обеспечивает составление запроса на xPDO и выполнение его через PDO.

Почему запрос строится, всё же на xPDO? Несколько причин:
1. Это очень удобно.
2. Защищает от инъекций.
3. В теории, позволит использовать другие СУБД, не только mySql.
4. Кушает немного времени. Больше чистого SQL, но на фоне выборки — ерунда.

Итак, pdoFetch расширяет основной класс, и добавляет к нему построение запроса, выборку и вывод данных. Возможно, в будущем появятся и другие классы — pdoUpdate, pdoRemove и т.д.

Вывод возможен в трех вариантах (параметр return):
sql. Возвращается чистый SQL, который вы можете проверить в PhpMyAdmin.
data. Возвращается массив данных выборки. Вы можете его обработать самостоятельно и вывести как хотите.
chunks. Выводятся оформленные результаты, пригодные для getPage. Это режим по умолчанию.

При построении запроса, все джоины и селекты логируются специальной функцией, и вы сможете посмотреть очень подробно, как прошел запрос. Туда же пишется и время выборки, время обработки чанков и что захотите — функция pdoTools::addTime('Текст записи').

Тестовый сниппет


Первым делом нужно установить pdoTools из репозитория.

Теперь мы можем запустить класс pdoFetch. Запускаем именно дочерний класс, он сам подключит и инициализирует основной.

Можно так:
$path = MODX_CORE_PATH . 'components/pdotools/model/pdotools/';
$pdoFetch = $modx->getService('pdofetch','pdoFetch', $path, $scriptProperties);
или так:
require_once MODX_CORE_PATH . 'components/pdotools/model/pdotools/pdofetch.class.php';
$pdoFetch = new pdoFetch($modx, $scriptProperties);
Разницы нет.

Главное, мы должны передать объект modX и параметры для запроса. Если передать пустой массив — будет выборка modResource.

Таким образом, простейший сниппет Test на pdoTools выглядит так:
$path = MODX_CORE_PATH . 'components/pdotools/model/pdotools/';
$pdoFetch = $modx->getService('pdofetch','pdoFetch', $path, $scriptProperties);

return $pdoFetch->run();
На странице мы вызываем его так (обратите внимание на limit):
[[!getPage?
    &element=`Test`
    &limit=`100`
    &tpl=`tpl.Test.row`
]]

<div class="pagination">
    <ul>
        [[!+page.nav]]
    </ul>
</div>

<pre>[[+pdoFetchLog]]</pre>
Чанк оформления:
<p>[[+id]] - [[+pagetitle]]</p>
Результат — 0,15 сек на выборку и вывод 100 ресурсов!

Обратите внимание на вывод лога под результатами — это метод pdoTools::getTime() — он получает отформатированный лог всех операций.

А что будет, если выбрать 500 ресурсов? 0.62 секунды, это выборка и рендер простого чанка!
Ну ладно, это простой пример, давайте более сложный посмотрим.

Снипет для выборки блогов


Работа с выборкой тикетов — это реальная задача, на которой я тренировался. Дело в том, что Tickets — очень сложная система и страницы в ней имеют множество необычных параметров, например количество комментариев, или просмотров.

pdoFetch выбирает все данные за один SQL запрос, что означает — для выборки всех значений тикетов нам придется сделать несколько джоинов и селектов.

Смотрим исходный код сниппета getSections и видим, что pdoFetch принимает следующие параметры:
  • class — базовый класс для выборки.
  • where — условия выборки, закодированные в JSON.
  • leftJoin, innerJoin, rightJoin — различные джойны в JSON. Форма массива такой: {класс: {псевдоним, поля объединения}}. В дальнейшем к полям подлючённых таблиц обращаемся по асевдонимам.
  • select — Поля для выборки, можно использовать all или * для выборки всех полей класса. Также json, вид {псевдоним: поля}.
  • groupby — поле для группировки результатов.
  • sortby — поле для сортировки
  • sortdir — направление сортировки
  • limit — ограничение количества результатов.
  • offset — пропуск результатов от начала.
  • fastMode — не запускать парсер MODX для обработки оставшихся плейхолдеров, а просто вырезать их.
  • return — что возвращать: sql, data или chunks.
В коде видно, что джоинится и что выбирается. В конце работы, если пользователь авторизован в контексте mgr, к результатам прибавляется лог.

Не смотря на чудовищное количество джойнов и разных селектов, с подсчетами сум и рядов из таблиц — скорость невероятная, всего 0.028 сек!
Потому что, главный тормоз в такой работе — это не mySql, а обработка результатов и вывод на экран. Результатов всего 9, обрабатывать особо нечего.

Отсюда вывод: нужно джойнить максимальное количество таблиц, и всё, что можно — считать там, чтобы в итоговом массиве данных было как можно больше будующих плейсхолдеров. Тогда парсеру MODX не придётся натруждать себя и ваши выборки залетают.

Второй вывод — использовать фильтры вывода и сниппеты в чанка категорически не рекомендуется.
А что же делать, если нам нужно проверить или изменить определённые поля? Смотрим следующий сниппет.

Снипет для выборки тикетов


Сниппет getTickets, исходный код вот тут.

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

Так как мы работаем с БД напрямую, методы класс Ticket не обезопасят вывод данных страницы. Значит, нам нужно изменить вывод этих данных самостоятельно.

Делается это просто:
1. Мы должны указать параметр return = data.
2. При получении массива данных, самостоятельно его обработать.

Смотрим во вторую часть сниппета и видим эту обработку. Обратите внимание, что для процессинга чанков применяется метод pdoFetch::getChunk(). Это особо быстрый парсер чанков, отличный от родного из класса modX.

Суть его в следующем: сначала заменить все возможные плейсхолдеры на значения, а остатки уже отдать в парсер MODX (если не включен fastMode). Так же он экономит время на получении чанка, потому что всегда держит его в памяти, не получая из кэша.

Это самая тяжелая выборка, ибо джойнов и плейсхолджеров тут гораздо больше, плюс у меня в чанках есть условие по выводу introtext и вызов сниппета dateAgo — это 2 главных фактора замедления.

Но всё равно, время выборки и обработки 10 тикетов всего 0.071 сек.


Заключение


pdoTools я теперь использую практически везде, что позволяет строить по настоящему быстрые сайты. Если освоить принципы работы с библиотекой — вы сможете за 10 — 15 минут делать сниппеты, совместимые с getPage, которые будут выбирать любые данные из любых таблиц, и выдавать их максимально быстро.

На этом сайте через pdoTools выбраются:
— Тикеты во всех блогах.
— Тикеты отдельного юзера юзера, пример.
— Комментарии отдельного юзера, пример.
— Секции тикетов в разделе "Блоги".
— История платежей.

Как видите, уже сейчас pdoTools можно использовать хоть для чего, а со временем возможностей еще прибавится.
Василий Наумкин
11 января 2013, 06:43
8
7 316
0

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

Andrei Kilin
11 января 2013, 15:37
0
Отличное начало, спасибо!
Andrei Kilin
11 января 2013, 16:48
0
На этом сайте через pdoTools выбраются:
— Тикеты отдельного юзера.
Борзану малясь: можешь подробнее разжевать по выборке тикетов по пользователю из всех секций?
    Василий Наумкин
    11 января 2013, 17:05
    0
    По умолчанию выбирается все записи из таблицы, значит их нужно ограничить.

    Нам подойдёт ограничение по классу, автору и статусу:
    $where = array(
        'Ticket.class_key' => 'Ticket'
        ,'Ticket.published' => 1
        ,'Ticket.deleted' => 0
    );
    if (!empty($_REQUEST['uid'])) {
        $where['Ticket.createdby'] = intval($_REQUEST['uid']);
    }

    Вот и всё.
    Также можно приджойнить TicketsSection, чтобы выбирать названия и урлы на разделы.
Александр Наумов
11 января 2013, 16:53
1
0
Спасибо, очень познавательная статья!
Andrei Kilin
14 января 2013, 10:37
0
Василий, а как ты выводишь dateAgo в tpl.Tickets.list.row?
У меня [[+date_ago]] не отрабатывает, хотя в «последних записях» нормально показывается.
    Василий Наумкин
    14 января 2013, 12:27
    0
    Сниппетом
    [[!dateAgo?input=`[[+createdon]]`]]

    Напряжно прямо в объект это вписывать — лишний тормоз будет.
      Andrei Kilin
      14 января 2013, 12:52
      0
      Меня просто дико смутило из описания последней беты:
      — Сниппет dateAgo интегрирован прямо в компонент. На самом деле, это код из компонента выделен в сниппет, но сниппет вышел раньше.
      А учитывая, что в Латестах выводится, да еще в чанке tpl.Tickets.list.row стоит [[+date_ago]] так вообще ступор произошел =))

      Понял, делаю сниппет.
        Василий Наумкин
        14 января 2013, 12:57
        0
        Конечно, интегрирован и доступен как метод Tickets::dateFormat();

        Но не засунут прям в класс Ticket, то есть, через getResources такого поля не будет. Можно сделать даже так, но я посчитал это лишней нагрузкой.

        Выводи тикеты через getTickets и добавь там обработку дат — будет быстрее, чем вызов сниппета в чанке. Надо будет так сделать из коробки, кстати.
          Andrei Kilin
          14 января 2013, 14:16
          0
          Хихи, «добавь обработку», я ж дурачок, я в кусках которые ты уже приводил запутался и никак не победю так как хочется :)
            Василий Наумкин
            14 января 2013, 14:21
            0
            Ну тогда делай сниппетом.

            А обработка показана в getTickets, смотри код. Уже добавил это по-умолчанию, в следующей версии будет.

            Да и версия будет, скорее всего, сегодня.
Григорий
15 января 2013, 00:38
0
пдотулс — кайф) Как раз в тему попал — в одном проекте возникла необходимость выводить много позиций сразу.
Немного смахивает на getProducts у Чирко (у него по сути это переработанная лайт-версия getResources с фильтрацией по TV и тд),

Небольшой офф. Задали мне тут задачу, сайт с анкетами (заполняются админом) людей. Анкет планируется, видимо, много, есть ли смысл использовать тикетс для этого (одна анкета — один тикет). Необходимо, чтобы у каждой такой анкеты настраивались параметры вроде «рост», «возраст», «цвет глаз», ну и фото естественно. Или не мудрить и юзать обычные ресурсы с TV?
    Василий Наумкин
    15 января 2013, 04:42
    0
    Tickets быстрее и лучше по нескольким причинам:

    1. Не чистят весь кэш сайта при обновлении.
    2. Не забивают дерево ресурсов — удобнее работать.
    3. Контент тикета автоматически фильтруется и типографируется Jevix.
      Григорий
      15 января 2013, 04:47
      0
      Именно по этим причинам меня тикетс и привлек (скорость и удобство для энд-юзера)
      Правильно ли я понимаю, что к тикету можно добавить свои поля и фильтровать по ним при выводе?
        Василий Наумкин
        15 января 2013, 04:50
        0
        Да, конечно, ТВ работают так же, как и у обычных ресурсов.

        Фильтровать можно по всякому, хоть через getResources, хоть своим сниппетом. Только учти, что в getResources обязательно нужен параметр &showHidden=`1`.
          Григорий
          15 января 2013, 11:28
          0
          отлично, щас буду разбираться)
Виталий Батушев
18 января 2013, 21:21
0
Вах! Теперь это еще и отдельный пакет!
    Василий Наумкин
    18 января 2013, 21:41
    0
    Так проще обновлять.

    Иначе расползётся разными версиями по другим компонентам — и будут непонятки.
      Виталий Батушев
      18 января 2013, 22:13
      0
      Да вчера во время отсутствия интернета думал, что неплохо тебе подсказать, что надо выделить его в отдельный компонент. А ты вона чо — сам знаешь, как надо :)
Виталий Батушев
19 января 2013, 20:39
0
Возник вопрос. Хочу я вместе с полями ресурса взять и все его TV.
Понаписал вот это:
$pdoFetch = $modx->getService('pdofetch','pdoFetch',$modx->getOption('pdotools.core_path',null,$modx->getOption('core_path').'components/pdotools/').'model/pdotools/',$scriptProperties);

$where = array('published' => 1,'deleted' => 0,'parent' => $parent);

$default = array(
	'class' => 'modResource'
	,'where' => json_encode($where)
	,'leftJoin' => '{"modTemplateVar":{"alias":"modTemplateVar","on":"modTemplateVar.id=modTemplateVarResource.tmplvarid"}},"modTemplateVarResource":{"alias":"modTemplateVarResource","on":"modTemplateVarResource.contentid=modResource.id"}}'
	,'select' => '{"modResource":"all","modTemplateVar":"all","modTemplateVarResource":"all"}'
	,'sortby' => 'menuindex'
	,'sortdir' => 'asc'
	,'return' => 'data'
);

$scriptProperties = array_merge($default, $scriptProperties);
$pdoFetch->config = array_merge($pdoFetch->config, $scriptProperties);

$rows = $pdoFetch->run();
На выходе нуль. В журнале ошибок: [2013-01-19 20:33:28] (ERROR @ /index.php) [pdoTools] Error 42S22: Unknown column 'modTemplateVar.id' in 'field list'

Вот чую, что глупость какую-то написать в leftJoin, но осознать ее не могу.
    Василий Наумкин
    19 января 2013, 23:31
    0
    Нужно подключать modTemplateVarResource, причем, сколько ТВ — столько раз и нужно подключать, под разными алиасами. Это потому, что один ТВ — это одна строка в таблице в id ТВ, id ресурса и значением.

    Я совсем не уверен, что это будет хорошо и быстро, если ты подключишь одну таблицу 10 раз.
      Виталий Батушев
      19 января 2013, 23:43
      0
      Угу. Спасибо. Затея была глупая. Переделал все по-другому.
Andrei Kilin
13 февраля 2013, 11:49
0
Подскажите как выбрать значение из extended пользователя. А то мой «метод тыка» привел к письму с линоды о том, что я ломаю их рейд =)
    Василий Наумкин
    13 февраля 2013, 14:15
    0
    Да это обычное поле, просто там внутри json строка.

    Выбираешь, декодируешь, обрабатываешь. Не знаю, какие могут быть проблемы.
      Andrei Kilin
      14 февраля 2013, 15:12
      0
      Вроде разобрался. спасибо
Fedor
09 марта 2013, 15:50
0
Василий, сколько будет стоить консультация по этой теме? Нужно организовать отбор и вывод товаров по TV + параметрам MiniShop. Не нашел вашу почту, пишу сюда.
    Fedor
    09 марта 2013, 18:17
    0
    ау… :(
      Василий Наумкин
      09 марта 2013, 20:58
      0
      Потерпи, попробую завтра разобраться с твоим вопросом.
        Fedor
        10 марта 2013, 13:41
        0
        Ок, было бы здорово :)
Мордынский Николай
18 марта 2013, 10:29
0
Вот бы getResource на рельсах XPDO ))
    Перетягин Илья
    26 марта 2013, 11:23
    0
    скорость нвероятная, всего 0.028 сек!
    Не могу пройти мимо ))
      Василий Наумкин
      26 марта 2013, 11:48
      0
      Ты зачем это другим людям то пишешь?

      Ссылку «Оставить новый комментарий» внизу не видно?

      За правки спасибо, уже думаю о каком-то удобном механизме уведомлений об опечатках.
        Перетягин Илья
        26 марта 2013, 17:24
        0
        На счет «другие люди» хотел еще написать, но отвлекли, а потом забылось…
        Последний коммент очень близко находится к тексту «Оставить новый комментарий» и сделан таким же цветом. Можно промазать..., а когда увидел, куда написал, то… что уж…
Алексей
07 июня 2013, 14:00
0
я правильно подключаю класс?
$packageName = 'doodles';
$packagepath = $modx->getOption('core_path') . 'components/' . $packageName . '/';
$modelpath = $packagepath . 'model/';
$modx->addPackage($packageName, $modelpath);
Алексей
07 июня 2013, 14:03
0
как отключить кэширование?
Peter Zenin
08 июня 2013, 21:55
0
А если запрос достаточно простой, что быстрее использовать getCollection('msCategory',array('parent'=>10)) или составить и выполнить запрос с pdotools?
Peter Zenin
09 июня 2013, 12:59
0
Я делаю запрос на msProduct, а pdoTools мне возвращает данные из таблицы site_content, разве что только с class_key = msProduct… Почему так? А как сделать полноценный запрос к msProduct через pdoTools?

$whereImages = array();
        $whereImages['parent'] = $row['id'];

        $defaultImages = array(
            'class' => 'msProduct',
            'where' => $modx->toJSON($whereImages),
            'fastMode' => true,
            'return' => 'data',
            'limit' => 0,
        );

        $pdoFetch->config = array_merge($pdoFetch->config, $defaultImages, $scriptProperties);

        $rowsImages = $pdoFetch->run();

        foreach ($rowsImages as $k_i => $row_i) {
            echo "[".$k_i."] => 
";
            //echo $row_i["thumb"];
            var_dump($row_i);
            echo "
\n";
            echo "<hr>\n";
        }
Мне надо получить данные поля thumb из таблицы ms2_products
    Василий Наумкин
    09 июня 2013, 13:06
    0
    Тебе нужен класс msProductData.
      Peter Zenin
      09 июня 2013, 13:37
      0
      Точно! Благодарю!
        Peter Zenin
        09 июня 2013, 14:01
        0
        Если кому пригодится, вот выборка всех данных продукта:
        $whereImages = array();
        $whereImages['parent'] = $productId;
        $leftJoin = '{"class":"msProductData","alias":"Data","on":"msProduct.id=Data.id"}';
        $resourceColumns = $modx->getSelectColumns('msProduct', 'msProduct');
        $dataColumns = $modx->getSelectColumns('msProductData', 'Data', '', array('id'), true);
        $select = '"msProduct":"'.$resourceColumns.'","Data":"'.$dataColumns.'"';
        $defaultImages = array(
            'class' => 'msProduct',
            'where' => $modx->toJSON($whereImages),
            'leftJoin' => '['.$leftJoin.']',
            'select' => '{'.$select.'}',
            'fastMode' => false,
            'return' => 'data',
            'limit' => 0,
        );
        $pdoFetch->config = array_merge($pdoFetch->config, $defaultImages, $scriptProperties);
        $rowsImages = $pdoFetch->run();
        
        foreach ($rowsImages as $k_i => $row_i) {
            echo "<hr>\n";
            echo $row_i["thumb"];
            echo "<hr>\n";
        }
Виталий Греков
02 февраля 2014, 04:50
0
Есть PHP скрипт, его задача подставлять параметры значений для вывода списка роликов в плейлисте, вот привожу его
<?php        
include_once('class/class.youtubelist.php');
             
       $video = new youtubelist('playlist');
       $video->set_playlist ('вывод данных с TV');
       $video->set_max(50);
       $video->set_order('relevance');
       $video->set_cachexml(false);               
       $video->set_cachelife(86400);                
       $video->set_xmlpath('./cache/');
       $video->set_lang('en');
       $video->set_start(1);
       $video->set_time('all_time');
       $video->set_descriptionlength(300);
       $video->set_titlelength(75);
                
if ( $video->get_videos() !=null ) {
	   foreach ($video->get_videos() as $yKey => $yValue) {
	        echo '<li><p>' . $yValue['title'] . '</p><span class="time">' . $yValue['time'] . '</span><a class="videoThumb" href="http://www.youtube.com/watch?v=' . $yValue['videoid'] . '">' . $yValue['description'] . '</a></li>';
	                }
	        }else{
	         echo '<li>Извините, нет видео</li>';
	        }
Помогите пожалуйста, оформить его в виде быстрого снипетта

P.S. Василий, в заметке ссылки на исходный код, ведут на 404
    Василий Наумкин
    02 февраля 2014, 07:28
    1
    +1
    <?php
    include_once('class/class.youtubelist.php');
    
    $video = new youtubelist('playlist');
    $video->set_playlist ('вывод данных с TV');
    $video->set_max(50);
    $video->set_order('relevance');
    $video->set_cachexml(false);
    $video->set_cachelife(86400);
    $video->set_xmlpath('./cache/');
    $video->set_lang('en');
    $video->set_start(1);
    $video->set_time('all_time');
    $video->set_descriptionlength(300);
    $video->set_titlelength(75);
    
    /** @var pdoTools $pdo */
    $pdo = $modx->getService('pdoTools');
    if (empty($tpl)) {$tpl = '@INLINE <li><p>[[+title]]</p><span class="time">[[+time]]</span><a class="videoThumb" href="http://www.youtube.com/watch?v=[[+videoid]]">[[+description]]</a></li>';}
    
    $output = '';
    if ($video->get_videos() != null) {
    	foreach ($video->get_videos() as $video) {
    		$output = $pdo->getChunk($tpl, $video);
    	}
    }
    else {
    	$output = '<li>Извините, нет видео</li>';
    }
    
    return $output;
      Виталий Греков
      03 февраля 2014, 01:25
      0
      Василий, спасибо большое.

      $video->set_playlist ('вывод данных с TV');
      место надписи «вывод данных с TV» должно быть значение TV, та как не знаю, как в сниппете прописать это, пока вставил код плейлиста на прямую, выглядит так
      $video->set_playlist ('se_cjkuWpJFbiZSVKv69ertW1VyuHdKJ');
      вот что получилось
      также залил скрипт проверить, вот так должно быть
      получается, сниппет выводит только один и почему то последний.
        Василий Наумкин
        03 февраля 2014, 06:53
        +1
        Получение ТВ из текущего ресурса
        $video->set_playlist($modx->resource->getTVValue('имяТВ'));

        А в цикле я точку пропустил, перед знаком равно:
        $output .= $pdo->getChunk($tpl, $video);
Oleg
29 января 2016, 11:29
0
Добрый день, подскажите реализована ли возможность выборки из БД с 2ой сортировкой?
    Василий Наумкин
    29 января 2016, 11:31
    0
    Вот бы еще узнать, что такое «2ой сортировкой»
      Oleg
      29 января 2016, 12:14
      0
      Извините за сумбурность.
      Уже просмотрел код и увидел, что используется
      $this->query->sortby($sortby, $sortdir);
      «2ой сортировкой» я подразумевал, что мне нужно отсортировать выборку, к примеру по количеству комментариев, а в случае если у записей количество комментариев одинаково, тогда такие записи сортировать еще по дате публикации.