Jak rozszerzyć WP_Query o niestandardową tabelę w zapytaniu?

31

Miałem już kilka dni nad tym problemem. Początkowo było to, jak przechowywać dane obserwujących użytkownika w bazie danych, dla których dostałem kilka fajnych rekomendacji tutaj w WordPress Answers. Następnie, zgodnie z zaleceniami, dodałem nową tabelę:

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

W powyższej tabeli pierwszy wiersz ma użytkownika o identyfikatorze 2, po którym następuje użytkownik o identyfikatorze 4. W drugim wierszu za użytkownikiem o identyfikatorze 3 znajduje się użytkownik o identyfikatorze z 10. Ta sama logika dotyczy trzeciego rzędu.

Teraz zasadniczo chcę rozszerzyć WP_Query, aby móc ograniczać posty pobierane do tego tylko przez lidera (liderów) użytkownika. Tak więc, biorąc pod uwagę powyższą tabelę, gdybym przekazał identyfikator użytkownika 10 do WP_Query, wyniki powinny zawierać tylko posty według identyfikatora użytkownika 2 i identyfikatora użytkownika 3.

Dużo szukałem, próbując znaleźć odpowiedź. Nie widziałem też żadnego samouczka, który pomógłby mi zrozumieć, jak rozszerzyć klasę WP_Query. Widziałem odpowiedzi Mike'a Schinkela (rozszerzenie WP_Query) na podobne pytania, ale tak naprawdę nie rozumiem, jak zastosować je do moich potrzeb. Byłoby wspaniale, gdyby ktoś mógł mi w tym pomóc.

Linki do odpowiedzi Mike'a zgodnie z prośbą: Link 1 , Link 2

Jan
źródło
Dodaj link do odpowiedzi Mikesa.
kaiser
1
czy możesz podać przykład tego, o co pytasz? WP_Querysłuży do otrzymywania postów i nie rozumiem, w jaki sposób wiąże się to z postami.
mor7ifer
@kaiser Zaktualizowałem pytanie o linki do odpowiedzi Mike'a.
John
@ m0r7if3r »Chcę rozszerzyć WP_Query, abym mógł ograniczać posty pobrane do tego tylko przez lidera (ów) użytkownika«, tak podobne do „get postów autora”.
kaiser
2
@ m0r7if3r Posty to dokładnie to, o co muszę zapytać. Jednak posty, które mają być pobierane, powinny być tworzone przez użytkowników wymienionych w tabeli niestandardowej jako liderzy określonego użytkownika. Innymi słowy, chcę powiedzieć WP_Query, idź pobrać wszystkie posty wszystkich użytkowników wymienionych na liście liderów użytkownika o identyfikatorze „10” w niestandardowej tabeli.
John

Odpowiedzi:

13

Ważna informacja: właściwym sposobem na to NIE jest modyfikowanie struktury tabeli, ale użycie wp_usermeta. Wówczas nie będziesz musiał tworzyć niestandardowego kodu SQL do wysyłania zapytań do swoich postów (choć nadal potrzebujesz niestandardowego kodu SQL, aby uzyskać listę wszystkich, którzy zgłaszają się do określonego przełożonego - na przykład w sekcji Administrator). Ponieważ jednak OP poprosił o napisanie niestandardowego SQL, oto najlepsza praktyka wprowadzania niestandardowego SQL do istniejącego zapytania WordPress.

Jeśli wykonujesz złożone sprzężenia, nie możesz po prostu użyć filtra posts_where, ponieważ musisz zmodyfikować sprzężenie, zaznaczenie i ewentualnie grupę według lub uporządkować według sekcji zapytania.

Najlepiej jest użyć filtra „posts_clauses”. Jest to bardzo przydatny filtr (który nie powinien być nadużywany!), Który pozwala dodawać / modyfikować różne części SQL generowane automatycznie przez wiele wierszy kodu w rdzeniu WordPress. Sygnatura oddzwaniania filtra to: function posts_clauses_filter_cb( $clauses, $query_object ){ }i oczekuje na powrót $clauses.

Klauzule

