В продолжение предыдущего топика про сложные SQL-запросы, хотел бы поделиться свежим опытом.
Сегодня на нашем сайте Клуба я решил сделать одну полезную штуку для себя — на страницах топиков и тегов выводить информацию о сопутствующих услугах. ? Справедливости ради сразу замечу, что данная «реклама» выводится только топиках, написанных мною, и не паразитирует на чужих топиках.
В целом все логично: есть какая-то статья, описывающая произвольную проблему, и есть соответствующая этой проблеме услуга. К примеру, проблема «тормозит сайт», услуга «техническая оптимизация сайта». Собственно, все что здесь нужно — это прописать желаемые теги в документах услуг, а при заходе на страницу мы выполняем поиск этих услуг по тегам текущего документа-топика (а теги у нас заполнены во всех топиках). И вот здесь есть ряд тонкостей, о которых я и расскажу ниже.
1. Дополнительное поле тегов в услугах. Это обычное TV-поле «Список (множественный выбор)». В поле Возможные значения прописываем SQL-запрос.
@SELECT DISTINCT tag FROM `[[+PREFIX]]society_topic_tags` ORDER BY 1
Это позволит формировать всегда актуальный список тегов. ? К слову, список тегов — почти 900 штук, но в админке тормоза с этим полем не замечены (хотя не знаю как на слабых тачках будет себя вести).
Указываем нужные теги и сохраняем. Далее остается только прописать поиск тих услуг по тегам. И вот здесь начинается интересное… Значение данной TV-шки записывается в БД одной строкой с разделителем ||, к примеру оптимизация||производительность||тормозит сайт||нагрузка на сервер. И все бы ничего, да только поиск по этой строке нам надо выполнить как правило тоже не одного тега, а нескольких, к примеру оптимизация, тормозит сайт, нагрузка на сервер. Ясное дело, что порядок тегов не всегда будет совпадать. Еще раз уточню задачу: нам надо найти все документы, в которых будет найден хоть один из перечисленных тегов.
Итак, вот конечный код (сразу для вставки в getdata-процессор):
<?php public function prepareQueryBeforeCount(xPDOQuery $c){ $c = parent::prepareQueryBeforeCount($c); $alias = $c->getAlias(); $where = array(); // Поиск по тегу if($tags = trim($this->getProperty('tags'))){ $tags_arr = explode(',', $tags); $tags_arr = array_map('trim', $tags_arr); $tags_arr = array_map(function($str){ global $modx; $str = $modx->quote($str); $str = "FIND_IN_SET({$str}, replace(tags_tv.value, '||', ','))"; return $str; }, $tags_arr); $find_in_set = implode(' OR ', $tags_arr); $c->innerJoin('modTemplateVarResource', "tags_tv", "tags_tv.tmplvarid = 18 AND tags_tv.contentid = {$alias}.id AND ({$find_in_set})"); } if($where){ $c->where($where); } return $c; } Как видно, здесь я передаваемую строку разбиваю по запятой в массив и набиваю в итоге вот в такую хитрую строку: FIND_IN_SET('оптимизация', replace(tags_tv.value, '||', ',')) OR FIND_IN_SET('тормозит сайт', replace(tags_tv.value, '||', ',')) OR FIND_IN_SET('нагрузка на сервер', replace(tags_tv.value, '||', ','))
Что здесь происходит? 1. replace(tags_tv.value, '||', ',') заменяет в TV-значении разделитель || на разделитель, (запятая). К сожалению, в этой ТВшке нельзя указать тип разделителя для записываемого значения. 2. FIND_IN_SET выполняет поиск заданного значения в строке с разделителем. В данной строке разделитель-запятая как бы разбивает строку на массив, в котором можно выполнять поиск с четким вхождением.
Ну а далее мы уже джоиним TV-поле с этим условием. Полный итоговый запрос выглядит примерно так:
SELECT modResource.* FROM `modx_site_content` AS `modResource` JOIN `modx_site_tmplvar_contentvalues` `tags_tv` ON tags_tv.tmplvarid = 1 AND tags_tv.contentid = modResource.id AND ( FIND_IN_SET('оптимизация', replace(tags_tv.value, '||', ',')) OR FIND_IN_SET('тормозит сайт', replace(tags_tv.value, '||', ',')) OR FIND_IN_SET('нагрузка на сервер', replace(tags_tv.value, '||', ',')) ) WHERE ( `modResource`.`deleted` = 0 AND `modResource`.`hidemenu` = 0 AND `modResource`.`published` = 1 )
Опять-таки статистики ради: в базе 1500 документов и 1700 записей TV-полей, выборка выполняется практически мгновенно.
Вопрос непосредственно modx не касается, но, возможно, в описанном поведении виноват именно он, возможно, нет. В файле .htaccess у всех прописаны следующие директивы:
RewriteEngine On RewriteBase / RewriteCond %{REQUEST_FILENAME} !-f RewriteCond %{REQUEST_FILENAME} !-d RewriteRule ^(.*)$ index.php?q=$1 [L,QSA]
Что здесь происходит — всем понятно. Предположим, на уровне Apache необходимо выполнить внутренний редирект с URI test.html на resource0.html (пример сильно упрощён, чтобы можно было сосредоточиться на проблеме).
Решить задачу можно 2 способами: 1.Перед стандартным редиректом modx (index.php?q=$1) прописать директивы:
RewriteCond %{REQUEST_FILENAME} !-f RewriteCond %{REQUEST_FILENAME} !-d RewriteRule ^test\.html$ resource0.html [QSA]
Здесь флаг [L] мы не ставим, чтобы после нашего редиректа сработал стандартный редирект modx (index.php?q=$1)
Загружаем в браузере URL site.ru/test.html Получаем: внешний (301) редирект на URL site.ru/resource0.html
2.Перед стандартным редиректом modx (index.php?q=$1) прописываем общий редирект с test.html на index.php?q=resource0.html
RewriteCond %{REQUEST_FILENAME} !-f RewriteCond %{REQUEST_FILENAME} !-d RewriteRule ^test\.html$ index.php?q=resource0.html [L,QSA]
Здесь указываем флаг [L], хотя и не обязательно, т.к. стандартный редирект modx после этого редиректа так и так не сработает.
Загружаем в браузере URL site.ru/test.html Получаем: внешний (301) редирект на URL site.ru/resource0.html
Итог: В обоих случаях получаем редирект на нужную нам страницу (resource0.html), но редирект выполняется внешний, а не внутренний. Почему ?
P.S. Для воспроизведения описанной ситуации достаточно изменить site.ru на имя своего сайта, resource0.html — на URI любого существующего ресурса.
Я кинул ссылку на фронт, а не на админку. Смотрите Личный кабинет -> Управление заказами. Но можно (а может даже лучше, так как эти процессоры более новые) использовать и basket/mgr/orders/products/getdata, передав в него order_id, если знаете ID заказа.
Здравствуйте, Николай. Насколько я знаю в админке всё extjs-ом рендерится, а я в нём совсем не разбираюсь, поэтому ищу способ решить вопрос без него. Но за наводку, спасибо, Коля.
Эм… А почему было не посмотреть как рендерится страница управления заказами в личном кабинете? /office/orders.html
Приветствую. Возник вопрос как вывести в личном кабинете состав каждого заказа, так же как в админке видит менеджер по заказам. Нашёл в составе сборки процессор basket/mgr/orders/products/getdata, вызвал его, получил массив объектов и потом путём фильтрации по id заказа, распихал товары по заказам. Думал всё задача решена, но на продакшене не завелось, оказалось там у процессора по умолчанию лимит на 20 стоял, а сам процессор вызывает всю корзину со всеми заказами всех клиентов за всё время, поднял лимит на 10 000 пока всё работает, но когда эта планка будет перебита, не все заказы можно будет развернуть и увидеть состав. Вот собственно вопрос, с какими параметрами можно его вызвать, что получить выборку составов заказов только того пользователя, который сейчас в личном кабинете. Или я совсем не в те дебри полез и есть гораздо более простое решение, прошу совета у сообщества, помогите, кто разбирается. Да и вот ссылка на сайт ac-t.ru на всякий случай.
Такое в документации не описано. А FIND_IN_SET() вообще в списке разрешенных методов не числится.
Искал по запросам в документации, толком не нашел, или искал криво что ли. Хорошо написал, понятно.
Вчера на modx.pro был найден довольно интересный вопрос. Пока я писал там ответ, понял, что его имеет смысл вынести в отдельный топик, так как он получился довольно объемный и раскрывает несколько тонких моментов. Крайне советую к изучению практикующим xPDO.
Топикстартер абсолютно прав:
у меня возникает ощущение, что я пытаюсь найти строки в которых значение ps_type одновременно равно и «Подвесной», и «Административный», что, закономерно, ни к чему не приводит.
Да, нельзя сделать выбоку с условием, которое требует разных значений от одной и той же строки. Но и условие OR тоже нельзя использовать, так как это приведет к выборке в том числе и тех товаров, у которых имеется только “Административный”, но нет других необходимых значений.
Предложенный Сергеем запрос годится, но только на выборку одного типа товаров, в данном случае «Подвесной, Административный». Но сразу выбрать еще и «Административный, Промышленный, Подвесной» он не позволит. (На самом деле в данном случае конечно же позволит, ибо вхождение “Подвесной, Административный” имеется и в “Административный, Промышленный, Подвесной”, но очевидно, что топикстартер просто не удачный пример привел, и что надо искать товары, у которых значения не пересекаются). Придется выполнять столько выборок, сколько типов товаров найти надо будет.
Если говорить о реализации запроса именно с этой структурой данных, сначала придется сгруппировать эти данные, а потом уже по ним выполнить поиск, а потом еще и подзапрос выполнить. А вот подзапросы на уровне xPDO в принципе только через хаки выполняются.
Вот попробуйте в консоли выполнить этот код:
$q = $modx->newQuery('msProduct'); $alias = $q->getAlias(); $q->select(array( "{$alias}.*", )); $q2 = $modx->newQuery('msProductOption'); $alias2 = $q2->getAlias(); $q2->select(array( "product_id", "group_concat(value) as `values`", )); $q2->groupby('product_id'); $q2->prepare(); $sql2 = $q2->toSQL(); // print "<br />" . $sql2; $q->query['where'][] = new xPDOQueryCondition( array( 'sql' => "EXISTS ( SELECT NULL FROM ({$sql2}) as t WHERE t.product_id = {$alias}.id AND ( (FIND_IN_SET('Административный', `values`) AND FIND_IN_SET('Подвесной', `values`)) OR (FIND_IN_SET('Административный', `values`) AND FIND_IN_SET('Промышленный', `values`) AND FIND_IN_SET('Подвесной', `values`)) ) )" ) ); $s = $q->prepare(); $sql = $q->toSQL(); print "<br />" . $sql . "\n"; // $s = $modx->prepare($sql); $s->execute(); print_r($s->errorInfo()); while($row = $s->fetch(PDO::FETCH_ASSOC)){ print_r($row); }


Этот код как раз выполняет то, вам нужно. На выходе будет получен SQL типа такого:
SELECT msProduct.* FROM `modx_site_content` AS `msProduct` WHERE EXISTS ( SELECT NULL FROM (SELECT `product_id`, group_concat(value) as `values` FROM `modx_ms2_product_options` AS `msProductOption` GROUP BY product_id ) as t WHERE t.product_id = msProduct.id AND ( (FIND_IN_SET('Административный', `values`) AND FIND_IN_SET('Подвесной', `values`)) OR (FIND_IN_SET('Административный', `values`) AND FIND_IN_SET('Промышленный', `values`) AND FIND_IN_SET('Подвесной', `values`)) ) )

И если он найдет нужные товары, то ниже выведет их в результат.
К сожалению, упростить этот запрос с текущей структурой вряд ли получится существенно, и не знаю можно ли такое выполнить через msProducts (только если в него можно передавать объект xPDOQuery или чистый SQL). Вариантов спасения видится два:
1. Использовать TV «выпадающий список множественный выбор». Он в БД складывает значения вида Значение1||Значение2||Значение3… Тогда можно будет передать в вызов условие типа такого:
print '<pre>'; $q = $modx->newQuery('msProduct'); $alias = $q->getAlias(); $q->select(array( "{$alias}.*", )); $tv_id = 1; $q->innerJoin('modTemplateVarResource', 'ps_type', "ps_type.tmplvarid = {$tv_id} AND ps_type.contentid = msProduct.id AND ( (FIND_IN_SET('Значение1', replace('||', ',', ps_type.value)) AND (FIND_IN_SET('Значение2', replace('||', ',', ps_type.value)))) OR (FIND_IN_SET('Значение1', replace('||', ',', ps_type.value)) AND (FIND_IN_SET('Значение2', replace('||', ',', ps_type.value)) AND FIND_IN_SET('Значение3', replace('||', ',', ps_type.value)))) )"); $s = $q->prepare(); $sql = $q->toSQL(); print "<br />" . $sql . "\n"; $s->execute(); print_r($s->errorInfo()); while($row = $s->fetch(PDO::FETCH_ASSOC)){ print_r($row); }
Вот этот вариант уже вполне годится, чтобы передать параметром в вызов msProducts.
2. Менее удобно, но тоже вполне подходящий вариант: делать выборку id-шников подходящих товаров на уровне чистого SQL-запроса (выдернув необходимое из примера выше) и эти id-шники уже передавать в вызов msProducts. Так получится гораздо меньше чистого SQL-ля, который все-таки сложнее в обслуживании, чем запросы на базе xPDO.
UPD: По умолчанию длина строки, формируемой методом GROUP_CONCAT(), ограничена 1024 символами. Из-за этого может резаться результирующее значение. Чтобы увеличить на лету это значение, перед выполняемым запросом делаем так:
$s = $this->modx->prepare('SET SESSION group_concat_max_len = 1000000;'); $s->execute();
Здравствуйте, посмотрите тут: habrahabr.ru/post/206112/ habrahabr.ru/post/144517/