Использование WP_Query для выборки нескольких категорий с ограничением постов на категорию?
У меня есть 3 категории, в каждой по 15 постов. Мне нужно сделать ОДИН запрос к базе данных, который вернет только первые 5 постов из каждой категории. Как это можно реализовать?
$q = new WP_Query(array( 'post__in' => array(2,4,8), 'posts_per_page' => **ПЕРВЫЕ_5_ИЗ_КЖДОЙ_КАТЕГОРИИ** ));
Если это невозможно, что будет эффективнее: получить все посты родительской категории и фильтровать их в цикле или создать 3 отдельных запроса?

То, что вам нужно, возможно, но потребует погружения в SQL, чего я стараюсь избегать, когда это возможно (не потому, что я не знаю SQL — я продвинутый SQL-разработчик, а потому что в WordPress лучше использовать API, когда это возможно, чтобы минимизировать будущие проблемы совместимости, связанные с потенциальными изменениями структуры базы данных).
SQL с оператором UNION
— это вариант
Чтобы использовать SQL, вам понадобится оператор UNION
в вашем запросе. Например, вот так (предполагая, что slugs ваших категорий — это "category-1"
, "category-2"
и "category-3"
):
SELECT * FROM wp_posts WHERE ID IN (
SELECT tr.object_id
FROM wp_terms t
INNER JOIN wp_term_taxonomy tt ON t.term_id = tt.term_id
INNER JOIN wp_term_relationships tr ON tt.term_taxonomy_id = tr.term_taxonomy_id
WHERE tt.taxonomy='category' AND t.slug='category-1'
LIMIT 5
UNION
SELECT tr.object_id
FROM wp_terms t
INNER JOIN wp_term_taxonomy tt ON t.term_id = tt.term_id
INNER JOIN wp_term_relationships tr ON tt.term_taxonomy_id = tr.term_taxonomy_id
WHERE tt.taxonomy='category' AND t.slug='category-2'
LIMIT 5
UNION
SELECT tr.object_id
FROM wp_terms t
INNER JOIN wp_term_taxonomy tt ON t.term_id = tt.term_id
INNER JOIN wp_term_relationships tr ON tt.term_taxonomy_id = tr.term_taxonomy_id
WHERE tt.taxonomy='category' AND t.slug='category-3'
LIMIT 5
)
Можно использовать SQL UNION с фильтром posts_join
Используя вышеуказанное, вы можете просто выполнить запрос напрямую или использовать фильтр posts_join
, как показано ниже. Обратите внимание: я использую PHP heredoc, поэтому убедитесь, что SQL;
находится строго слева. Также обратите внимание, что я использовал глобальную переменную, чтобы вы могли определить категории вне хука, перечислив их slugs в массиве. Этот код можно поместить в плагин или в файл functions.php
вашей темы:
<?php
global $top_5_for_each_category_join;
$top_5_for_each_category_join = array('category-1','category-2','category-3');
add_filter('posts_join','top_5_for_each_category_join',10,2);
function top_5_for_each_category_join($join,$query) {
global $top_5_for_each_category_join;
$unioned_selects = array();
foreach($top_5_for_each_category_join as $category) {
$unioned_selects[] =<<<SQL
SELECT object_id
FROM wp_terms t
INNER JOIN wp_term_taxonomy tt ON t.term_id = tt.term_id
INNER JOIN wp_term_relationships tr ON tt.term_taxonomy_id = tr.term_taxonomy_id
WHERE tt.taxonomy='category' AND t.slug='{$category}'
LIMIT 5
SQL;
}
$unioned_selects = implode("\n\nUNION\n\n",$unioned_selects);
return $join . " INNER JOIN ($unioned_selects) categories ON wp_posts.ID=categories.object_id " ;
}
Но могут быть побочные эффекты
Конечно, использование хуков модификации запросов, таких как posts_join
, может вызывать побочные эффекты, поскольку они действуют глобально на все запросы. Поэтому обычно нужно оборачивать модификации в условие if
, чтобы применять их только при необходимости, и определить критерии для проверки может быть непросто.
Может, лучше сосредоточиться на оптимизации?
Однако, я предполагаю, что ваш вопрос связан скорее с оптимизацией, чем с возможностью выполнить запрос "топ-5 для 3 категорий", верно? Если это так, возможно, есть другие варианты с использованием WordPress API?
Лучше использовать Transients API для кэширования?
Я предполагаю, что ваши записи не меняются так часто, правильно? Что если вы примете три (3) запроса периодически и затем закэшируете результаты, используя Transients API? Вы получите поддерживаемый код и отличную производительность — даже лучше, чем с запросом UNION
, потому что WordPress сохранит списки записей как сериализованный массив в одной записи таблицы wp_options
.
Вы можете взять следующий пример и поместить его в корень вашего сайта как test.php
, чтобы протестировать:
<?php
$timeout = 4; // 4 часа
$categories = array('category-1','category-2','category-3');
include "wp-load.php";
$category_posts = get_transient('top5_posts_per_category');
if (!is_array($category_posts) || count($category_posts)==0) {
echo "Привет каждые {$timeout} часа!";
$category_posts = array();
foreach($categories as $category) {
$posts = get_posts("post_type=post&numberposts=5&taxonomy=category&term={$category}");
foreach($posts as $post) {
$category_posts[$post->ID] = $post;
}
}
set_transient('top5_posts_per_category',$category_posts,60*60*$timeout);
}
header('Content-Type:text/plain');
print_r($category_posts);
Итог
Хотя да, вы можете сделать то, о чем просили, используя SQL-запрос с UNION
и фильтр posts_join
, вероятно, лучше использовать кэширование с Transients API.
Надеюсь, это поможет?