$clausesjest tablicą zawierającą następujące klucze; każdy klucz jest ciągiem SQL, który zostanie bezpośrednio użyty w końcowej instrukcji SQL wysłanej do bazy danych:

  • gdzie
  • Grupuj według
  • Przystąpić
  • Zamów przez
  • odrębny
  • pola
  • limity

Jeśli dodajesz tabelę do bazy danych (rób to tylko wtedy, gdy absolutnie nie możesz wykorzystać post_meta, user_meta lub taksonomii) prawdopodobnie będziesz musiał dotknąć więcej niż jednej z tych klauzul, na przykład fields(„WYBIERZ” część instrukcji SQL), join(wszystkie tabele, inne niż w klauzuli „FROM”) i być może orderby.

Modyfikowanie klauzul

Najlepszym sposobem na to jest odjęcie odpowiedniego klucza z $clausestablicy uzyskanej z filtra:

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

Teraz, jeśli zmodyfikujesz $join, będziesz modyfikować bezpośrednio, $clauses['join']więc zmiany zostaną wprowadzone $clausespo jego zwróceniu.

Zachowanie oryginalnych klauzul

Możliwe, że (nie, poważnie, słuchaj) będziesz chciał zachować istniejący SQL wygenerowany dla Ciebie przez WordPress. Jeśli nie, prawdopodobnie powinieneś posts_requestzamiast tego spojrzeć na filtr - jest to kompletne zapytanie mySQL tuż przed wysłaniem go do bazy danych, abyś mógł całkowicie zablokować go własnym. Dlaczego chcesz to zrobić? Prawdopodobnie nie.

