¿Cómo extender WP_Query para incluir una tabla personalizada en la consulta?

31

He pasado días sobre este problema ahora. Inicialmente, era cómo almacenar los datos de seguidores de un usuario en la base de datos, para lo cual obtuve un par de buenas recomendaciones aquí en WordPress Answers. Después, siguiendo las recomendaciones, he agregado una nueva tabla como esta:

id  leader_id   follower_id
1   2           4
2   3           10
3   2           10

En la tabla anterior, la primera fila tiene un usuario con una ID de 2 al que sigue un usuario con una ID de 4. En la segunda fila, un usuario con una ID de 3 es seguido por un usuario con una ID de 10. La misma lógica se aplica para la tercera fila.

Ahora, esencialmente quiero extender WP_Query para poder limitar las publicaciones obtenidas a las de, solo por el líder (s) de un usuario. Entonces, teniendo en cuenta la tabla anterior, si tuviera que pasar la ID de usuario 10 a WP_Query, los resultados solo deberían contener publicaciones por ID de usuario 2 e ID de usuario 3.

He buscado mucho tratando de encontrar una respuesta. Tampoco he visto ningún tutorial que me ayude a comprender cómo extender la clase WP_Query. He visto las respuestas de Mike Schinkel (extendiendo WP_Query) a preguntas similares, pero realmente no he entendido cómo aplicarlo a mis necesidades. Sería genial si alguien pudiera ayudarme con esto.

Enlaces a las respuestas de Mike según lo solicitado: Enlace 1 , Enlace 2

John
fuente
Agregue un enlace a las respuestas de Mikes, por favor.
kaiser
1
¿Puedes dar un ejemplo de lo que estarías buscando? WP_Queryes para obtener publicaciones, y no entiendo cómo se vincula esto con las publicaciones.
mor7ifer
@kaiser He actualizado la pregunta con enlaces a las respuestas de Mike.
John
@ m0r7if3r »Quiero ampliar WP_Query para poder limitar las publicaciones obtenidas a las de, solo por el líder (s) de un usuario«, de forma similar a "obtener publicaciones por autor".
kaiser
2
@ m0r7if3r Publicaciones es exactamente lo que necesito consultar. Pero las publicaciones que se deben buscar deben ser realizadas por usuarios que figuran como líderes de un determinado usuario en la tabla personalizada. Entonces, en otras palabras, quiero decirle a WP_Query, vaya a buscar todas las publicaciones de todos los usuarios que figuran como líderes de un usuario que tiene un ID '10' en la tabla personalizada.
John

Respuestas:

13

Descargo de responsabilidad importante: la forma correcta de hacer esto NO es modificar la estructura de su tabla, sino usar wp_usermeta. Entonces no necesitará crear ningún SQL personalizado para consultar sus publicaciones (aunque aún necesitará algo de SQL personalizado para obtener una lista de todos los que informan a un supervisor en particular, por ejemplo, en la sección Administrador). Sin embargo, dado que el OP preguntó sobre la escritura de SQL personalizado, esta es la mejor práctica actual para inyectar SQL personalizado en una consulta de WordPress existente.

Si está haciendo uniones complejas, no puede simplemente usar el filtro posts_where, porque también necesitará modificar la unión, la selección y posiblemente el grupo por u ordenar por secciones de la consulta.

Su mejor opción es usar el filtro 'posts_clauses'. Este es un filtro muy útil (¡no se debe abusar!) Que le permite agregar / modificar las diversas partes del SQL que se generan automáticamente por las muchas líneas de código dentro del núcleo de WordPress. La firma de devolución de llamada del filtro es: function posts_clauses_filter_cb( $clauses, $query_object ){ }y espera que regrese $clauses.

Las cláusulas

$clauseses una matriz que contiene las siguientes claves; cada clave es una cadena SQL que se usará directamente en la declaración SQL final enviada a la base de datos:

  • dónde
  • agrupar por
  • unirse
  • ordenar
  • distinto
  • campos
  • límites

Si está agregando una tabla a la base de datos (solo haga esto si no puede aprovechar post_meta, user_meta o taxonomías) probablemente necesitará tocar más de una de estas cláusulas, por ejemplo, el fields(el "SELECCIONAR" parte de la instrucción SQL), join(todas sus tablas, excepto la de su cláusula "FROM"), y quizás el orderby.

Modificando las Cláusulas

La mejor manera de hacerlo es subreferenciar la clave relevante de la $clausesmatriz que obtuvo del filtro:

$join = &$clauses['join'];

Ahora, si modifica $join, en realidad estará modificando directamente, $clauses['join']por lo que los cambios se realizarán $clausescuando lo devuelva.

