Про xPDO

Эта заметка назревала уже очень давно, полгода минимум. Вокруг замечательного MODX Revolution сломано много копий. Ходят слухи, что он «тормозной», «прожорливый» и «неповоротливый». И главным виновником всегда называют xPDO.

Конечно, это чушь и цель заметки — развенчание мифов. Закрыть, наконец, вопрос с «тормозами» и «прожорливостью». Показать, насколько Revolution удобен и гибок, что он позволяет работать как через ORM xPDO, так и без него — через обычный PDO.

Вы можете работать с MODX разными путями, вам не навязывают xPDO. Не нравится — не пользуйтесь, пишите вручную SQL запросы.

Со вступлением покончено, идем дальше.

Подготовка


Для того, чтобы разобраться с вопросом нам потребуется файл тестирования. Создаем, например, test.php в корне сайта и пишем туда вот это:
// Функция для вывода времени и потребления памяти
function getStatus($text = '') {
    global $memory_start;
    static $microtime_start = null;
    
    if ($microtime_start === null) {$time = 0;}
    else {$time = microtime(true) - $microtime_start;}

    $memory = memory_get_usage();
    if (!empty($memory_start)) {
        $memory2 = number_format(($memory - $memory_start) / 1024, 2,","," ");
        $memory2 = " ($memory2 Кб.)";
    } else {$memory2 = '';}
    $memory = number_format($memory / 1024, 2,","," ");
    
    echo $text.'
<small>время: '.$time.' сек.
память: '.$memory.' Кб.'.$memory2.'</small>

';
    
    $microtime_start = microtime(true);
}

getStatus('Старт');

// Подключаем MODX
define('MODX_API_MODE', true);
require 'index.php';
getStatus('MODX подключен');

// Включаем обработку ошибок
$modx->getService('error','error.modError');
$modx->setLogLevel(modX::LOG_LEVEL_INFO);
$modx->setLogTarget(XPDO_CLI_MODE ? 'ECHO' : 'HTML');
getStatus('MODX готов к работе');

// Потребление памяти на момент готовности MODX
$memory_start = memory_get_usage();
Да, мы будем работать в MODX_API_MODE, чтобы избежать нагруженных шаблонов и прочей возможной шелухи на рабочем сайте. Нас ведь интересует работа фреймворка, да?

Запускаем файл, и видим:
Старт
время: 0 сек.
память: 312,00 Кб.

MODX подключен
время: 0.017445087432861 сек.
память: 2 510,34 Кб.

MODX готов к работе
время: 0.00021815299987793 сек.
память: 2 520,45 Кб.
Все понятно? Запуск и подготовка к работе всего MODX Revolution занимает 2,5 мегабайта памяти и 2 десятитысячных секунды. Все остальное — это время, которое тратит программист.

Данные цифры реальны только при включенном php-apc. Если отключить — потребление памяти и времени будет в 2 раза выше:
Старт
время: 0 сек.
память: 324,34 Кб.

MODX подключен
время: 0.037600021362305 сек.
память: 5 038,35 Кб.

MODX готов к работе
время: 0.00060105323791504 сек.
память: 5 090,96 Кб.
Все дальнейшие цифры и тесты я буду приводить с php-apc. Это фактически стандарт для хостингов, он включен по умолчанию на MODX Cloud и, возможно, войдет в ядро PHP 6.

Все команды мы добавляем в этот тестовый файл и в интересующем месте вызываем getStatus('текстовая метка');.

3 способа работы с БД


Как принято обращаться к Mysql на PHP? Очень просто:
mysql_connect('localhost','root','rootpassword');
mysql_select_db('modx');

$sql = "SELECT * FROM `modx_site_content` WHERE id > 0 LIMIT 100";
$res = mysql_query($sql);
$arr = array();
while ($row = mysql_fetch_assoc($res)) {
    $arr[] = $row;
}
getStatus('Выборка '.count($arr).' ресурсов стандартными функциями Mysql');
Вот так мы получили массив с записями из таблицы. Не очень удобно, да? Зато офигенно быстро: 0,0023038387298584 сек. и 3 873,64 Кб. (1 019,61 Кб.). В скобочках память, скушанная именно этой операцией. Без скобочек — общая, львинную долю которой отожрал MODX.

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

