Come estendere WP_Query per includere la tabella personalizzata nella query?


31

Sono stato giorni su questo problema ora. Inizialmente era, come archiviare i dati dei follower di un utente nel database, per il quale ho ricevuto un paio di buoni consigli qui su WordPress Answers. Dopo, seguendo i consigli, ho aggiunto una nuova tabella come questa:

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

Nella tabella sopra, la prima riga ha un utente con un ID di 2 che è seguito da un utente con un ID di 4. Nella seconda riga, un utente con un ID di 3 è seguito da un utente con un ID di 10. La stessa logica si applica per la terza riga.

Ora, essenzialmente, voglio estendere WP_Query in modo da poter limitare i post recuperati a quello di, solo dai leader di un utente. Quindi, prendendo in considerazione la tabella sopra, se dovessi passare l'ID utente 10 a WP_Query i risultati dovrebbero contenere solo post per ID utente 2 e ID utente 3.

Ho cercato molto cercando di trovare una risposta. Né ho visto alcun tutorial che mi aiuti a capire come estendere la classe WP_Query. Ho visto le risposte di Mike Schinkel (estendere WP_Query) a domande simili, ma non ho davvero capito come applicarlo alle mie esigenze. Sarebbe bello se qualcuno potesse aiutarmi con questo.

Collegamenti alla risposta di Mike come richiesto: Link 1 , Link 2


Aggiungi un link alle risposte di Mikes, per favore.
Kaiser

1
puoi fare un esempio di ciò per cui vorresti fare una query? WP_Queryè per ottenere post e non riesco a capire come questo si collega ai post.
mor7ifer,

@kaiser Ho aggiornato la domanda con collegamenti alle risposte di Mike.
Giovanni,

@ m0r7if3r »Voglio estendere WP_Query in modo da poter limitare i post recuperati a quello di, solo dai leader di un utente«, così simile a "ottenere post per autore".
Kaiser

2
@ m0r7if3r Posts è esattamente ciò di cui ho bisogno per chiedere. Ma i post da recuperare dovrebbero essere gli utenti che sono elencati come leader di un determinato utente nella tabella personalizzata. Quindi, in altre parole, voglio dirlo a WP_Query, andare a recuperare tutti i post di tutti gli utenti che sono elencati come leader di un utente con un ID "10" nella tabella personalizzata.
Giovanni,

Risposte:


13

Dichiarazione di non responsabilità importante: il modo corretto per farlo NON è modificare la struttura della tabella, ma usare wp_usermeta. Quindi non sarà necessario creare alcun SQL personalizzato per interrogare i tuoi post (anche se avrai comunque bisogno di alcuni SQL personalizzati per ottenere un elenco di tutti coloro che segnalano a un supervisore particolare, ad esempio nella sezione Amministratore). Tuttavia, poiché l'OP ha chiesto di scrivere un SQL personalizzato, ecco la migliore procedura corrente per iniettare un SQL personalizzato in una query WordPress esistente.

Se stai eseguendo join complessi, non puoi semplicemente utilizzare il filtro posts_where, poiché dovrai modificare il join, la selezione e, eventualmente, il gruppo o anche l'ordine per sezioni della query.

La soluzione migliore è utilizzare il filtro "posts_clauses". Questo è un filtro molto utile (che non dovrebbe essere abusato!) Che ti consente di aggiungere / modificare le varie porzioni di SQL generate automaticamente dalle molte molte righe di codice all'interno del core di WordPress. La firma del callback del filtro è: function posts_clauses_filter_cb( $clauses, $query_object ){ }e si aspetta che tu ritorni $clauses.

Le clausole

$clausesè un array che contiene le seguenti chiavi; ogni chiave è una stringa SQL che verrà utilizzata direttamente nell'istruzione SQL finale inviata al database:

  • dove
  • raggruppare per
  • aderire
  • ordinato da
  • distinto
  • i campi
  • limiti

