web-dev-qa-db-fra.com

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

J'ai eu des jours sur cette question maintenant. Au départ, il s’agissait de savoir comment stocker les données d’adhérent d’un utilisateur dans une base de données, pour lesquelles j’ai reçu quelques recommandations intéressantes ici à WordPress Answers. Après avoir suivi 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 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 La même logique s’applique pour la troisième rangée.

Maintenant, je souhaite essentiellement étendre WP_Query afin de pouvoir limiter les publications extraites à celle de, uniquement par le ou les responsables d'un utilisateur. Donc, en prenant en compte le tableau ci-dessus, si je devais passer l'ID utilisateur 10 à WP_Query, les résultats ne devraient contenir que les messages de l'ID utilisateur 2 et de l'ID utilisateur 3.

J'ai beaucoup cherché à trouver une réponse. Ni, j'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 génial si quelqu'un pouvait m'aider avec ça.

Liens vers les réponses de Mike à la demande: lien 1 , lien 2

29
John

Je réponds extrêmement tard à cette question et je m'excuse pour la même chose. J'avais été trop occupé par les délais pour pouvoir m'occuper de ça.

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

Tout d’abord, laissez-moi vous expliquer pourquoi cette question a été posée en premier lieu. D'après la question et ses commentaires, on pourrait comprendre que j'essaie de faire en sorte que WP_Query tire les messages de tous les utilisateurs (responsables) suivis par un utilisateur donné (suiveur). La relation entre le suiveur et le responsable est stockée dans une table personnalisée follow. La solution la plus courante à ce problème consiste à extraire l'ID utilisateur de tous les leaders d'un suiveur du tableau ci-dessous et à le 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'atteindre les résultats souhaités. Cependant, il n'est pas évolutif. Dès que vous avez un suiveur après des dizaines et des milliers de leaders, le tableau résultant d'identifiants de leader devient extrêmement volumineux et contraint votre site WordPress à utiliser 100 à 250 Mo de mémoire à chaque chargement de page, puis à bloquer le site. La solution au problème consiste à exécuter une requête SQL directement sur la base de données et à extraire les publications pertinentes. C'est alors que la solution de @ m0r7if3r est venue à la rescousse. Suite à la recommandation de @ kaiser, j'ai entrepris de tester les deux implémentations. J'ai importé environ 47 000 utilisateurs à partir d'un fichier CSV pour les enregistrer sur une nouvelle installation de test de WordPress. L'installation fonctionnait thème Twenty Eleven. Après cela, j'ai lancé une boucle for pour que 50 utilisateurs environ suivent tous les autres utilisateurs. La différence de temps d'interrogation pour les solutions de @kaiser et de @ m0r7if3r était stupéfiante. La solution de @ kaiser prenait normalement entre 2 et 5 secondes pour chaque requête. La variation que je suppose se produit lorsque WordPress met les requêtes en cache pour une utilisation ultérieure. D'autre part, la solution de @ m0r7if3r a démontré un temps d'interrogation 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 d'interrogation a considérablement augmenté.

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