Preservando las Cláusulas Originales

Es probable que (no, en serio, escuche) desee conservar el SQL existente que WordPress generó para usted. De lo contrario, probablemente debería mirar el posts_requestfiltro; esa es la consulta completa de mySQL justo antes de que se envíe a la base de datos, por lo que puede bloquearla totalmente con la suya. Por qué querrías hacer esto? Probablemente no lo hagas.

Por lo tanto, para preservar el SQL existente en las cláusulas, recuerde agregar a las cláusulas, no asignarlas (es decir: $join .= ' {NEW SQL STUFF}';no usar $join = '{CLOBBER SQL STUFF}';. Tenga en cuenta que debido a que cada elemento de la $clausesmatriz es una cadena, si desea agregarle, probablemente desee insertar un espacio antes de cualquier otro token de caracteres; de lo contrario, probablemente creará algún error de sintaxis SQL.

Puede suponer que siempre habrá algo en cada una de las cláusulas, y recuerde comenzar cada nueva cadena con un espacio, como en:, $join .= ' my_tableo, siempre puede agregar una pequeña línea que solo agrega un espacio si necesita:

$join = &$clauses['join'];
if (! empty( $join ) ) $join .= ' ';
$join .= "JOIN my_table... "; // <-- note the space at the end
$join .= "JOIN my_other_table... ";


return $clauses;

Eso es algo estilístico más que cualquier otra cosa. Lo importante para recordar es: ¡ siempre deje un espacio ANTES de su cadena si va a agregar a una cláusula que ya tiene algo de SQL!

Poniendo todo junto

La primera regla del desarrollo de WordPress es tratar de utilizar la mayor cantidad de funciones básicas que pueda. Esta es la mejor manera de preparar su trabajo para el futuro. Supongamos que el equipo central decide que WordPress ahora usará SQLite u Oracle o algún otro lenguaje de base de datos. ¡Cualquier MySQL escrito a mano puede volverse inválido y romper su plugin o tema! Es mejor dejar que WP genere la mayor cantidad de SQL posible por sí mismo, y solo agregue los bits que necesita.

Entonces, el primer orden de negocios es aprovechar WP_Querypara generar la mayor cantidad posible de su consulta base. El método exacto que usamos para hacer esto depende en gran medida de dónde se supone que debe aparecer esta lista de publicaciones. Si es una subsección de la página (no es su consulta principal) que usaría get_posts(); si es la consulta principal, supongo que podría usarla query_posts()y terminar con ella, pero la forma correcta de hacerlo es interceptar la consulta principal antes de que llegue a la base de datos (y consuma los ciclos del servidor), así que use el requestfiltro.

Bien, entonces has generado tu consulta y el SQL está a punto de ser creado. Bueno, de hecho, ha sido creado, simplemente no enviado a la base de datos. Al usar el posts_clausesfiltro, agregará la tabla de relaciones de sus empleados a la mezcla. Llamemos a esta tabla {$ wpdb-> prefijo}. 'user_relationship', y es una tabla de intersección. (Por cierto, le recomiendo que genere esta estructura de tabla y la convierta en una tabla de intersección adecuada con los siguientes campos: 'relacion_id', 'id_usuario', 'related_user_id', 'relacion_tipo'; esto es mucho más flexible y poderoso. .. pero yo divago).

Si entiendo lo que quieres hacer, quieres pasar la identificación de un líder y luego ver solo las publicaciones de los seguidores de ese líder. Espero haber acertado. Si no está bien, tendrá que tomar lo que digo y adaptarlo a sus necesidades. Me quedaré con la estructura de su mesa: tenemos a leader_idy a follower_id. Por lo tanto, JOIN estará {$wpdb->posts}.post_authoractivado como una clave foránea para 'follower_id' en su tabla 'user_relationship'.

add_filter( 'posts_clauses', 'filter_by_leader_id', 10, 2 ); // we need the 2 because we want to get all the arguments

function filter_by_leader_id( $clauses, $query_object ){
  // I don't know how you intend to pass the leader_id, so let's just assume it's a global
  global $leader_id;

  // In this example I only want to affect a query on the home page.
  // This is where the $query_object is used, to help us avoid affecting
  // ALL queries (since ALL queries pass through this filter)
  if ( $query_object->is_home() ){
    // Now, let's add your table into the SQL
    $join = &$clauses['join'];
    if (! empty( $join ) ) $join .= ' '; // add a space only if we have to (for bonus marks!)
    $join .= "JOIN {$wpdb->prefix}employee_relationship EMP_R ON EMP_R.follower_id = {$wpdb->posts}.author_id";

    // And make sure we add it to our selection criteria
    $where = &$clauses['where'];
    // Regardless, you always start with AND, because there's always a '1=1' statement as the first statement of the WHERE clause that's added in by WP/
    // Just don't forget the leading space!
    $where .= " AND EMP_R.leader_id={$leader_id}"; // assuming $leader_id is always (int)

    // And I assume you'll want the posts "grouped" by user id, so let's modify the groupby clause
    $groupby = &$clauses['groupby'];
    // We need to prepend, so...
    if (! empty( $groupby ) ) $groupby = ' ' . $groupby; // For the show-offs
    $groupby = "{$wpdb->posts}.post_author" . $groupby;
  }

  // Regardless, we need to return our clauses...
  return $clauses;
}
Tom Auger
fuente
13

Estoy respondiendo esta pregunta extremadamente tarde y mis disculpas por lo mismo. Había estado demasiado ocupado con los plazos para atender esto.

Muchas gracias a @ m0r7if3r y @kaiser por proporcionar las soluciones básicas que podría ampliar e implementar en mi aplicación. Esta respuesta proporciona detalles sobre mi adaptación de las soluciones ofrecidas por @ m0r7if3r y @kaiser.

Primero, permítanme explicar por qué se hizo esta pregunta en primer lugar. A partir de la pregunta y sus comentarios, uno podría deducir que estoy tratando de hacer que WP_Query extraiga publicaciones de todos los usuarios (líderes) que sigue un usuario (seguidor) determinado. La relación entre el seguidor y el líder se almacena en una tabla personalizada follow. La solución más común a este problema es extraer las ID de usuario de todos los líderes de un seguidor de la tabla siguiente y colocarlas en una matriz. Vea abajo:

global $wpdb;
$results = $wpdb->get_results($wpdb->prepare('SELECT leader_id FROM cs_follow WHERE follower_id = %s', $user_id));

foreach($results as $result)
    $leaders[] = $result->leader_id;

Una vez que tenga la matriz de líderes, puede pasarla como argumento a WP_Query. Vea abajo:

if (isset($leaders)) $authors = implode(',', $leaders); // Necessary as authors argument of WP_Query only accepts string containing post author ID's seperated by commas

$args = array(
    'post_type'         => 'post',
    'posts_per_page'    => 10,
    'author'            => $authors
);

$wp_query = new WP_Query( $args );

// Normal WordPress loop continues

La solución anterior es la forma más sencilla de lograr los resultados deseados. Sin embargo, no es escalable. En el momento en que tenga un seguidor siguiendo a decenas y miles de líderes, la matriz resultante de ID de líderes se volvería extremadamente grande y obligaría a su sitio de WordPress a usar 100MB - 250MB de memoria en cada carga de página y eventualmente bloquear el sitio. La solución al problema es ejecutar una consulta SQL directamente en la base de datos y buscar publicaciones relevantes. Fue entonces cuando la solución de @ m0r7if3r vino a rescatar. Siguiendo la recomendación de @ kaiser, me propuse probar ambas implementaciones. Importé alrededor de 47K usuarios de un archivo CSV para registrarlos en una nueva instalación de prueba de WordPress. La instalación ejecutaba el tema Twenty Eleven. Después de esto, ejecuté un bucle for para que unos 50 usuarios siguieran a todos los demás usuarios. La diferencia en el tiempo de consulta para la solución de @kaiser y @ m0r7if3r fue asombrosa. La solución de @ kaiser normalmente tomaba de 2 a 5 segundos para cada consulta. La variación que supongo ocurre cuando WordPress almacena en caché las consultas para su uso posterior. Por otro lado, la solución de @ m0r7if3r demostró un tiempo de consulta de 0.02 ms en promedio. Para probar ambas soluciones, tenía la indexación activada para la columna leader_id. Sin indexación hubo un aumento dramático en el tiempo de consulta.

El uso de la memoria cuando se utiliza una solución basada en matriz se situó en torno a 100-150 MB y se redujo a 20 MB al ejecutar un SQL directo.

Llegué a un golpe con la solución de @ m0r7if3r cuando necesitaba pasar la identificación del seguidor a la función de filtro posts_where. Al menos, según mi conocimiento, WordPress no permite pasar una variable a las funciones del archivador. Sin embargo, puede usar variables globales, pero quería evitar los globales. Terminé extendiendo WP_Query para finalmente abordar el problema. Así que aquí está la solución final que implementé (basada en la solución de @ m0r7if3r).

class WP_Query_Posts_by_Leader extends WP_Query {
    var $follower_id;

    function __construct($args=array()) {
        if(!empty($args['follower_id'])) {
            $this->follower_id = $args['follower_id'];
            add_filter('posts_where', array($this, 'posts_where'));
        }

        parent::query($args);
    }

    function posts_where($where) {
        global $wpdb;
        $table_name = $wpdb->prefix . 'follow';
        $where .= $wpdb->prepare(" AND post_author IN (SELECT leader_id FROM " . $table_name . " WHERE follower_id = %d )", $this->follower_id);
        return $where;
    }
}


$args = array(
    'post_type'         => 'post',
    'posts_per_page'    => 10,
    'follower_id'       => $follower_id
);

$wp_query = new WP_Query_Posts_by_Leader( $args );

Nota: Finalmente probé la solución anterior con 1.2 millones de entradas en la siguiente tabla. El tiempo promedio de consulta fue de alrededor de 0.060 ms.

John
fuente
3
Nunca te dije cuánto aprecié la discusión sobre esta pregunta. Ahora que descubrí que me lo perdí,
kaiser
8

Puede hacer esto con una solución completamente SQL usando el posts_wherefiltro. Aquí hay un ejemplo de eso:

if( some condition ) 
    add_filter( 'posts_where', 'wpse50305_leader_where' );
    // lol, question id is the same forward and backward

function wpse50305_leader_where( $where ) {
    $where .= $GLOBALS['wpdb']->prepare( ' AND post_author '.
        'IN ( '.
            'SELECT leader_id '.
            'FROM custom_table_name '.
            'WHERE follower_id = %s'.
        ' ) ', $follower_id );
    return $where;
}

Creo que también puede haber una manera de hacer esto JOIN, pero no puedo encontrarlo. Seguiré jugando con él y actualizaré la respuesta si lo consigo.

Alternativamente, como sugirió @kaiser , puede dividirlo en dos partes: obtener los líderes y hacer la consulta. Tengo la sensación de que esto podría ser menos eficiente, pero sin duda es el camino más comprensible. Tendría que probar la eficiencia por usted mismo para determinar qué método es mejor, ya que las consultas SQL anidadas pueden ser bastante lentas.

DE LOS COMENTARIOS:

Debe poner la función en su functions.phpy hacer lo add_filter()correcto antes query()de que WP_Queryse llame al método de . Inmediatamente después de eso, debe hacerlo remove_filter()para que no afecte las otras consultas.

mor7ifer
fuente
1
Editó su A y agregó prepare(). Espero que no te importe la edición. Y sí: el rendimiento tiene que ser medido por OP. De todos modos: sigo pensando que esto debería ser simplemente usermeta y nada más.
Kaiser
@ m0r7if3r Thx por probar una solución. Acabo de publicar un comentario en respuesta a la respuesta de Kaiser, con preocupaciones sobre posibles problemas de escalabilidad. Por favor, tómalo en consideración.
John
1
@kaiser No te preocupes en lo más mínimo, de hecho lo aprecio mucho :)
mor7ifer
@ m0r7if3r Gracias. Tener chicos como tú en las rocas de la comunidad :)
kaiser
1
Debe poner la función en su functions.phpy hacer lo add_filter()correcto antes query()de que WP_Queryse llame al método de . Inmediatamente después de eso, debe hacerlo remove_filter()para que no afecte las otras consultas. No estoy seguro de cuál sería el problema con la reescritura de URL, lo he usado posts_whereen muchas ocasiones y nunca lo vi ...
mor7ifer
6