Se stai aggiungendo una tabella al database (fallo solo se non riesci assolutamente a sfruttare post_meta, user_meta o tassonomie) probabilmente dovrai toccare più di una di queste clausole, ad esempio il fields(il "SELEZIONA" parte dell'istruzione SQL), la join(tutte le tabelle, tranne quella nella clausola "FROM") e forse la orderby.

Modifica delle clausole

Il modo migliore per farlo è sottoporre la chiave pertinente $clausesall'array ottenuto dal filtro:

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

Ora, se modifichi $join, in realtà modificherai direttamente in $clauses['join']modo che le modifiche vengano apportate $clausesquando lo restituisci.

Preservare le clausole originali

È probabile (no, seriamente, ascolta) che vorrai preservare l'SQL esistente che WordPress ha generato per te. In caso contrario, dovresti probabilmente guardare il posts_requestfiltro, ovvero la query mySQL completa appena prima che venga inviata al database, quindi puoi bloccarlo totalmente con il tuo. Perché vorresti farlo? Probabilmente no.

Quindi, al fine di preservare l'SQL esistente nelle clausole, ricordati di aggiungere alle clausole, non di assegnarle (cioè: usa $join .= ' {NEW SQL STUFF}';non $join = '{CLOBBER SQL STUFF}';. Nota che poiché ogni elemento $clausesdell'array è una stringa, se vuoi aggiungerlo, probabilmente vorrai inserire uno spazio prima di qualsiasi altro token di carattere, altrimenti creerai probabilmente un errore di sintassi SQL.

Puoi solo supporre che ci sarà sempre qualcosa in ciascuna delle clausole, quindi ricorda di iniziare ogni nuova stringa con uno spazio, come in: $join .= ' my_tableoppure, puoi sempre aggiungere una piccola riga che aggiunge uno spazio solo se è necessario:

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


return $clauses;

Questa è una cosa stilistica più di ogni altra cosa. Il bit importante da ricordare è: lasciare sempre uno spazio PRIMA della stringa se si aggiunge a una clausola che contiene già un codice SQL!

Mettendolo insieme

La prima regola dello sviluppo di WordPress è cercare di utilizzare quante più funzionalità di base possibile. Questo è il modo migliore per rendere il tuo lavoro a prova di futuro. Supponiamo che il core team decida che WordPress ora utilizzerà SQLite o Oracle o qualche altro linguaggio di database. Qualsiasi mySQL scritto a mano può diventare non valido e interrompere il tuo plugin o tema! Meglio consentire a WP di generare da solo il maggior numero possibile di SQL e aggiungere semplicemente i bit necessari.

Pertanto, il primo ordine del giorno è sfruttare la WP_Querymaggior quantità possibile di query di base. Il metodo esatto che utilizziamo per farlo dipende in gran parte da dove dovrebbe apparire questo elenco di post. Se si tratta di una sottosezione della pagina (non la query principale) che useresti get_posts(); se è la query principale, suppongo che potresti usarla query_posts()e farla finita, ma il modo corretto per farlo è intercettare la query principale prima che colpisca il database (e consumi i cicli del server), quindi usa il requestfiltro.

Ok, quindi hai generato la tua query e sta per essere creato l'SQL. Bene, in effetti, è stato creato, ma non inviato al database. Utilizzando il posts_clausesfiltro, aggiungerai la tabella delle relazioni con i dipendenti nel mix. Chiamiamo questa tabella {$ wpdb-> prefisso}. 'user_relationship', ed è una tabella di intersezione. (A proposito, ti consiglio di generalizzare questa struttura di tabella e trasformarla in una tabella di intersezione appropriata con i seguenti campi: 'relationship_id', 'user_id', 'related_user_id', 'relationship_type'; questo è molto più flessibile e potente. .. ma sto divagando).

Se capisco cosa vuoi fare, vuoi passare l'ID di un leader e quindi vedere solo i post dei follower di quel leader. Spero di aver capito bene. Se non è giusto, dovrai prendere ciò che dico e adattarlo alle tue esigenze. Seguirò la struttura del tuo tavolo: abbiamo a leader_ide a follower_id. Quindi JOIN sarà {$wpdb->posts}.post_authorattivo come chiave esterna per "follower_id" nella tabella "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;
}

13

Sto rispondendo a questa domanda molto tardi e mi scuso per lo stesso. Ero stato troppo impegnato con le scadenze per occuparmene.

Un grande ringraziamento a @ m0r7if3r e @kaiser nel fornire le soluzioni di base che potrei estendere e implementare nella mia applicazione. Questa risposta fornisce dettagli sul mio adattamento delle soluzioni offerte da @ m0r7if3r e @kaiser.