Как через него можно работать в MODX? Еще проще:
$sql = "SELECT * FROM {$modx->getTableName('modResource')} WHERE id > ? LIMIT 100";
$q = $modx->prepare($sql);
$q->execute(array(0));
$arr = $q->fetchAll(PDO::FETCH_ASSOC);
getStatus('Выборка '.count($arr).' ресурсов через PDO');
Результаты: 0,0021560192108154 сек. и 3 989,02 Кб. (1 134,99 Кб.). Разница в районе погрешности, мы так же пишем прямые запросы в БД, ограниченные только нашей фантазией. Однако уже видно прелести PDO: более простой и удобный код и подготовленные выражения, которые защитят нас от инъекций.

Нужно знать, что тот запрос написан для mySQL и с MsSQL, например, может и не работать из-за разницы этих БД. Понятно, что язык там один — SQL, но есть разные ньюансы. А MODX поддерживает MsSQL.

Как же написать наш супер-пупер компонент с разными крутыми запросами, чтобы работало сразу на всех БД, которые умеет MODX? А вот тут нам на помощь приходит xPDO.
$q = $modx->newQuery('modResource');
$q->where(array('id:>' => 0));
$q->limit('100');
if ($q->prepare() && $q->stmt->execute()) {
    $arr = $q->stmt->fetchAll(PDO::FETCH_ASSOC);
}
getStatus('Выборка '.count($arr).' ресурсов через xPDO');
Результаты: 0.00597095489502 сек. и 10 845,90 Кб. (2 398,56 Кб.). Ресурсов ушло больше в 2 раза, но наш запрос теперь и поддерживает 2 базы данных (Mysql и MsSQL), а если кудесники MODX добавят поддержку других БД — то newQuery построит и подготовит запрос и для них.

Нужно понимать, что разница во времени обусловлена генерацией запроса для MySQL методом $modx->newQuery(), что не влияет на скорость выборки. Это значит, что если выбирать много данных, то разница небольшая.
ВыборкаPDOxPDO
500 ресурсов 0.011471033096313 сек.
7 083,84 Кб. (3 994,36 Кб.)
0.017184972763062 сек.
7 849,96 Кб. (4 759,93 Кб.)
1000 ресурсов 0.037826061248779 сек.
11 446,41 Кб. (8 357,57 Кб.)
0.046993970870972 сек.
12 465,15 Кб. (9 375,13 Кб.)

При этом, конечно, вы можете и сами написать драйвер для любой БД и подключить его в свой код. Вы можете сделать корпоративное веб-приложение, к примеру, для госучреждений, которые традиционно работают с целым зоопарком различного ПО. И оно будет поддерживать все БД, которые вам нужны — запросы не придется переписывать каждый раз.

То есть, наш запрос стал гораздо более универсальным и абстрактным. Это, конечно, кушает больше ресурсов, но вас никто не заставляет делать именно так. Есть PDO, его никто не спрятал — пишите все запросы на нем и у вас будет мега-скорость.

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

Зачем еще нужен xPDO?


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

Остальные причины — это удобство, безопасность и целостность данных.

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

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

Сколько вы тратите на хостинг в месяц? Лично я, около 1000 рублей — это 2 часа моей работы на заказ. Поэтому, каждый решает для себя, что лучше — писать код долго, муторно и экономить на железе, или же быстро и удобно — но с дополнительными тратами на сервер.

Напоминаю, что в MODX Revolution возможны оба варианта — выбор за вами. Оптимальный, как обычно, посередине.
Например, я после разработки сайта оптимизирую его узкие места, в том числе, переписывая работу с БД на PDO, где нужно.

Итак, в чем же удобство? А удобство в том, что xPDO предоставляет нам наши данные в виде объектов, с различными методами.
Получая ресурс через $modx->getObject(), вы не просто достаете данные из таблицы — вы получаете экземпляр класса modResource, который обладает уникальными методами, при этом наследуя дополнительные от родителя — xPDOSimpleObject, типа set(), get(), save(), remove(), toArray().

