Comment étendre WP_Query pour inclure une table personnalisée dans la requête?

31

Cela fait maintenant des jours que je me penche sur cette question. Au départ, c'était comment stocker les données des suiveurs d'un utilisateur dans la base de données, pour laquelle j'ai reçu quelques bonnes recommandations ici sur WordPress Answers. Après, en suivant les recommandations, j'ai ajouté un nouveau tableau comme celui-ci:

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

Dans le tableau ci-dessus, la première ligne a un utilisateur avec un ID de 2 qui est suivi par un utilisateur avec un ID de 4. Dans la deuxième ligne, un utilisateur avec un ID de 3 est suivi par un utilisateur avec un ID de 10. La même logique s'applique pour la troisième ligne.

Maintenant, je veux essentiellement étendre WP_Query afin de pouvoir limiter les messages récupérés à celui de, uniquement par le (s) leader (s) d'un utilisateur. Donc, en tenant compte du tableau ci-dessus, si je passais l'ID utilisateur 10 à WP_Query, les résultats ne devraient contenir que des publications par ID utilisateur 2 et ID utilisateur 3.

J'ai beaucoup cherché en essayant de trouver une réponse. De plus, je n'ai vu aucun tutoriel pour m'aider à comprendre comment étendre la classe WP_Query. J'ai vu les réponses de Mike Schinkel (étendant WP_Query) à des questions similaires, mais je n'ai vraiment pas compris comment l'appliquer à mes besoins. Ce serait formidable si quelqu'un pouvait m'aider avec ça.

Liens vers les réponses de Mike comme demandé: Lien 1 , Lien 2

John
la source
Ajoutez un lien vers les réponses de Mikes, s'il vous plaît.
kaiser
1
pouvez-vous donner un exemple de ce que vous recherchez? WP_Queryest pour obtenir des messages, et je n'arrive pas à comprendre comment cela se lie aux messages.
mor7ifer
@kaiser J'ai mis à jour la question avec des liens vers les réponses de Mike.
John
@ m0r7if3r »Je souhaite étendre WP_Query afin de pouvoir limiter les articles récupérés à celui de, uniquement par le (s) chef (s) d'un utilisateur«, donc similaire à «obtenir les articles par auteur».
kaiser
2
@ m0r7if3r Posts est exactement ce que je dois rechercher. Mais les publications à récupérer doivent être effectuées par des utilisateurs répertoriés comme chefs de file d'un certain utilisateur dans le tableau personnalisé. Donc, en d'autres termes, je veux dire à WP_Query, allez chercher tous les messages de tous les utilisateurs répertoriés comme leaders d'un utilisateur ayant un ID «10» dans le tableau personnalisé.
John

Réponses:

13

Avis de non-responsabilité important: la bonne façon de procéder n'est PAS de modifier la structure de votre table, mais d'utiliser wp_usermeta. Ensuite, vous n'aurez pas besoin de créer de SQL personnalisé pour interroger vos publications (bien que vous ayez toujours besoin de SQL personnalisé pour obtenir une liste de tous ceux qui relèvent d'un superviseur particulier - dans la section Admin, par exemple). Cependant, puisque l'OP a demandé d'écrire du SQL personnalisé, voici la meilleure pratique actuelle pour injecter du SQL personnalisé dans une requête WordPress existante.

Si vous effectuez des jointures complexes, vous ne pouvez pas simplement utiliser le filtre posts_where, car vous devrez également modifier les sections de la jointure, de la sélection et éventuellement du groupe par ou de l'ordre de la requête.

Le mieux est d'utiliser le filtre 'posts_clauses'. Il s'agit d'un filtre très utile (à ne pas abuser!) Qui vous permet d'ajouter / modifier les différentes parties du SQL généré automatiquement par les nombreuses lignes de code dans le noyau WordPress. La signature de rappel du filtre est: function posts_clauses_filter_cb( $clauses, $query_object ){ }et il attend que vous reveniez $clauses.

Les clauses

$clausesest un tableau qui contient les clés suivantes; chaque clé est une chaîne SQL qui sera directement utilisée dans la dernière instruction SQL envoyée à la base de données:

  • par groupe
  • joindre
  • commandé par
  • distinct
  • des champs
  • limites

Si vous ajoutez une table à la base de données (ne faites cela que si vous ne pouvez absolument pas tirer parti de post_meta, user_meta ou taxonomies), vous devrez probablement toucher plusieurs de ces clauses, par exemple, le fields(le "SELECT" partie de l'instruction SQL), la join(toutes vos tables, à l'exception de celle de votre clause "FROM"), et peut-être la orderby.

Modification des clauses

La meilleure façon de le faire est de sous-référencer la clé appropriée du $clausestableau que vous avez obtenue du filtre:

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