Innanzi tutto, lasciatemi spiegare perché questa domanda è stata posta in primo luogo. Dalla domanda e dai suoi commenti si potrebbe capire che sto cercando di convincere WP_Query a tirare post da tutti gli utenti (leader) seguiti da un determinato utente (follower). La relazione tra follower e leader viene archiviata in una tabella personalizzata follow. La soluzione più comune a questo problema è estrarre gli ID utente di tutti i leader di un follower dalla seguente tabella e posizionarli in un array. Vedi sotto:

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 volta che hai l'array di leader, puoi passarlo come argomento a WP_Query. Vedi sotto:

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 soluzione sopra è il modo più semplice per ottenere i risultati desiderati. Tuttavia, non è scalabile. Nel momento in cui hai un follower che segue decine e migliaia di leader, la serie risultante di ID leader diventerebbe estremamente grande e costringerebbe il tuo sito WordPress a utilizzare 100 MB - 250 MB di memoria su ogni caricamento della pagina e alla fine arrestare il sito. La soluzione al problema è eseguire la query SQL direttamente sul database e recuperare i post pertinenti. Fu allora che la soluzione di @ m0r7if3r venne in soccorso. Seguendo la raccomandazione di @ kaiser ho deciso di testare entrambe le implementazioni. Ho importato circa 47K utenti da un file CSV per registrarli su una nuova installazione di prova di WordPress. L'installazione eseguiva il tema Twenty Eleven. In seguito ho eseguito un ciclo for per fare in modo che circa 50 utenti seguano ogni altro utente. La differenza nel tempo di query per la soluzione di @kaiser e @ m0r7if3r era sconcertante. La soluzione di @ kaiser normalmente impiegava dai 2 ai 5 secondi per ogni query. La variazione che presumo si verifica quando WordPress memorizza nella cache le query per un uso successivo. D'altra parte, la soluzione di @ m0r7if3r ha dimostrato in media un tempo di query di 0,02 ms. Per testare entrambe le soluzioni ho avuto l'indicizzazione ON per la colonna leader_id. Senza indicizzazione si è verificato un notevole aumento del tempo di query.

L'utilizzo della memoria quando si utilizzava una soluzione basata su array era di circa 100-150 MB e scendeva a 20 MB durante l'esecuzione di un SQL diretto.

Ho avuto un bump con la soluzione di @ m0r7if3r quando avevo bisogno di passare l'ID follower alla funzione di filtro posts_where. Almeno, per quanto ne so, WordPress non consente di passare una variabile alle funzioni del filer. Tuttavia, puoi utilizzare le variabili globali, ma volevo evitare i globali. Ho finito per estendere WP_Query per risolvere finalmente il problema. Quindi ecco la soluzione finale che ho implementato (basato sulla soluzione di @ 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: alla fine ho provato la soluzione sopra con 1,2 milioni di voci nella seguente tabella. Il tempo medio di interrogazione è stato di circa 0,060 ms.


3
Non ti ho mai detto quanto ho apprezzato la discussione su questa domanda. Ora che ho scoperto che mi mancava, ho aggiunto un voto :)
Kaiser

8

Puoi farlo con una soluzione interamente SQL usando il posts_wherefiltro. Ecco un esempio:

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

Penso che ci possa essere anche un modo per farlo JOIN, ma non riesco a trovarlo. Continuerò a giocarci e aggiornerò la risposta se la ottengo.

In alternativa, come suggerito da @kaiser , puoi dividerlo in due parti: ottenere i leader e fare la domanda. Ho la sensazione che questo potrebbe essere meno efficiente, ma è sicuramente il modo più comprensibile di andare. Dovresti testare tu stesso l'efficienza per determinare quale metodo è migliore, poiché le query SQL nidificate possono diventare piuttosto lente.

DAI COMMENTI:

Dovresti inserire la funzione nel tuo functions.phpe fare il add_filter()giusto prima che venga chiamato il query()metodo di WP_Query. Immediatamente dopo ciò, è necessario in remove_filter()modo che non influisca sulle altre query.


1
Ho modificato la tua A e aggiunto prepare(). Spero non ti dispiaccia la modifica. E sì: le prestazioni devono essere misurate dall'OP. Comunque: penso ancora che questo dovrebbe essere semplicemente usermeta e nient'altro.
Kaiser

@ m0r7if3r Thx per provare una soluzione. Ho appena pubblicato un commento in risposta alla risposta di Kaiser, con preoccupazioni su possibili problemi di scalabilità. Ti preghiamo di prenderlo in considerazione.
Giovanni,

1
@kaiser Non importa minimamente, anzi lo apprezzo molto :)
mor7ifer,

@ m0r7if3r Grazie. Avere ragazzi come te nelle rocce della comunità :)
kaiser,

1
Dovresti inserire la funzione nel tuo functions.phpe fare il add_filter()giusto prima che venga chiamato il query()metodo di WP_Query. Immediatamente dopo ciò, è necessario in remove_filter()modo che non influisca sulle altre query. Non sono sicuro di quale sarebbe il problema con la riscrittura degli URL, l'ho usato posts_wherein molte occasioni e non l'ho mai visto ...
Mor7ifer,

6