Etiqueta de plantilla

Simplemente coloque ambas funciones en su functions.phparchivo. Luego ajuste la primera función y agregue su nombre de tabla personalizado. Luego, necesita algún intento / error para deshacerse de la ID de usuario actual dentro de la matriz resultante (ver comentario).

/**
 * Get "Leaders" of the current user
 * @param int $user_id The current users ID
 * @return array $query The leaders
 */
function wpse50305_get_leaders( $user_id )
{
    global $wpdb;

    return $wpdb->query( $wpdb->prepare(
        "
            SELECT `leader_id`, `follower_id`
            FROM %s
                WHERE `follower_id` = %s
            ORDERBY `leader_id` ASC
        ",
        // Edit the table name
        "{$wpdb->prefix}custom_table_name"
        $user_id
    ) );
}

/**
 * Get posts array that contain posts by 
 * "Leaders" the current user is following
 * @return array $posts Posts that are by the current "Leader
 */
function wpse50305_list_posts_by_leader()
{
    get_currentuserinfo();
    global $current_user;

    $user_id = $current_user->ID;

    $leaders = wpse5035_get_leaders( $user_id );
    // could be that you need to loop over the $leaders
    // and get rid of the follower ids

    return get_posts( array(
        'author' => implode( ",", $leaders )
    ) );
}