Maintenant, si vous modifiez $join, vous serez en fait en train de modifier directement $clauses['join']afin que les modifications soient apportées $clauseslorsque vous le retournerez.

Préserver les clauses originales

Il y a de fortes chances (non, sérieusement, écoutez) que vous souhaitiez conserver le SQL existant que WordPress a généré pour vous. Sinon, vous devriez probablement regarder le posts_requestfiltre à la place - c'est la requête complète mySQL juste avant qu'elle ne soit envoyée à la base de données, afin que vous puissiez la remplir complètement avec la vôtre. Pourquoi voudriez-vous faire ça? Vous ne le faites probablement pas.

Donc, afin de préserver le SQL existant dans les clauses, n'oubliez pas d'ajouter aux clauses, pas de les affecter (c'est-à-dire: $join .= ' {NEW SQL STUFF}';ne pas utiliser $join = '{CLOBBER SQL STUFF}';. Notez que parce que chaque élément du $clausestableau est une chaîne, si vous souhaitez y ajouter, vous voudrez probablement insérer un espace avant tout autre jeton de caractère, sinon vous créerez probablement une erreur de syntaxe SQL.

Vous pouvez simplement supposer qu'il y aura toujours quelque chose dans chacune des clauses, et n'oubliez donc pas de commencer chaque nouvelle chaîne avec un espace, comme dans:, $join .= ' my_tableou, vous pouvez toujours ajouter une petite ligne qui n'ajoute un espace que si vous devez:

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


return $clauses;

C'est une chose stylistique plus que toute autre chose. Le point important à retenir est: toujours laisser un espace AVANT votre chaîne si vous ajoutez à une clause qui contient déjà du SQL!

Mettre ensemble

La première règle de développement de WordPress est d' essayer d'utiliser autant de fonctionnalités de base que possible. C'est le meilleur moyen de pérenniser votre travail. Supposons que l'équipe principale décide que WordPress utilisera désormais SQLite ou Oracle ou un autre langage de base de données. Tout mySQL manuscrit peut devenir invalide et casser votre plugin ou votre thème! Mieux vaut laisser WP générer autant de SQL que possible, et ajouter simplement les bits dont vous avez besoin.

Donc, le premier ordre du jour consiste à tirer parti WP_Queryde générer autant de votre requête de base que possible. La méthode exacte que nous utilisons pour ce faire dépend en grande partie de l' endroit où cette liste de messages est censée apparaître. Si c'est une sous-section de la page (pas votre requête principale) que vous utiliseriez get_posts(); si c'est la requête principale, je suppose que vous pouvez l'utiliser query_posts()et en finir, mais la bonne façon de le faire est d'intercepter la requête principale avant qu'elle ne frappe la base de données (et consomme des cycles de serveur), utilisez donc le requestfiltre.

D'accord, vous avez donc généré votre requête et le SQL est sur le point d'être créé. Eh bien, en fait, il a été créé, mais pas envoyé à la base de données. En utilisant le posts_clausesfiltre, vous allez ajouter votre table de relations avec les employés dans le mix. Appelons cette table {$ wpdb-> prefix}. 'user_relationship', et c'est une table d'intersection. (Soit dit en passant, je vous recommande de généraliser cette structure de table et de la transformer en une table d'intersection appropriée avec les champs suivants: 'relation_id', 'user_id', 'related_user_id', 'relation_type'; c'est beaucoup plus flexible et puissant. .. mais je m'égare).

Si je comprends ce que vous voulez faire, vous voulez transmettre un ID de leader et ne voir que les publications de ses suiveurs. J'espère avoir bien compris. Si ce n'est pas bien, vous devrez prendre ce que je dis et l'adapter à vos besoins. Je m'en tiendrai à la structure de votre table: nous avons un leader_idet un follower_id. Ainsi, le JOIN sera {$wpdb->posts}.post_authoractivé en tant que clé étrangère du 'follower_id' sur votre table '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
la source
13

Je réponds à cette question extrêmement tard et je m'en excuse. J'avais été bien trop occupé par les délais pour m'occuper de cela.

Un grand merci à @ m0r7if3r et @kaiser pour avoir fourni les solutions de base que j'ai pu étendre et implémenter dans mon application. Cette réponse détaille mon adaptation des solutions proposées par @ m0r7if3r et @kaiser.

Tout d'abord, permettez-moi d'expliquer pourquoi cette question a été posée en premier lieu. D'après la question et les commentaires de celle-ci, on pourrait comprendre que j'essaie d'obtenir WP_Query pour extraire les messages de tous les utilisateurs (leaders) qu'un utilisateur donné (suiveur) suit. La relation entre le suiveur et le leader est stockée dans une table personnalisée follow. La solution la plus courante à ce problème consiste à extraire les ID utilisateur de tous les leaders d'un suiveur du tableau suivant et à les placer dans un tableau. Voir ci-dessous:

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;

Une fois que vous avez le tableau des leaders, vous pouvez le passer comme argument à WP_Query. Voir ci-dessous:

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 solution ci-dessus est le moyen le plus simple d'obtenir les résultats souhaités. Cependant, il n'est pas évolutif. Au moment où un suiveur suit des dizaines et des milliers de leaders, le tableau d'ID de leader résultant deviendrait extrêmement volumineux et forcerait votre site WordPress à utiliser 100 Mo à 250 Mo de mémoire à chaque chargement de page et finirait par planter le site. La solution au problème consiste à exécuter une requête SQL directement sur la base de données et à récupérer les publications pertinentes. C'est à ce moment que la solution de @ m0r7if3r est venue à la rescousse. Suite à la recommandation de @ kaiser, j'ai décidé de tester les deux implémentations. J'ai importé environ 47K utilisateurs à partir d'un fichier CSV pour les enregistrer sur une nouvelle installation de test de WordPress. L'installation exécutait le thème Twenty Eleven. Après cela, j'ai exécuté une boucle for pour faire environ 50 utilisateurs suivre tous les autres utilisateurs. La différence de temps de requête pour la solution @kaiser et @ m0r7if3r était stupéfiante. La solution de @ kaiser prenait normalement environ 2 à 5 secondes pour chaque requête. La variation que je suppose se produit lorsque WordPress met en cache les requêtes pour une utilisation ultérieure. D'autre part, la solution de @ m0r7if3r a démontré un temps de requête de 0,02 ms en moyenne. Pour tester les deux solutions, j'avais l'indexation ON pour la colonne leader_id. Sans indexation, le temps de requête a considérablement augmenté.

L'utilisation de la mémoire lors de l'utilisation d'une solution basée sur une baie se situait autour de 100-150 Mo et est tombée à 20 Mo lors de l'exécution d'un SQL direct.

J'ai heurté une bosse avec la solution de @ m0r7if3r lorsque j'ai eu besoin de passer l'ID de suiveur à la fonction de filtre posts_where. Au moins, à ma connaissance, WordPress ne permet aucun moyen de transmettre une variable aux fonctions de filer. Vous pouvez cependant utiliser des variables globales, mais je voulais éviter les globales. J'ai fini par étendre WP_Query pour enfin résoudre le problème. Voici donc la solution finale que j'ai implémentée (basée sur la solution 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 );

Remarque: j'ai finalement essayé la solution ci-dessus avec 1,2 million d'entrées dans le tableau suivant. Le temps de requête moyen était d'environ 0,060 ms.

John
la source
3
Je ne vous ai jamais dit combien j'ai apprécié la discussion sur cette question. Maintenant que j'ai découvert que je l'avais manqué, j'ai ajouté un vote positif :)
kaiser
8

Vous pouvez le faire avec une solution entièrement SQL à l'aide du posts_wherefiltre. Voici un exemple de cela:

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;
}