Aby zachować istniejący kod SQL w klauzulach, pamiętaj o dołączeniu do klauzul, a nie przypisaniu do nich (tj.: $join .= ' {NEW SQL STUFF}';Nie używaj $join = '{CLOBBER SQL STUFF}';. Zauważ, że ponieważ każdy element $clausestablicy jest łańcuchem, jeśli chcesz do niego dołączyć, prawdopodobnie będziesz chciał wstawić spację przed innymi tokenami znaków, w przeciwnym razie prawdopodobnie utworzysz jakiś błąd składni SQL.

Możesz po prostu założyć, że zawsze będzie coś w każdej z klauzul, więc pamiętaj, aby rozpocząć każdy nowy ciąg spacją, jak w:, $join .= ' my_tablelub zawsze możesz dodać małą linię, która dodaje spację tylko wtedy, gdy potrzebujesz:

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


return $clauses;

To stylistyczna rzecz bardziej niż cokolwiek innego. Ważną rzeczą do zapamiętania jest: zawsze zostawiaj spację PRZED łańcuchem, jeśli dołączasz do klauzuli, która zawiera już trochę SQL!

Składając to razem

Pierwszą zasadą rozwoju WordPressa jest próba użycia jak największej liczby podstawowych funkcji. To najlepszy sposób, aby w przyszłości sprawdzić swoją pracę. Załóżmy, że główny zespół zdecyduje, że WordPress będzie teraz używać SQLite, Oracle lub innego języka baz danych. Każdy ręcznie napisany mySQL może stać się nieważny i uszkodzić wtyczkę lub motyw! Lepiej pozwolić WP generować tak dużo SQL, jak to możliwe, i po prostu dodać potrzebne bity.

Tak więc pierwsze zamówienie firmy wykorzystuje WP_Querydo wygenerowania jak największej liczby zapytań podstawowych. Dokładna metoda, której używamy, zależy w dużej mierze od tego, gdzie ta lista postów ma się pojawić. Jeśli jest to podsekcja strony (nie twoje główne zapytanie), skorzystasz get_posts(); jeśli jest to główne zapytanie, myślę, że można go użyć query_posts()i gotowe, ale właściwym sposobem na to jest przechwycenie głównego zapytania, zanim trafi ono do bazy danych (i zużyje cykle serwera), więc użyj requestfiltru.

OK, więc wygenerowałeś zapytanie i SQL zostanie wkrótce stworzony. Cóż, w rzeczywistości został stworzony, po prostu nie wysłany do bazy danych. Korzystając z posts_clausesfiltra, dodasz tabelę relacji pracowniczych do miksu. Nazwijmy tę tabelę {$ wpdb-> prefix}. „user_relationship” i jest to tabela skrzyżowań. (Nawiasem mówiąc, zalecam uogólnienie tej struktury tabeli i przekształcenie jej w odpowiednią tabelę przecięcia z następującymi polami: „id_użytkownika”, „id_użytkownika”, „id_użytkownika_użytkownika”, „typ_użytkownika”; jest to o wiele bardziej elastyczne i wydajne. .. ale dygresję).

Jeśli rozumiem, co chcesz zrobić, chcesz przekazać identyfikator lidera, a następnie zobaczyć tylko posty obserwujących tego lidera. Mam nadzieję, że dobrze to zrozumiałem. Jeśli to nie w porządku, musisz wziąć to, co mówię i dostosować to do swoich potrzeb. Pozostanę przy twojej strukturze stołu: mamy a leader_idi a follower_id. Tak więc JOIN będzie włączony {$wpdb->posts}.post_authorjako klucz obcy do „follower_id” w tabeli „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
źródło
13

Odpowiadam na to pytanie bardzo późno i przepraszam za to samo. Byłem zbyt zajęty terminami, aby dotrzymać tego terminu.

Ogromne podziękowania dla @ m0r7if3r i @kaiser w zapewnieniu podstawowych rozwiązań, które mogłem rozszerzyć i wdrożyć w mojej aplikacji. Ta odpowiedź zawiera szczegółowe informacje na temat mojej adaptacji rozwiązań oferowanych przez @ m0r7if3r i @kaiser.

Najpierw wyjaśnię, dlaczego pytanie to zostało zadane w pierwszej kolejności. Z pytania i jego komentarzy można wywnioskować, że staram się, aby WP_Query ściągał posty wszystkich użytkowników (liderów), za którymi podąża dany użytkownik (obserwujący). Relacje między obserwującym a liderem są przechowywane w niestandardowej tabeli follow. Najczęstszym rozwiązaniem tego problemu jest pobranie identyfikatorów użytkowników wszystkich liderów obserwujących z tabeli śledzenia i umieszczenie ich w tablicy. Patrz poniżej:

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;

Gdy masz już tablicę liderów, możesz przekazać ją jako argument do WP_Query. Patrz poniżej:

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

Powyższe rozwiązanie jest najprostszym sposobem na osiągnięcie moich pożądanych rezultatów. Jest jednak nieskalowalny. W momencie, gdy obserwujesz obserwujących dziesiątki i tysiące liderów, wynikowa tablica identyfikatorów liderów byłaby bardzo duża i zmusiłaby twoją witrynę WordPress do użycia 100 MB - 250 MB pamięci przy każdym ładowaniu strony i ostatecznie zawiesiłaby stronę. Rozwiązaniem problemu jest uruchomienie zapytania SQL bezpośrednio w bazie danych i pobranie odpowiednich postów. Właśnie wtedy rozwiązanie @ m0r7if3r przyszło na ratunek. Zgodnie z zaleceniem @ kaiser postanowiłem przetestować obie implementacje. Zaimportowałem około 47 000 użytkowników z pliku CSV, aby zarejestrować ich podczas nowej instalacji testowej WordPress. W instalacji działał motyw Twenty Eleven. Następnie uruchomiłem pętlę for, aby około 50 użytkowników śledziło każdego innego użytkownika. Różnica w czasie zapytania dla rozwiązania @kaiser i @ m0r7if3r była oszałamiająca. Rozwiązanie @ kaiser zwykle zajęło około 2 do 5 sekund dla każdego zapytania. Odmiana, którą zakładam, dzieje się, gdy WordPress buforuje zapytania do późniejszego wykorzystania. Z drugiej strony rozwiązanie @ m0r7if3r wykazało czas zapytania średnio 0,02 ms. Do testowania obu rozwiązań miałem indeksowanie WŁĄCZONE w kolumnie lider_id. Bez indeksowania nastąpił gwałtowny wzrost czasu zapytania.

Zużycie pamięci podczas korzystania z rozwiązania opartego na macierzy wynosiło około 100-150 MB i spadło do 20 MB podczas uruchamiania bezpośredniego SQL.

Wystąpił problem z rozwiązaniem @ m0r7if3r, gdy potrzebowałem przekazać identyfikator obserwującego do funkcji filtrowania posts_where. Co najmniej, zgodnie z moją wiedzą, WordPress nie zezwala na przekazywanie zmiennych do funkcji filtrowania. Możesz jednak użyć zmiennych globalnych, ale chciałem uniknąć globalnych. Ostatecznie rozszerzyłem WP_Query, aby w końcu rozwiązać ten problem. Oto ostatnie rozwiązanie, które wdrożyłem (oparte na rozwiązaniu @ 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 );

Uwaga: ostatecznie wypróbowałem powyższe rozwiązanie z 1,2 milionem wpisów w poniższej tabeli. Średni czas zapytania wynosił około 0,060 ms.

Jan
źródło
3
Nigdy nie mówiłem, jak bardzo doceniam dyskusję na ten temat. Teraz, gdy dowiedziałem się, że mi tego brakowało, dodałem opinię:
kaiser
8

Możesz to zrobić za pomocą rozwiązania całkowicie SQL przy użyciu posts_wherefiltra. Oto przykład:

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

Myślę, że może to być również sposób JOIN, ale nie mogę tego wymyślić. Będę się z nią bawił i zaktualizuję odpowiedź, jeśli ją otrzymam.

Alternatywnie, jak sugerował @kaiser , możesz podzielić go na dwie części: uzyskanie liderów i wykonanie zapytania. Mam wrażenie, że może to być mniej wydajne, ale z pewnością jest to bardziej zrozumiała droga. Będziesz musiał sam przetestować wydajność, aby określić, która metoda jest lepsza, ponieważ zagnieżdżone zapytania SQL mogą działać dość wolno.

KOMENTARZE:

Powinieneś umieścić tę funkcję w swoim functions.phpi zrobić to add_filter()dobrze przed wywołaniem query()metody WP_QueryNatychmiast po tym powinieneś, remove_filter()aby nie wpłynęło to na inne zapytania.

mor7ifer
źródło
1
Zmodyfikowałem swoje A i dodałem prepare(). Mam nadzieję, że nie przeszkadza ci edycja. I tak: wydajność musi być mierzona przez OP. W każdym razie: nadal uważam, że powinna to być po prostu usermeta i nic więcej.
kaiser
@ m0r7if3r Dzięki za wypróbowanie rozwiązania. Właśnie opublikowałem komentarz w odpowiedzi na odpowiedź Kaiser, z obawami dotyczącymi możliwych problemów ze skalowalnością. Proszę, weź to pod uwagę.
John
1
@kaiser Nie przejmuj się, w rzeczywistości raczej to doceniam :)
mor7ifer 26.04.
@ m0r7if3r Dzięki. Posiadanie facetów takich jak ty w skałach społeczności :)
kaiser
1
Powinieneś umieścić tę funkcję w swoim functions.phpi zrobić to add_filter()dobrze przed wywołaniem query()metody WP_QueryNatychmiast po tym powinieneś, remove_filter()aby nie wpłynęło to na inne zapytania. Nie jestem pewien, jaki byłby problem z przepisywaniem adresów URL, wielokrotnie korzystałem posts_wherei nigdy tego nie widziałem ...
mor7ifer,
6