Dentro de la plantilla

Aquí puedes hacer lo que quieras con tus resultados.

foreach ( wpse50305_list_posts_by_leader() as $post )
{
    // do something with $post
}

NOTA Nosotros ¡No tenemos testdata, etc por lo que lo anterior es un poco de un juego de adivinanzas. Asegúrese de que usted editar esta respuesta con lo que ha funcionado para usted, por lo que tenemos un resultado satisfactorio para los lectores posteriores. Aprobaré la edición en caso de que tengas una reputación demasiado baja. Entonces también puede eliminar esta nota. Gracias.

emperador
fuente
2
JOINEs mucho más caro. Además: como mencioné, no tenemos datos de prueba, por lo tanto, pruebe ambas respuestas e ilumínenos con sus resultados.
Kaiser
1
WP_Query funciona con JOIN entre la tabla de publicaciones y postmeta al realizar consultas. He visto que el uso de la memoria PHP aumenta a 70MB - 200MB por carga de página. Ejecutar algo así con muchos usuarios simultáneos requeriría una infraestructura extrema. Supongo que, dado que WordPress ya implementa una técnica similar, JOIN debería ser menos exigente en comparación con trabajar con una variedad de ID.
John
1
@John es bueno escucharlo. Realmente quiero saber el futuro.
Kaiser
44
Ok, aquí están los resultados de la prueba. Para esto agregué unos 47K usuarios de un archivo csv. Más tarde, ejecutó un bucle for para que los primeros 45 usuarios siguieran a los demás usuarios. Esto resultó en 3,704,951 registros guardados en mi tabla personalizada. Inicialmente, la solución de @ m0r7if3r me dio un tiempo de consulta de 95 segundos, que bajó a 0.020 ms después de activar la indexación en la columna leader_id. La memoria PHP total consumida fue de alrededor de 20 MB. Por otro lado, su solución tomó alrededor de 2 a 5 segundos para consultar con la indexación activada. La memoria PHP total consumida fue de alrededor de 117 MB.
John
1
Agrego otra respuesta (podemos procesar y modificar / editar en eso) ya que el formato de código en los comentarios simplemente apesta: P
kaiser
3