Je pense qu'il peut y avoir un moyen de le faire JOINaussi, mais je ne peux pas le trouver. Je continuerai à jouer avec et à mettre à jour la réponse si je l'obtiens.

Alternativement, comme l' a suggéré @kaiser , vous pouvez le diviser en deux parties: obtenir les leaders et faire la requête. J'ai le sentiment que cela pourrait être moins efficace, mais c'est certainement la voie la plus compréhensible. Vous devrez tester l'efficacité par vous-même pour déterminer la meilleure méthode, car les requêtes SQL imbriquées peuvent devenir assez lentes.

DES COMMENTAIRES:

Vous devez mettre la fonction dans votre functions.phpet faire le add_filter()bon avant query()d' WP_Queryappeler la méthode de . Immédiatement après cela, vous devez remove_filter()pour que cela n'affecte pas les autres requêtes.

mor7ifer
la source
1
Modifié votre A et ajouté prepare(). J'espère que cela ne vous dérange pas la modification. Et oui: la performance doit être mesurée par OP. Quoi qu'il en soit: je pense toujours que cela devrait être simplement usermeta et rien d'autre.
kaiser
@ m0r7if3r Thx pour avoir essayé une solution. Je viens de poster un commentaire en réponse à la réponse de Kaiser, avec des inquiétudes sur d'éventuels problèmes d'évolutivité. Veuillez en tenir compte.
John
1
@kaiser Cela ne me dérange pas du tout, en fait je l'apprécie plutôt :)
mor7ifer
@ m0r7if3r Merci. Avoir des gars comme vous dans les roches de la communauté :)
kaiser
1
Vous devez mettre la fonction dans votre functions.phpet faire le add_filter()bon avant query()d' WP_Queryappeler la méthode de . Immédiatement après cela, vous devez remove_filter()pour que cela n'affecte pas les autres requêtes. Je ne sais pas quel serait le problème avec la réécriture d'URL, je l'ai utilisé posts_whereà plusieurs reprises et je n'ai jamais vu cela ...
mor7ifer
6

