¿Cómo usar WP_Query para consultar múltiples categorías con un límite de publicaciones por categoría?
Tengo 3 categorías con 15 posts cada una, quiero hacer UNA sola consulta a la base de datos que traiga solo los primeros 5 posts de cada categoría, ¿cómo puedo hacerlo?
$q = new WP_Query(array( 'post__in' => array(2,4,8), 'posts_per_page' => **PRIMEROS_5_DE_CADA_CAT** ));
En caso de que no sea posible, ¿qué es más eficiente, obtener todos los posts de la categoría principal y recorrerlos o crear 3 consultas diferentes?

Lo que deseas es posible, pero requerirá que profundices en SQL, lo cual prefiero evitar siempre que sea posible (no porque no lo conozca, soy un desarrollador avanzado de SQL, sino porque en WordPress es mejor utilizar la API siempre que se pueda para minimizar futuros problemas de compatibilidad relacionados con posibles cambios en la estructura de la base de datos).
SQL con un operador UNION
es una posibilidad
Para usar SQL, necesitas un operador UNION
en tu consulta, algo como esto, asumiendo que los slugs de tus categorías son "category-1"
, "category-2"
y "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
)
Puedes usar SQL UNION con un filtro posts_join
Usando lo anterior, puedes simplemente hacer la llamada directamente o puedes usar un gancho de filtro posts_join
como se muestra a continuación; ten en cuenta que estoy usando un heredoc de PHP para que el SQL;
quede alineado a la izquierda. También nota que usé una variable global para permitirte definir las categorías fuera del gancho, listando los slugs de categoría en un array. Puedes colocar este código en un plugin o en el archivo functions.php
de tu tema:
<?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 " ;
}
Pero puede haber efectos secundarios
Por supuesto, usar ganchos de modificación de consultas como posts_join
siempre invita a efectos secundarios, ya que actúan globalmente en las consultas, por lo que generalmente necesitas envolver tus modificaciones en un if
que solo las use cuando sea necesario, y los criterios para probar pueden ser complicados.
¿Enfocarse en la optimización en su lugar?
Sin embargo, supongo que tu pregunta está más preocupada por la optimización que por poder hacer una consulta de los 5 mejores por 3 categorías, ¿verdad? Si ese es el caso, tal vez haya otras opciones que usen la API de WordPress.
¿Mejor usar la API de transients para el almacenamiento en caché?
Supongo que tus posts no cambiarán con tanta frecuencia, ¿correcto? ¿Qué tal si aceptas el impacto de las tres (3) consultas periódicamente y luego almacenas en caché los resultados usando la API de transients? Obtendrás código mantenible y un gran rendimiento; un poco mejor que la consulta UNION
anterior porque WordPress almacenará las listas de posts como un array serializado en un registro de la tabla wp_options
.
Puedes tomar el siguiente ejemplo y colocarlo en la raíz de tu sitio web como test.php
para probarlo:
<?php
$timeout = 4; // 4 horas
$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 "¡Hola cada {$timeout} horas!";
$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);
Resumen
Aunque sí, puedes hacer lo que pediste usando una consulta SQL UNION
y el gancho de filtro posts_join
, probablemente sea mejor usar el almacenamiento en caché con la API de transients.
¿Espero que esto ayude?

El almacenamiento en caché mediante transients es una excelente manera de reducir el impacto en el sitio.

@Mike, si viviera en tu continente, ¡habría tomado clases contigo! ¡gracias de nuevo!

¿No hay una manera de limitar el hook de join a solo un objeto WP_query?

@Manny Fleurmond - Normalmente digo que hagas otra pregunta (lo cual deberías hacer de todos modos) pero una forma es crear una subclase de WP_Query
, añadir una propiedad y luego verificar esa propiedad.

¿podrías también guardar el transient indefinidamente y luego borrarlo en save_post? hay un buen ejemplo (usando el hook edit_term) en el codex

WP_Query()
no soporta algo como Primeros X Para Cada Categoría. En lugar de WP_Query()
puedes usar get_posts()
en cada una de tus categorías, es decir, tres veces:
<?php
$posts = array();
$categories = array(2,4,8);
foreach($categories as $cat) {
$posts[] = get_posts('numberposts=5&offset=1&category='.$cat);
}
?>
$posts
ahora contiene los primeros cinco posts para cada categoría.

¿no son 3 consultas a la base de datos? Quería saber si se puede hacer en 1 consulta porque pensé que es más eficiente.

bueno, para eso está la base de datos, ¿no? para consultar los datos. La base de datos está hecha para este tipo de cosas. Probablemente sea más rápido que ejecutar una consulta SQL complicada como la que pides. No temas usar estas herramientas. Si rompe tu sitio, repórtalo aquí.

mm... entiendo, no sé mucho sobre eficiencia en php/mysql/bases de datos (me gustaría leer más), estaba seguro de que menos consultas es mejor y luego procesar el resultado yo mismo.

bueno, es cierto, generalmente más es más y menos es menos. Pero primero prueba y comprueba. Cuanto "peor" sea tu software, más potencial tendrá para optimización. Así que mejor aprende haciendo, porque al final escribirás mejor software más rápido y podrás escribir software más rápido mejor.

No conozco una forma de obtener las primeras cinco publicaciones para cada una de las categorías en una sola consulta. Si en algún momento solo vas a tener 45 publicaciones, entonces acceder a la base de datos una vez y obtener tus cinco publicaciones por categoría probablemente sea el enfoque más eficiente. Sin embargo, acceder a la base de datos tres veces y combinar los resultados no es algo malo.