Об этом всегда нужно помнить.

Например:
$res = $modx->getObject('modResource', 1);  // Получаем объект ресурса #1
print_r($res->toArray()); // Печатаем данные самого объекта
echo $res->getTVValue('tvname'); // Выводим данные связанного ТВ параметра по его имени
$res->set('pagetitle', 'Страница 55'); // Меняем заголовок страницы
$res->save(); // Фиксируем изменения
$res->remove(); // Удаляем ресурс
Попробуйте переписать это на PDO, как будет время и сравните, сколько у вас выйдет строк.

А потом почитайте подробнейшее руководство по работе с объектами и их связями на английском или на русском. Прикинули, как работать со всем этим багатствов ручками, прописывая постоянные SELECT, UPDATE, INSERT и DELETE? Проще сразу застрелиться.

С xPDO вы поднимаетесь на совершенно иной уровень работы. Если посмотреть в исходники MODX, то можно увидеть, сколько там различных проверок и наворотов при, казалось бы, простых действиях. Например, при изменении какого-то поля ресурса, присланные данные приводятся к виду, который требует модель это объекта. Если вы пришлете в поле parent строку «15», то она превратится в целое 15, понятно?

Внимательная система делает огромное количество работы за вас, предоставляя вам возможность просто творить, не думая о типах данных и SQL инъекциях. Вы мыслите иными категориями: объектом, его методами и связями с другими объектами.

При удалении ресурса будут удалены все объекты, подчиненные ему, типа значений ТВ параметров. Если вы удаляете контекст — будут удалены все ресурсы и значения ТВ параметров этого контекста. Вам не нужно за этим следить самому.

Вот вам безопасность и целостность данных. Если вы правильно написали модель своего расширения, расписали все связи — xPDO сам позаботится о актуальности данных. Понятно, что в объектах MODX все прописано как надо.

Это и есть ORM, если кратко. Методы getObject(), getCollection() и другие нужно применять не от балды, а строго осознавая, что сейчас вам нужны именно объекты, а не просто данные из таблицы БД.
Например, вы знаете, что вместо getCollection() можно применять getIterator(), который достает не сразу все подходящие объекты, за раз, а по очереди — в цикле? Экономия памяти, между прочим.

Если вам нужно вывести 100 товаров на странице, очевидно, что лучше использовать PDO — будет быстрее и вы не производите никаких манипуляций, только показываете содержимое таблицы БД. А вот если нужно выбрать 10 ресурсов и изменить у них родителя — тут удобнее xPDO.

То есть, нужно понимать, что именно вы делаете.

Заключение


MODX Revolution, как и xPDO — это огромный чемодан с инструментами на все случаи жизни. Это фреймворк, который по старой привычке косит под CMS. И конечный результат зависит только от умений и знаний программиста.

Хотите мега скорости — используйте PDO, или даже другой фреймворк. Хотя, чистый PHP будет гораздо быстрее. Но даже он не сравнится со статичным HTML. Намек ясен?

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

Подходить к ORM с мерками прямых запросов в БД просто нельзя — это разные вещи, для разных задач. Вы не просто работаете с БД — вы работаете с объектом своей системы, который имеет связи с другими объектами.

MODX кушает от 2,5 до 6 мегабайт ОЗУ, и тратит 2-4 десятитысячных секунды на инициализацию. Все остальное время и память — на вашей совести.

Надеюсь, после этой заметки у многих прояснится в голове.
Василий Наумкин
30 октября 2012, 18:36
modx.pro
18
16 899
0

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