Tag szablonu

Po prostu umieść obie funkcje w swoim functions.phppliku. Następnie dostosuj pierwszą funkcję i dodaj niestandardową nazwę tabeli. Następnie potrzebujesz metody try / error, aby pozbyć się bieżącego ID użytkownika w wynikowej tablicy (patrz komentarz).

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

Wewnątrz szablonu

Tutaj możesz robić, co chcesz z wynikami.

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

UWAGA We Do not mają testdata itp więc powyższe jest trochę gra w zgadywanie. Upewnij się, że jesteś edytować tę odpowiedź z co pracował dla ciebie, więc mamy satysfakcjonujące rezultaty dla późniejszych czytelników. Zatwierdzę zmianę, jeśli masz za mało powtórzeń. Następnie możesz również usunąć tę notatkę. Dzięki.

kajzer
źródło
2
JOINjest znacznie droższy. Plus: jak wspomniałem, nie mamy danych testowych, więc proszę przetestuj obie odpowiedzi i oświeć nas swoimi wynikami.
kaiser
1
WP_Query samo działa z JOIN między tabelą postów a postmeta podczas zapytania. Widziałem, że użycie pamięci PHP spadło do 70 MB - 200 MB na ładowanie strony. Uruchomienie czegoś takiego z wieloma jednoczesnymi użytkownikami wymagałoby ekstremalnej infrastruktury. Domyślam się, że ponieważ WordPress już implementuje podobną technikę, JOIN powinny być mniej podatkowe w porównaniu do pracy z tablicą identyfikatorów.
Jan
1
@John dobrze słyszeć. naprawdę chcę poznać nadchodzące.
kaiser
4
Ok, oto wyniki testu. W tym celu dodałem około 47 000 użytkowników z pliku csv. Później uruchomiono pętlę for, aby pierwszych 45 użytkowników śledziło każdego innego użytkownika. Spowodowało to zapisanie 3 704 951 rekordów w mojej tabeli niestandardowej. Początkowo rozwiązanie @ m0r7if3r dało mi czas zapytania wynoszący 95 sekund, który spadł do 0,020 ms po włączeniu indeksowania w kolumnie lider_id. Całkowite zużycie pamięci PHP wyniosło około 20 MB. Z drugiej strony rozwiązanie zajęło około 2 do 5 sekund na zapytanie z włączonym indeksowaniem. Całkowite zużycie pamięci PHP wyniosło około 117 MB.
John
1
Dodam kolejną odpowiedź (możemy to przetwarzać i modyfikować / edytować), ponieważ formatowanie kodu w komentarzach jest po prostu do kitu: P
kaiser
3