Nota: esta respuesta aquí es para evitar una discusión extensa en los comentarios

  1. Aquí está el Código OP de los comentarios, para agregar el primer conjunto de usuarios de prueba. Tengo que ser modificado a un ejemplo del mundo real.

    for ( $j = 2; $j <= 52; $j++ ) 
    {
        for ( $i = ($j + 1); $i <= 47000; $i++ )
        {
            $rows_affected = $wpdb->insert( $table_name, array( 'leader_id' => $i, 'follower_id' => $j ) );
        }
    }

    OP Acerca de la prueba Para esto agregué unos 47K usuarios de un archivo csv. Más tarde, ejecutó un bucle for para que los primeros 45 usuarios siguieran a los demás usuarios.

    • Esto resultó en 3,704,951 registros guardados en mi tabla personalizada.
    • Inicialmente, la solución de @ m0r7if3r me dio un tiempo de consulta de 95 segundos, que bajó a 0.020 ms después de activar la indexación en la columna leader_id. La memoria PHP total consumida fue de alrededor de 20 MB.
    • Por otro lado, su solución tomó alrededor de 2 a 5 segundos para consultar con la indexación activada. La memoria PHP total consumida fue de alrededor de 117 MB.
  2. Mi respuesta a esta prueba ↑:

    una prueba más de la "vida real": deje que cada usuario siga $leader_amount = rand( 0, 5 );ay luego agregue el número de $leader_amount x $random_ids = rand( 0, 47000 );cada usuario. Hasta ahora, lo que sabemos es: mi solución sería extremadamente mala si un usuario se sigue entre sí. Además: tendrás que mostrar cómo hiciste la prueba y dónde agregaste exactamente los temporizadores.

    También tengo que decir que el seguimiento de tiempo anterior ↑ no se puede medir realmente, ya que también tomaría el tiempo de calcular el ciclo juntos. Mejor sería recorrer el conjunto resultante de ID en un segundo ciclo.

proceso adicional aquí

emperador
fuente
2
Nota para aquellos que han estado siguiendo esta P: Estoy en proceso de medir el rendimiento bajo varias condiciones y publicaré el resultado en un día o 3. Esta ha sido una tarea extremadamente lenta debido a la escala de datos de prueba que deben ser generado.
John