Иван Брежнев
30 октября 2012, 23:31
0
Красава ))
    Григорий Розенбаум
    02 ноября 2012, 22:43
    0
    Привет, Василий!

    Вопрос не совсем по теме именно xPDO, скорее по APCCache. Правильно ли я понимаю, что если на сервере доступен APC и в настройках revo указан нужный обработчик (cache.xPDOAPCCache), то скрипт (сниппет) корректно работающий с xPDOFileCache будет работать и с APC?

    Проблема собственно вот в чем — есть интернет-магазин на шаред хостинге, php5.3 + APC. Используется modx revo+ shopkeeper. Изначально про обработчики не знал, использовался обычный файл кеш, но работало все неправильно (корзина не обновлялась, не добавлялись товары) до тех пор, пока я не прописал в htaccess php_flag apc.cache_by_default Off. Заработало, но временами страница загружалась долго (причем страница с древовидным меню, ибо вывод товаров происходит сниппетом, не использующем xPDO вообще).
    Потом наткнулся на вашу статью, прописал обработчик, убрал флаг в htaccess. Грузиться все стало заметно быстрее, но вот корзина по-прежнему не хочет работать как надо. Сейчас apc.cache_by_default выключено, но обработчик прописан для apc. Нужны ли какие-то доработки в скрипт чтобы он работал с cache by default ON? Или может быть поработать с apc.filters и каким-то образом прописать туда, что нужно кешировать а что нет (как это сделать — это следующий вопрос)))
    php знаю хреновато, хотя базовые навыки ООП и программирования вообще есть, поэтому и требуется наводка более опытных товарищей)

    Заранее спасибо!
      Alexey
      05 ноября 2012, 21:11
      0
      Для полноты картины не хватает данных о выводе через rowboat
        Василий Наумкин
        09 ноября 2012, 21:19
        0
        Ни разу не пользовался.

        Читал, вроде полезная штука, но руки так и не дошли. Проще свой скриптик накидать за 5 минут.
        СикретНаме
        14 ноября 2012, 13:39
        0
        «Тому парню с того форума» не закидывали статью эту? Думаю, он сдулся бы вконец.

        Хотя, конечно. справедливости ради я бы и сам задал вопрос, единственный в его перлах существенный. Действительно, а почему именно жёстко вшили xPDO, а не факультативно? Мне именно любопытно, в чём могла быть серемяга (точно знаю, что разрабы MODX не идиоты, как тот парень думает, и понимали, что делают (изначально не исключаю, что для таких вещей факультатив нереален (например, по безопасности) и, следовательно, надо жёстко вшивать)).
          Василий Наумкин
          14 ноября 2012, 13:58
          0
          Того парня, как нашкодившего котенка, я носом тыкаю в эту заметку — он все равно делает вид, что ничего не видел.

          А объяснение простое, и очевидное — без xPDO не было бы Revolution вообще. Был бы Evolution с обвесами, но не более.

          Чем глубже разбираюсь в Revo — тем больше, простите, охуеваю от возможностей.

          К примеру, сейчас мы пишем каменты к ресурсу класса не modResource, а Ticket. Среди прочего, отличается он еще и тем, что в любом случае при получении контента превращает теги MODX в html-сущности.
          А при выводе контента у себя на странице — прогоняет его через Jevix, не только фильтруя, но и типографируя.

          То есть, нефильтрованным контент этого ресурса можно получить только через прямой запрос в БД. Есть еще гора отличий в создании\изменении такого ресурса, и в админке.

          Однако, я про них не думаю, ибо создал один раз этот новый класс ресурса — и дальше он работает по моим правилам, наследуя остальное от родителя modResource, а тот от xPDOSimpleObject, а потом xPDOObject… Ну вы поняли.

          Как, ну как это сделать без ОРМ?! Короче, такой вопрос могут задавать только те люди, которые в Рево не ушли дальше использования стандартных сниппетов в Рево.
            СикретНаме
            14 ноября 2012, 14:19
            0
            На всякий случай, я не имел ввиду, что можно делать такие и этому подобные кульбиты без ОРМ :0)

            "… без xPDO не было бы Revolution вообще. Был бы Evolution с обвесами, но не более." — а и не надо, был бы много более крутой Evo, не без доли здравого смысла сказал бы «тот парень с того форума». Однако, моё предположение ниже (если оно ± верное), основанное на Ваших знаниях = перефразированный Ваш ответ «добивает хлопца».

            Итак, суть вопроса «почему нельзя подключать/не подключать xPDO».

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

            Если это ± так, «тот парень с того форума» окончательно «кончился», ибо ± вполне чёткий и понятный ответ на его единственно потенциально существенный и даже ключевой вопрос достаточно полон.

            П.С.
            Повторюсь, это так только, если в целом я правильно понял Вас.

            П.П.С.
            Для меня этот момент важен, т.к. не только «тот парень...» гнёт такую линию «последним рубежом обороны», а я особо ничего и ответить не мог.
              Василий Наумкин
              14 ноября 2012, 14:26
              0
              Авторы MODX мне не рассказывали, почему они решили именно так, но копаясь во внутренностях их системы — я не представляю, как можно иначе.

              Упертым баранам давайте ссылку на эту заметку — тут показано, как быстро работать с БД, без ненавистного ОРМ.
              Кому кажется долгим сама инициализация ядра MODX — дорога в чистый php или еще куда подальше.
          Ренат Гареев
          13 мая 2013, 16:37
          0
          Скажите, пожалуйста, а xPDO может выполнить UPDATE или DELETE — запрос? Никак не получается найти живой пример. Я хотел написать скрипт, который бы массово менял значения полей в БД, но в итоге решение было таким:
          $q=$modx->newQuery('modResource');
          $q->select(array(
          		'id',
          		'content'
          	)
          );
          
          $results = $modx->getCollection('modResource', $q);
          
          foreach ($results as $r)
          {
          	$newcontent = '';
          	$temp = explode('[~',$r->content);
          	if (count($temp) > 1)
          	{
          		$newcontent = str_replace('[~','[[~',$r->content);
          		$newcontent = str_replace('~]',']]',$newcontent);
          
          		$sql = "UPDATE modx_site_content SET content = '".$newcontent."' WHERE id = '".$r->id."'";
          		$query = $modx->prepare($sql);
          		$query->execute();
          	}
          }
          Mykhajlo Tsymbala
          17 июня 2013, 09:48
          0
          посоветуйте пожалуйста как зделать выборку с таблицы БД чтобы она отображалась в выпадающемся списке
          в место масива
          Cabinets.combo.Region = function(config) {
              config = config || {};
              Ext.applyIf(config,{
                  store: new Ext.data.ArrayStore({
                      id: 0
                      ,fields: ['region','display']
                      ,data: [
                          ['MB','Megabyte']
                          ,['GB','Gigabyte']
                          ,['TB','Terabyte']
                          ,['PB','Petabyte']
                          ,['EB','Exabyte']
                          ,['ZB','Zettabyte']
                          ,['YB','Yottabyte']
                      ]
                  })
                  ,mode: 'local'
                  ,displayField: 'display'
                  ,valueField: 'region'
              });
              Cabinets.combo.Region.superclass.constructor.call(this,config);
          };
          Ext.extend(Cabinets.combo.Region,MODx.combo.ComboBox);
          Ext.reg('cabinets-combo-region',Cabinets.combo.Region);
            Вадим Чесноков
            10 февраля 2014, 05:53
            0
            Никак не пойму, как подсунуть xPDO UPPER(), когда пишем в JSON формате наримр аот сдесь:
            $params['where']='{"longtitle:LIKE": "%'.$str_upper.'%"}';
            $result.="<table id=\"table\" border=\"1\" width=\"100%\">".$modx->runSnippet('getPage', $params);
              Василий Наумкин
              10 февраля 2014, 09:56
              0
              Зачем?
              MODX создаёт регистронезависимые таблицы, по умолчанию.

              Ну а вообще, в where можно указывать чистый SQL:
              &where=`["UPPER(longtitle) LIKE '%ЗНАЧЕНИЕ%'"]`
                Вадим Чесноков
                10 февраля 2014, 13:13
                0
                Да вот чёрт меня дернул при установке выставить UTF8_bin. А переконвертировать в UTF8_general_ci боязно, вдруг чего пропадёт. Жалко.
                  Василий Наумкин
                  10 февраля 2014, 13:26
                  0
                  Ничего не пропадёт, если сделать сначала бэкап таблицы.

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