Uwaga: Ta odpowiedź ma na celu uniknięcie rozszerzonej dyskusji w komentarzach

  1. Oto kod OP z komentarzy, aby dodać pierwszy zestaw użytkowników testowych. Muszę zostać zmodyfikowany na przykład z prawdziwego świata.

    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 O teście Do tego dodałem około 47 tysięcy użytkowników z pliku csv. Później uruchomiono pętlę for, aby pierwszych 45 użytkowników śledziło każdego innego użytkownika.

    • Spowodowało to zapisanie 3 704 951 rekordów w mojej tabeli niestandardowej.
    • Początkowo rozwiązanie @ m0r7if3r dało mi czas zapytania wynoszący 95 sekund, który spadł do 0,020 ms po włączeniu indeksowania w kolumnie lider_id. Całkowite zużycie pamięci PHP wyniosło około 20 MB.
    • Z drugiej strony rozwiązanie zajęło około 2 do 5 sekund na zapytanie z włączonym indeksowaniem. Całkowite zużycie pamięci PHP wyniosło około 117 MB.
  2. Moja odpowiedź na ten test ↑:

    bardziej „rzeczywisty” test: pozwól każdemu użytkownikowi śledzić, $leader_amount = rand( 0, 5 );a następnie dodaj liczbę $leader_amount x $random_ids = rand( 0, 47000 );do każdego użytkownika. Jak dotąd wiemy, że: moje rozwiązanie byłoby wyjątkowo złe, jeśli użytkownik podąża za sobą. Ponadto: pokażesz, jak wykonałeś test i gdzie dokładnie dodałeś liczniki.

    Muszę też stwierdzić, że śledzenia nad czasem ↑ nie da się tak naprawdę zmierzyć, ponieważ zajęłoby to również czas na obliczenie pętli razem. Lepiej byłoby zapętlić wynikowy zestaw identyfikatorów w drugiej pętli.

dalszy proces tutaj

kajzer
źródło
2
Uwaga dla tych, którzy śledzili to pytanie: Jestem w trakcie pomiaru wydajności w różnych warunkach i opublikuję wynik w ciągu dnia lub 3. To było niezwykle czasochłonne zadanie ze względu na skalę danych testowych, które muszą być wygenerowane.
Jan