Пользовательские запросы к БД в MODx Revolution

    Данный топик наверняка будет полезен тем, у кого довольно большие проекты на MODx Revolution, так как с обычными сайтами-визитками достаточно и стандартных методов работы а-ля $modx->getObject(), $modx->getCollection() и т.п. И данные методы по сути своей не просто работа с базой данных, а еще и с объектами MODx.

    При работе с большими проектами методы а-ля $modx->getCollection() для нас не лучшее решение по двум причинам:
    1. Перерасход ресурсов. Данные методы не просто получают данные из БД, но еще и создают инстанции получаемых объектов. В данном случае получая информацию о 10000 документов, мы получаем 10000 объектов modResource, что не очень круто.
    2. Осложняется задача подсчета получаемых записей. Помимо прямых сложностей подсчета еще на уровне запроса, даже если вы получите 10 записей одного и того же документа (к примеру), MODx вернет вам как результат только один объект modResource. И хотя часто такое устроит многих программистов (они получили уникальные объекты и рады), кого-то это не устроит, так как опять же происходит перерасход ресурсов, а по конечному результату сразу и не видно, что запрос не оптимизированный.
    К тому же при работе на крупных проектах нам чаще всего нужны не сами объекты, а только информация (записи из базы данных).

    Описанные здесь методы работы с БД ставят 2 задачи:
    1. Дать бОльшую гибкость в написании запросов к БД.
    2. Придерживаться стандартных методов xPDO, то есть избежать чистого SQL, так как чистый SQL по некоторым причинам в фреймворках вообще не кашерно (хотя бы с точки зрения возможной миграции на другой тип БД, смены названий таблиц, префиксов или еще чего-нибудь)

    Итак, к делу.

    Для начала нам необходимо освоить важный метод
    $modx->newQuery($class);


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

    Вот более развернутый пример:
    $q = $modx->newQuery('modResource');
    $q->where(array(
    	'context_key' => 'web'
    ));
    $result = $modx->getCollection('modResource', $q);
    


    В данном случае $q — это часто встречающаяся нам в документации так называемая criteria.
    Это почти что тоже самое, что и where, когда мы передаем его в качестве второго параметра, только более мощный инструмент, так как у него много важных методов типа Sortby, leftJoin, innerJoin, Limit и другие.

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

    Итак, несколько переделаем наш запрос.
    $q = $modx->newQuery('modResource');
    $q->where(array(
    	'context_key' => 'web'
    ));
    $q->prepare();
    $sql = $q->toSQL();
    


    Вот здесь мы уже получим чистый SQL, что наверняка много кому понадобится.
    В данном примере мы увидели еще один важный метод
    $q->prepare();

    Он как раз и готовит конечный SQL.

    Теперь же мы можем выполнить этот SQL
    $q = $modx->newQuery('modResource');
    $q->where(array(
    	'context_key' => 'web'
    ));
    
    $q->limit(10);    // Добавим лимит записей
    
    $q->prepare();
    $sql = $q->toSQL();
    
    $query = $modx->prepare($sql);
    $query->execute();
    
    $result = $query->fetchAll(PDO::FETCH_ASSOC);
    print_r($result);


    UPD: этот пример оставлю в качестве демонстрашки $modx->prepare($sql);, но сразу за этим смотрите исправленный пример с одним вызовом ->prepare();
    $q = $modx->newQuery('modResource');
    $q->where(array(
    	'context_key' => 'web'
    ));
    
    $q->limit(10);    // Добавим лимит записей
    
    $q->prepare();
     
    $q->stmt->execute();
    
    $result = $q->stmt->fetchAll(PDO::FETCH_ASSOC);
    print_r($result);


    На выходе мы как раз и получим массив данных.
    Но колонки будут иметь не совсем удачные названия а-ля
    [modResource_id] => 0
    [modResource_type] => document

    Чтобы было понятней, добавим явный SELECT в запрос.
    $q = $modx->newQuery('modResource');
    $q->where(array(
    	'context_key' => 'web'
    ));
    $q->select(array(
       'modResource.*'
    ));
    $q->limit(10); 
    
    $q->prepare();
    $q->stmt->execute();
    
    $result = $q->stmt->fetchAll(PDO::FETCH_ASSOC);
    print_r($result);


    Вот теперь все хорошо с именами колонок :-)

    А теперь довольно полезный и наглядный пример: Получим 1/10 записей со сдвигом 1/20 и упорядочим по ID.
    $q = $modx->newQuery('modResource');
    $q->where(array(
    	'context_key' => 'web'
    ));
    $q->select(array(
       'modResource.*'
    ));
    
    // Подсчитываем общее число записей
    $total = $modx->getCount('modResource', $q);
    
    // Устанавливаем лимит 1/10 от общего количества записей
    // со сдвигом 1/20 (offset)
    
    $q->limit($total / 10, $total / 20);   
    
    // И сортируем по ID в обратном порядке
    $q->sortby('id', 'DESC');
    
    $q->prepare();
    $q->stmt->execute();
    
    $result = $q->stmt->fetchAll(PDO::FETCH_ASSOC);
    print_r($result);


    Кстати, довольно легко эти примеры переделать так, чтобы получить конечные объекты.

    
    $q = $modx->newQuery('modResource');
    $q->where(array(
        'context_key' => 'web'
    ));
    $q->select(array(
       'modResource.*'
    ));
    
    // Подсчитываем общее число записей
    $total = $modx->getCount('modResource', $q);
    
    // Устанавливаем лимит 1/10 от общего количества записей
    // со сдвигом 1/20 (offset)
    
    $q->limit($total / 10, $total / 20);   
    
    // И сортируем по ID в обратном порядке
    $q->sortby('id', 'DESC');
    
    $q->prepare();
    
    // Получаем объекты
    $docs = $modx->getCollection('modResource', $q);
    


    Суть методов $modx->getObject() и $modx->getCollection() заключается в том, чтобы получив данные из БД, инициировать указанный класс и набить в него полученные данные методом $object->fromArray($array());

    Кстати, настоятельно не советую играться с print_r($docs);, так как результат методов а-ля $modx->getCollection() — массив указанных объектов, каждый из которых является расширенным объектом xPDO и MODx вместе взятых, то есть это ооочень много информации.
    Потому для вывода информации из объектов используйте метод $object->toArray();
    В данном случае примерно так:
    foreach($docs as $doc){
    	print_r($doc->toArray());
    }


    Еще на заметку: элементы в массиве объектов MODx перечислены не по порядку, а каждый ключ — ID объекта (записи), потому вы не можете наверняка обратиться к 11-му элементу через $docs[10], так как документ с ID 10 может оказаться 1-ым, или 100-ым, или его может вообще не быть, хотя в массиве будет 100 документов.
    Можете сами убедиться, переделав вывод как
    foreach($docs as $id => $doc){
    	print "<br />". $id;
    }


    Для работы с массивами объектов MODx очень полезно изучить методы работы с элементами массивов, описанных здесь.

    end() — Устанавливает внутренний указатель массива на его последний элемент
    key() — Выбирает ключ из массива
    each() — Возвращает текущую пару ключ/значение из массива и смещает его указатель
    prev() — Передвигает внутренний указатель массива на одну позицию назад
    reset() — Устанавливает внутренний указатель массива на его первый элемент
    next() — Передвигает внутренний указатель массива на одну позицию вперёд

    К примеру, если мы хотим получить первый элемент из массива MODx, никак нельзя обращаться $doc = $docs[0]; В 99,9% вы ничего не получите, так как записи с ID = 0 практически никогда не используются.
    Правильно обратиться так: $doc = current($doc);

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

    P.S. Небольшой сборный запрос с парой полезных фильтров.
    Получим все настройки из контекстов WEB и MGR, значения которых IS NOT NULL и != ''

    $q = $modx->newQuery('modContext');
    
    $where = array(
       'modContext.key:in' => array('web', 'mgr'),
       'cs.value:!=' => NULL,
       'cs.value:!=' => '',
    );
    $q->select(array(
       'modContext.key',
       'cs.key as setting_key',
       'cs.value'
    ));
    
    $q->innerJoin('modContextSetting', 'cs', 'cs.context_key = modContext.key');
    
    $q->where($where);
    
    $q->prepare();
    $q->stmt->execute();
    
    $result = $q->stmt->fetchAll(PDO::FETCH_ASSOC);
    print_r($result);
    
    Поделиться публикацией
    Ммм, длинные выходные!
    Самое время просмотреть заказы на Фрилансим.
    Мне повезёт!
    Реклама
    Комментарии 12
    • +2
      Спасибо за материал, по работе с xPDO его не так много на русском
    • +2
      Хочу немного дополнить — пара фокусов с xPDO.
      Обратите внимание на скорость и потребление памяти у разных запросов.

      На xPDO можно работать очень быстро, только не все об этом знают.

      Кстати, вместо getCollection() можно использовать getIterator() — меньше жрет памяти.
      • 0
        Опять небольшой лаг с ответами.
        Чуть ниже написал.
      • +1
        Василий, спасибо!
        Знал, что можно чуть более изящно написать, но что-то не докопал. А то у меня два раза prepare() двух объектов делается, хотя можно в одном все сделать.
        Оставлю прошлый вариант как демонстрашка и перепишу с учетом $q->stmt->
        • 0
          Лучше делать так
          ...
          $q->prepare();
          
          if($q->stmt && $q->stmt->execute()){
              $result = ...
          }
          


          Вообще очень полезная статья.
          • +1
            Да, с вами согласен, что проверки — очень хорошо, и всегда для себя пишу с проверками, но не хотелось в примерах отдельные шаги захламлять дополнительными конструкциями.
            Наверняка всякий уважающий себя программист для себя проверки напишет.
          • 0
            что насчёт ускорения $modx->getChunk? Делаю обычным способом запрос (getObject->getMany->getMany — нужно вывести дерево такое же, как в админке занесено) — запрос выполняется ~1.5 секунд, при включенном кеше — это хороший результат, но как только добавляю getChunk получаю результат ~40 секунд, что очень плохо даже с кешем (кеш nginx сбрасывается каждый час и кому-то «невезёт»).
            • 0
              оказывается была проблема в том, что я «комментировал» html куски через [[ <html/> ]] (то есть modx не находил такой снипет и ничего не отображал)
              • 0
                Честно скажу: тут ничем не помогу. С этим не сталкивался.
                • НЛО прилетело и опубликовало эту надпись здесь
                • 0
                  getChunk нормально по скорости. Эта функция кэширует чанк, а не делает каждый раз запросы.

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