Tag modello

Inserisci entrambe le funzioni nel tuo functions.phpfile. Quindi regola la 1a funzione e aggiungi il nome della tua tabella personalizzata. Quindi è necessario qualche tentativo / errore per sbarazzarsi dell'ID utente corrente all'interno dell'array risultante (vedere commento).

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

All'interno del modello

Qui puoi fare quello che vuoi con i tuoi risultati.

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

NOTA Abbiamo Non tiratevi abbiamo testdata, ecc in modo quanto sopra è un po 'di un gioco di indovinare. Assicurati di modificare questa risposta con ciò che ha funzionato per te, quindi abbiamo un risultato soddisfacente per i lettori successivi. Approverò la modifica nel caso in cui tu abbia un rappresentante troppo basso. Quindi puoi anche eliminare questa nota. Grazie.


2
JOINè molto più costoso. Inoltre: come ho già detto, non disponiamo di dati di test, quindi prova entrambe le risposte e illuminaci con i tuoi risultati.
Kaiser

1
WP_Query stesso funziona con JOIN tra la tabella dei post e postmeta durante le query. Ho visto un aumento dell'utilizzo della memoria PHP a 70 MB - 200 MB per caricamento della pagina. Eseguire qualcosa del genere con molti utenti simultanei richiederebbe un'infrastruttura estrema. La mia ipotesi sarebbe che dal momento che WordPress implementa già una tecnica simile, i JOIN dovrebbero essere meno tassativi rispetto al lavorare con una serie di ID.
John,

1
@Giovanni buoni da ascoltare. voglio davvero sapere il prossimo.
Kaiser

4
Ok ecco i risultati del test. Per questo ho aggiunto circa 47K utenti da un file CSV. Successivamente, ha eseguito un ciclo for per fare in modo che i primi 45 utenti seguano ogni altro utente. Ciò ha comportato 3.704.951 record salvati nella mia tabella personalizzata. Inizialmente, la soluzione di @ m0r7if3r mi ha dato un tempo di interrogazione di 95 secondi, che è sceso a 0,020 ms dopo aver attivato l'indicizzazione sulla colonna leader_id. La memoria totale di PHP consumata era di circa 20 MB. D'altra parte, la tua soluzione ha impiegato da 2 a 5 secondi per la query con l'indicizzazione attivata. La memoria totale di PHP consumata era di circa 117 MB.
Giovanni,

1
Aggiungo un'altra risposta (possiamo elaborarla e modificarla / modificarla) poiché la formattazione del codice nei commenti fa semplicemente schifo: P
kaiser,

3

Nota: questa risposta qui è per evitare discussioni estese nei commenti

  1. Ecco il codice OP dai commenti, per aggiungere la prima serie di utenti di test. Devo essere modificato in un esempio del mondo reale.

    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 Informazioni sul test Per questo ho aggiunto circa 47K utenti da un file CSV. Successivamente, ha eseguito un ciclo for per fare in modo che i primi 45 utenti seguano ogni altro utente.

    • Ciò ha comportato 3.704.951 record salvati nella mia tabella personalizzata.
    • Inizialmente, la soluzione di @ m0r7if3r mi ha dato un tempo di interrogazione di 95 secondi, che è sceso a 0,020 ms dopo aver attivato l'indicizzazione sulla colonna leader_id. La memoria totale di PHP consumata era di circa 20 MB.
    • D'altra parte, la soluzione ha impiegato da 2 a 5 secondi per la query con l'indicizzazione attivata. La memoria totale di PHP consumata era di circa 117 MB.
  2. La mia risposta a questo ↑ test:

    un test più "reale": lascia che ogni utente segua un $leader_amount = rand( 0, 5 );e quindi aggiungi il numero di $leader_amount x $random_ids = rand( 0, 47000 );a ciascun utente. Finora la cosa che sappiamo è: la mia soluzione sarebbe estremamente negativa se un utente si stesse seguendo. Inoltre: mostrerai come hai fatto il test e dove hai aggiunto esattamente i timer.

    Devo anche affermare che il ↑ sopra il monitoraggio del tempo non può essere realmente misurato, poiché ci vorrebbe anche il tempo per calcolare il ciclo insieme. Meglio sarebbe passare in rassegna l'insieme risultante di ID in un secondo ciclo.

ulteriore processo qui


2
Nota per coloro che hanno seguito questo Q: Sono in procinto di misurare le prestazioni in varie condizioni e pubblicherò il risultato in un giorno o 3. Questo è stato un compito estremamente dispendioso in termini di tempo a causa della scala dei dati di test che devono essere generato.
John,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.