J'ai rencontré un problème avec la solution de @ m0r7if3r lorsque je devais passer l'ID suiveur à la fonction de filtre posts_where. Au moins, selon mes connaissances, WordPress ne permet aucun moyen de transmettre une variable aux fonctions de fichier. 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 mise en œuvre (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 ci-dessous. Le temps moyen d'interrogation était d'environ 0,060 ms.

11
John

Avertissement important: Pour ce faire, vous ne devez PAS modifier la structure de votre table, mais utiliser wp_usermeta. Dans ce cas, vous n'aurez pas besoin de créer de code SQL personnalisé pour interroger vos publications (vous aurez néanmoins besoin d'un code SQL personnalisé pour obtenir la liste de toutes les personnes relevant d'un superviseur particulier - dans la section Admin, par exemple). Cependant, étant donné que le PO a demandé comment écrire du SQL personnalisé, voici la meilleure pratique pour injecter du SQL personnalisé dans une requête WordPress existante.

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

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

Les clauses

$clauses est 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 le faites que si vous ne pouvez absolument pas utiliser post_meta, user_meta ou les taxonomies), vous devrez probablement toucher plusieurs de ces clauses, par exemple, la fields (la "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

Pour ce faire, la meilleure solution consiste à sous-référencer la clé correspondante dans le tableau $clauses obtenu à partir du filtre:

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

Maintenant, si vous modifiez $join, vous modifierez directement $clauses['join'] pour que les modifications soient dans $clauses lorsque vous le renverrez.

Préserver les clauses originales

Les chances sont (non, sérieusement, écoutez), vous voudrez conserver le SQL existant que WordPress a généré pour vous. Sinon, vous devriez probablement regarder plutôt le filtre posts_request - il s'agit de la requête mySQL complète juste avant son envoi à la base de données, afin que vous puissiez totalement l'enlever avec la vôtre. Pourquoi voudriez-vous faire cela? Vous n'avez probablement pas.

Ainsi, afin de préserver le code SQL existant dans les clauses, pensez à ajouter des clauses aux clauses, et non à les affecter (par exemple: utilisez $join .= ' {NEW SQL STUFF}'; pas $join = '{CLOBBER SQL STUFF}';. Notez que, comme chaque élément du tableau $clauses est une chaîne, si vous souhaitez 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. N'oubliez donc pas de commencer chaque nouvelle chaîne par un espace, comme dans: $join .= ' my_table, ou vous pouvez toujours ajouter une petite ligne qui ajoute un espace uniquement si vous en avez besoin. :

$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 le suivant: laissez toujours un espace AVANT votre chaîne si vous ajoutez une clause contenant déjà du code SQL!

Mettre ensemble

La première règle du développement WordPress est de essayez d'utiliser autant de fonctionnalités de base que possible. C'est le meilleur moyen de mettre votre travail à l'épreuve. Supposons que l’équipe principale décide que WordPress utilisera désormais SQLite, Oracle ou un autre langage de base de données. Toute mySQL écrite à la main 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.

Le premier ordre de travail consiste donc à utiliser WP_Query pour générer autant que possible votre requête de base. La méthode exacte que nous utilisons pour cela dépend en grande partie de/ cette liste de publications est supposée apparaître. S'il s'agit d'une sous-section de la page (et non de votre requête principale), vous utiliseriez get_posts(); Si c'est la requête principale, je suppose que vous pouvez 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 filtre request.

Bon, vous avez donc généré votre requête et le code 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 filtre posts_clauses, vous allez ajouter votre table de relations avec les employés au mixage. Appelons cette table {$ wpdb-> prefix}. 'user_relationship', et c'est une table d'intersection. (Au fait, je vous recommande de génerer cette structure de table et de la transformer en une table d'intersection avec les champs suivants: 'relationship_id', 'user_id', 'related_user_id', 'relationship_type'; c'est beaucoup plus flexible et puissant. .. mais je m'égare).

Si je comprends ce que vous voulez faire, vous voulez passer un identifiant de leader puis voir uniquement les messages de ses suiveurs. J'espère que j'ai bien compris. Si ce n'est pas correct, vous devrez prendre ce que je dis et l'adapter à vos besoins. Je vais rester avec votre structure de table: nous avons un leader_id et un follower_id. Ainsi, JOIN sera sur {$wpdb->posts}.post_author en tant que clé étrangère de "suiveur_id" sur votre table "utilisateur_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;
}
12
Tom Auger

Vous pouvez le faire avec une solution entièrement SQL en utilisant le filtre posts_where. 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 pourrait y avoir un moyen de faire cela avec JOIN également, mais je ne peux pas le faire. Je vais continuer à jouer avec et mettre à jour la réponse si je l'obtenir.

Alternativement, comme @kaiser suggéré, vous pouvez le scinder en deux parties: obtenir les leaders et exécuter la requête. J'ai l'impression que cela pourrait être moins efficace, mais c'est certainement la façon la plus compréhensible de procéder. Vous devrez tester l'efficacité par vous-même pour déterminer quelle méthode est la meilleure, car les requêtes SQL imbriquées peuvent être très lentes.

DES COMMENTAIRES:

Vous devriez mettre la fonction dans votre functions.php et faire la add_filter() juste avant que la méthode query() de WP_Query soit appelée. Immédiatement après, vous devriez remove_filter() afin que cela n’affecte pas les autres requêtes.

8
mor7ifer

Tag de modèle

Il suffit de placer les deux fonctions dans votre fichier functions.php. Puis ajustez la 1ère fonction et ajoutez votre nom de table personnalisé. Ensuite, vous avez besoin d'essayer/d'erreur pour supprimer 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
}

NOTENous n´avons pas des données de test, etc., 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 puissions obtenir un résultat satisfaisant pour les lecteurs ultérieurs. Je vais approuver le montage au cas où votre représentation serait trop basse. Vous pouvez alors également supprimer cette note. Merci.

5
kaiser

Remarque: Cette réponse vise à éviter une discussion prolongée dans les commentaires.

  1. Voici le code OPs des commentaires, pour ajouter le premier ensemble d’utilisateurs testés. 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 47 000 utilisateurs à partir d’un fichier csv. Plus tard, une boucle for a été exécutée pour que les 45 premiers utilisateurs suivent tous les autres utilisateurs.

    • Cela a eu pour résultat 3 704 951 enregistrements sauvegardés dans ma table personnalisée.
    • Initialement, la solution de @ m0r7if3r me donnait un temps de requête de 95 secondes, qui passait à 0,020 ms après l'activation de l'indexation sur la colonne leader_id. La mémoire totale utilisée par PHP était d'environ 20 Mo.
    • D'autre part, votre solution prenait environ 2 à 5 secondes pour une requête avec indexation activée. La mémoire totale utilisée par PHP était d'environ 117 Mo.
  2. Ma réponse à ce test ↑:

    un test plus "réel": laissez chaque utilisateur suivre une $leader_amount = Rand( 0, 5 ); puis ajoutez le nombre de $leader_amount x $random_ids = Rand( 0, 47000 ); à chaque utilisateur. Jusqu'ici, nous savons que: ma solution serait extrêmement mauvaise si un utilisateur se suivait l'un l'autre. De plus: vous aurez montré comment vous avez fait le test et où vous avez exactement ajouté les minuteries.

    Je dois aussi dire que le suivi ↑ supérieur au temps ne peut pas être vraiment mesuré, car il faudrait aussi du temps pour calculer la boucle ensemble. Il serait préférable de parcourir l'ensemble d'identifiants résultant dans une seconde boucle.

poursuite du processus ici

3
kaiser