Balise de modèle

Placez simplement les deux fonctions dans votre functions.phpfichier. Ajustez ensuite la 1ère fonction et ajoutez votre nom de table personnalisé. Ensuite, vous avez besoin d'un essai / erreur pour vous débarrasser de l'ID utilisateur actuel dans le tableau résultant (voir le commentaire).

/**
 * 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 )
    ) );
}

À l'intérieur du modèle

Ici, vous pouvez faire ce que vous voulez avec vos résultats.

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

REMARQUE Nous n'avons pas de données de test, etc. donc ce qui précède est un peu un jeu de devinettes. Assurez-vous que vous modifiez cette réponse avec ce qui a fonctionné pour vous, afin que nous ayons un résultat satisfaisant pour les lecteurs ultérieurs. J'approuverai la modification au cas où vous auriez un représentant trop bas. Vous pouvez ensuite également supprimer cette note. Merci.

kaiser
la source
2
JOINest beaucoup plus cher. Plus: Comme je l'ai mentionné, nous n'avons pas de données de test, veuillez donc tester les deux réponses et nous éclairer avec vos résultats.
kaiser
1
WP_Query lui-même fonctionne avec les jointures entre la table des publications et postmeta lors de la requête. J'ai vu une augmentation de l'utilisation de la mémoire PHP à 70 Mo - 200 Mo par chargement de page. Exécuter quelque chose comme ça avec de nombreux utilisateurs simultanés nécessiterait une infrastructure extrême. Ma conjecture serait que, puisque WordPress implémente déjà une technique similaire, les JOIN devraient être moins contraignants que de travailler avec un tableau d'ID.
John
1
@John bon d'entendre. vraiment envie de connaître les résultats.
kaiser
4
Ok voici les résultats des tests. Pour cela, j'ai ajouté environ 47K utilisateurs à partir d'un fichier csv. Plus tard, a exécuté une boucle for pour que les 45 premiers utilisateurs suivent tous les autres utilisateurs. Cela a abouti à 3 704 951 enregistrements enregistrés dans ma table personnalisée. Initialement, la solution de @ m0r7if3r m'a donné un temps de requête de 95 secondes, qui est descendu à 0,020 ms après avoir activé l'indexation sur la colonne leader_id. La mémoire PHP totale consommée était d'environ 20 Mo. En revanche, votre solution a pris environ 2 à 5 secondes pour la requête avec l'indexation activée. La mémoire PHP totale consommée était d'environ 117 Mo.
John
1
J'ajoute une autre réponse (nous pouvons traiter et modifier / éditer là-dessus) car le formatage du code dans les commentaires est tout simplement nul: P
kaiser
3

Remarque: Cette réponse est ici pour éviter une discussion prolongée dans les commentaires

  1. Voici le code OPs des commentaires, pour ajouter le premier ensemble d'utilisateurs de test. Je dois être modifié pour un exemple du monde réel.

    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 About Test Pour cela, j'ai ajouté environ 47K utilisateurs à partir d'un fichier csv. Plus tard, a exécuté une boucle for pour que les 45 premiers utilisateurs suivent tous les autres utilisateurs.

    • Cela a abouti à 3 704 951 enregistrements enregistrés dans ma table personnalisée.
    • Initialement, la solution de @ m0r7if3r m'a donné un temps de requête de 95 secondes, qui est descendu à 0,020 ms après avoir activé l'indexation sur la colonne leader_id. La mémoire PHP totale consommée était d'environ 20 Mo.
    • En revanche, votre solution a pris environ 2 à 5 secondes pour la requête avec l'indexation activée. La mémoire PHP totale consommée était d'environ 117 Mo.
  2. Ma réponse à ce ↑ test:

    un test plus «réel»: laissez chaque utilisateur suivre un $leader_amount = rand( 0, 5 );, puis ajoutez le nombre de $leader_amount x $random_ids = rand( 0, 47000 );à chaque utilisateur. Jusqu'à présent, la chose que nous savons est la suivante: ma solution serait extrêmement mauvaise si un utilisateur se suivait. De plus: vous devrez montrer comment vous avez fait le test et où exactement vous avez ajouté les minuteurs.

    Je dois également déclarer que le ↑ suivi du temps ci-dessus ne peut pas être vraiment mesuré, car il faudrait également du temps pour calculer la boucle ensemble. Il serait préférable de parcourir l'ensemble d'ID résultant dans une deuxième boucle.

processus supplémentaire ici

kaiser
la source
2
Remarque pour ceux qui ont suivi ce Q: Je suis en train de mesurer les performances dans diverses conditions et je publierai le résultat dans un jour ou 3. Cela a été une tâche extrêmement longue en raison de l'échelle des données de test qui doivent être généré.
John