Кеширование через транзиенты — отличный способ снизить нагрузку на сайт.

@Mike, если бы я жил на вашем континенте, я бы записался к вам на курсы! еще раз спасибо!

Разве нет способа ограничить хук join только для одного объекта WP_query?

@Manny Fleurmond - Обычно я советую задать отдельный вопрос (что вам в любом случае стоит сделать), но один из способов - создать подкласс WP_Query
, добавить свойство и затем проверять его наличие.

можешь также сохранить transient на неопределённый срок и затем сбросить его при save_post? есть хороший пример (с использованием хука edit_term) в codex

@MikeSchinkel спустя 2 года, и я постоянно использую transients!

WP_Query()
не поддерживает функциональность типа Первые X записей для каждой категории. Вместо WP_Query()
вы можете использовать get_posts()
для каждой из ваших категорий, то есть три раза:
<?php
$posts = array();
$categories = array(2,4,8);
foreach($categories as $cat) {
$posts[] = get_posts('numberposts=5&offset=1&category='.$cat);
}
?>
Теперь $posts
содержит первые пять записей для каждой категории.

разве это не 3 запроса к базе данных? Мне хотелось узнать, можно ли сделать это за 1 запрос, потому что я думал, что это эффективнее.

но ведь для этого и нужна база данных, верно? Запрашивать данные из неё. База данных создана для таких вещей. Вероятно, это быстрее, чем выполнять сложный SQL-запрос, о котором ты просишь. Не бойся использовать эти возможности. Если это сломает твой сайт, сообщи об этом здесь.

хм.. понял, я не очень разбираюсь в эффективности php/mysql/баз данных (хотел бы почитать больше), был уверен, что меньше запросов — лучше, а затем уже сам разбирать результат.

ну, вообще-то, конечно, больше — это больше, а меньше — это меньше. Но сначала попробуй и протестируй. Чем "хуже" твое программное обеспечение, тем больше у него потенциал для оптимизации. Так что лучше учиться на практике, потому что в итоге ты будешь писать лучшее ПО быстрее и лучше писать более быстрое ПО.

Я не знаю способа получить первые пять записей для каждой из категорий в одном запросе. Если у вас когда-либо будет всего 45 записей, то обращение к базе данных один раз и получение пяти записей для каждой категории, вероятно, будет наиболее эффективным подходом. Однако обращение к базе данных три раза и объединение результатов — это не так уж плохо.
