Як розширити WP_Query, щоб включити власну таблицю в запит?


31

Я вже кілька днів займаюся цим питанням. Спочатку було так, як зберігати дані послідовників користувача в базі даних, для чого я отримав пару приємних рекомендацій у WordPress Answers. Після, дотримуючись рекомендацій, я додав таку нову таблицю:

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

У таблиці вище, у першому рядку є користувач з ідентифікатором 2, за яким слід користувач з ідентифікатором 4. У другому рядку слід користувач з ідентифікатором 3, а користувач із ідентифікатором з 10. Ця ж логіка застосовується і до третього ряду.

Тепер, по суті, я хочу розширити WP_Query, щоб я міг обмежувати повідомлення, отримані лише до цього, лише лідерами (користувачами) користувача. Отже, беручи до уваги вищевказану таблицю, якщо я повинен був передати ідентифікатор користувача 10 до WP_Query, результати повинні містити лише повідомлення за ідентифікатором користувача 2 та ідентифікатором користувача 3.

Я багато шукав, намагаючись знайти відповідь. Також я не бачив жодного підручника, який допоможе мені зрозуміти, як розширити клас WP_Query. Я бачив відповіді Майка Шінкеля (розширюючи WP_Query) на подібні запитання, але я дійсно не зрозумів, як застосувати його до моїх потреб. Було б чудово, якби хтось міг мені допомогти у цьому.

Посилання на відповіді Майка за запитом: Посилання 1 , Посилання 2


Додайте посилання на відповіді Майка, будь ласка.
кайзер

1
чи можете ви навести приклад того, про що ви б запитали? WP_Queryпризначений для отримання публікацій, і я не розумію, як це пов'язано з публікаціями.
mor7ifer

@kaiser Я оновив питання із посиланнями на відповіді Майка.
Джон

@ m0r7if3r »Я хочу розширити WP_Query так, щоб я міг обмежувати повідомлення, отримані лише тим, хто є лідером (ами) користувача«, подібним до «отримувати повідомлення від автора».
кайзер

2
@ m0r7if3r Повідомлення - це саме те, що мені потрібно запитувати. Але публікації, які слід отримувати, повинні бути користувачами, які вказані як лідери певного користувача у спеціальній таблиці. Отже, іншими словами, я хочу сказати WP_Query, перейдіть за всіма повідомленнями всіх користувачів, які вказані як лідери користувачів, що мають ідентифікатор "10" у спеціальній таблиці.
Джон

Відповіді:


13

Важлива відмова від відповідальності: правильний спосіб зробити це НЕ змінювати структуру таблиці, а використовувати wp_usermeta. Тоді вам не потрібно буде створювати будь-який користувацький SQL для запиту ваших публікацій (хоча вам все одно знадобиться певний спеціальний SQL, щоб отримати список усіх, хто звітує перед певним керівником - наприклад, у розділі Адміністратор). Однак, оскільки ОП запитала про написання користувальницького SQL, ось поточна найкраща практика введення спеціального SQL в існуючий WordPress Query.

Якщо ви робите складні приєднання, ви не можете просто використовувати фільтр posts_where, тому що вам потрібно буде змінити об'єднання, вибір і, можливо, групу, або упорядкувати за розділами запиту.

Краще всього скористатися фільтром "posts_clause". Це надзвичайно корисний фільтр (який не слід зловживати!), Який дозволяє додавати / змінювати різні частини SQL, які автоматично генеруються безліччю багатьох рядків коду в ядрі WordPress. Підпис зворотного дзвінка фільтра такий: function posts_clauses_filter_cb( $clauses, $query_object ){ }і він очікує повернення $clauses.

Статті

$clauses- масив, який містить такі ключі; кожен ключ - це рядок SQL, який буде безпосередньо використаний у підсумковому операторі SQL, відправленому до бази даних:

  • де
  • груповий
  • приєднатися
  • Сортувати по
  • виразний
  • поля
  • межі

Якщо ви додаєте таблицю в базу даних (зробіть це лише в тому випадку, якщо ви абсолютно не можете використовувати post_meta, user_meta або таксономії), можливо, вам доведеться торкнутися більше одного з цих пунктів, наприклад, fields("SELECT" частина заяви SQL), то join(все ваші таблиці, інші , ніж у вашому «FROM» статті), і можливо orderby.

Зміна застережень

Найкращий спосіб зробити це - перенаправити відповідний ключ з $clausesмасиву, який ви отримали з фільтра:

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

Тепер, якщо ви модифікуєте $join, ви насправді будете безпосередньо модифікувати, $clauses['join']так що зміни будуть, $clausesколи ви повернете їх.

Збереження оригінальних статей

Швидше за все, (ні, серйозно, слухайте) ви захочете зберегти існуючий SQL, який WordPress створив для вас. Якщо ні, то, напевно, слід подивитися на posts_requestфільтр - це повний запит mySQL безпосередньо перед тим, як він буде відправлений у базу даних, тож ви можете повністю заглушити його власним. Чому б ти хотів це зробити? Ви, мабуть, ні.

Отже, щоб зберегти існуючий SQL у застереженнях, не забудьте додати до них пункти, а не призначати їх (тобто: $join .= ' {NEW SQL STUFF}';не використовувати $join = '{CLOBBER SQL STUFF}';. Зауважте, що оскільки кожен елемент $clausesмасиву є рядком, якщо ви хочете до нього додати, ви, ймовірно, захочете вставити пробіл перед будь-якими іншими символами символів, інакше ви, ймовірно, створите деяку помилку синтаксису SQL.

Ви можете просто припустити, що у кожному із застережень завжди буде щось, і тому не забудьте почати кожну нову рядок з пробілу, як у: $join .= ' my_tableабо, ви завжди можете додати невеликий рядок, який додає пробіл лише у випадку, якщо вам потрібно:

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


return $clauses;

Це стилістична річ більше, ніж усе інше. Важливий біт, який слід пам’ятати: завжди залишайте пробіл перед РОБОМ, якщо ви додаєте до пункту, який вже містить SQL!

Збираючи його разом

Перше правило розробки WordPress - спробувати використовувати якнайбільше основних функціональних можливостей. Це найкращий спосіб підтвердити свою роботу в майбутньому. Припустимо, основна команда вирішить, що WordPress тепер буде використовувати SQLite або Oracle або якусь іншу мову бази даних. Будь-який написаний вручну mySQL може стати недійсним і зламати ваш плагін або тему! Краще дозволити WP генерувати якомога більше SQL, а просто додати потрібні біти.

Тому перший порядок бізнесу - це використання WP_Queryмаксимальної кількості базових запитів, наскільки це можливо. Точний метод, який ми використовуємо для цього, багато в чому залежить від того, де має з’являтися цей список публікацій. Якщо це підрозділ сторінки (не ваш основний запит), який ви використовуєте get_posts(); якщо це основний запит, я вважаю, що ви можете його використовувати query_posts()і виконувати з ним, але правильний спосіб зробити це - перехопити головний запит, перш ніж він потрапить у базу даних (і споживає серверні цикли), тому використовуйте requestфільтр.

Гаразд, значить, ви створили запит, і SQL збирається створити. Ну, насправді він створений, просто не відправлений у базу даних. Використовуючи posts_clausesфільтр, ви збираєтеся додати таблицю стосунків зі своїми працівниками у суміш. Назвемо цю таблицю {$ wpdb-> префікс}. 'user_relationship', і це таблиця перетину. (До речі, я рекомендую вам генерізувати цю структуру таблиці та перетворити її у відповідну таблицю перетину з такими полями: 'Relationship_id', 'user_id', 'related_user_id', 'Relations_type'; це набагато більш гнучко та потужно. .. але я відволікаюсь).

Якщо я розумію, що ви хочете зробити, ви хочете передати посвідчення лідера, а потім побачити лише повідомлення підписників цього лідера. Я сподіваюся, що я отримав це право. Якщо це неправильно, вам доведеться взяти те, що я кажу, і адаптувати це до ваших потреб. Я дотримуюся вашої структури столу: у нас є leader_idа follower_id. Таким чином, ПРИЄДНАЙТЕ буде працювати {$wpdb->posts}.post_authorяк зовнішній ключ до "follower_id" у вашій таблиці "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

Я відповідаю на це питання вкрай пізно, і свої вибачення за те саме. Я був занадто зайнятий строками, щоб взяти участь у цьому.

Велика подяка @ m0r7if3r та @kaiser за надання базових рішень, які я міг би розширити та впровадити у своїй програмі. Ця відповідь дає детальну інформацію про мою адаптацію рішень, запропонованих @ m0r7if3r та @kaiser.

По-перше, дозвольте мені пояснити, чому це питання задавали в першу чергу. З питання та коментарів до цього можна зрозуміти, що я намагаюся отримати WP_Query для отримання публікацій усіх користувачів (лідерів), за якими дотримується даний користувач (підписник). Відносини між послідовником і лідером зберігаються у спеціальній таблиці follow. Найпоширенішим рішенням цієї проблеми є витягнути ідентифікатори користувачів усіх лідерів послідовника з таблиці слідування і розмістити його в масиві. Дивись нижче:

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;

Коли у вас є масив лідерів, ви можете передати його як аргумент WP_Query. Дивись нижче:

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

Наведене вище рішення - найпростіший спосіб досягти бажаних результатів. Однак це не масштабується. У той момент, коли у вас є підписник, який слідує за десятками і тисячами лідерів, результат масиву ідентифікаторів лідера стане надзвичайно великим і змусить ваш сайт WordPress використовувати 100MB - 250MB пам'яті при завантаженні кожної сторінки і врешті-решт завершити роботу сайту. Рішення проблеми полягає в запуску SQL-запиту безпосередньо на базі даних та набору відповідних постів. Ось тоді на допомогу прийшло рішення @ m0r7if3r. Дотримуючись рекомендації @ kaiser, я поставив за мету перевірити обидві реалізації. Я імпортував близько 47 тис. Користувачів з CSV-файлу, щоб зареєструвати їх на новій тестовій установці WordPress. Встановлення виконувало тему Twenty Eleven. Після цього я запустив цикл for, щоб приблизно 50 користувачів підписалися на кожного іншого користувача. Різниця в часі запитів як для рішення @kaiser, так і для @ m0r7if3r була приголомшливою. Рішення @ kaiser зазвичай займало від 2 до 5 секунд для кожного запиту. Я вважаю, що варіант, коли WordPress кешує запити для подальшого використання. З іншого боку, рішення @ m0r7if3r продемонструвало час запиту в середньому 0,02 мс. Для тестування обох рішень я індексував значення ON для стовпця leader_id. Без індексування відбулося різке збільшення часу запитів.

Використання пам'яті при використанні рішення на основі масиву становило близько 100-150 Мбайт і знизилося до 20 МБ при запуску прямого SQL.

Я натиснув нарізку з рішенням @ m0r7if3r, коли мені потрібно було передати ідентифікатор послідовника до функції фільтра posts_where. Принаймні, за моїми знаннями, WordPress не дає можливості передавати змінну функціям файлів. Ви можете використовувати глобальні змінні, хоча я хотів уникнути глобальних змінних. Я врешті розширив WP_Query, щоб нарешті вирішити проблему. Ось ось остаточне рішення, яке я реалізував (на основі рішення @ 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 );

Примітка: я врешті-решт спробував вищевказане рішення із 1,2 мільйонами записів у наступній таблиці. Середній час запиту становив близько 0,060 мс.


3
Я ніколи не говорив вам, наскільки я ціную дискусію з цього питання. Тепер, коли я дізнався, що я пропустив це, я додав підсумок :)
kaiser

8

Це можна зробити за допомогою рішення повністю SQL за допомогою posts_whereфільтра. Ось приклад цього:

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

Я думаю, що може бути і спосіб це зробити JOIN, але я не можу цього придумати. Я буду продовжувати грати з ним і оновити відповідь, якщо отримаю.

Крім того, як @kaiser запропонував, ви можете розділити його на дві частини: отримання лідерів і виконання запиту. У мене є відчуття, що це може бути менш ефективно, але це, безумовно, більш зрозумілий шлях. Вам доведеться перевірити ефективність для себе, щоб визначити, який метод краще, оскільки вкладені SQL запити можуть надходити досить повільно.

З КОМЕНТАРІВ:

Ви повинні поставити функцію у своє functions.phpі зробити add_filter()правильно перед query()викликом методу WP_Query. Відразу після цього слід remove_filter()зробити так, щоб він не впливав на інші запити.


1
Редагував свій А та додав prepare(). Сподіваюсь, ви не заперечуєте проти редагування. І так: Продуктивність має бути виміряна ОП. У будь-якому випадку: я все ще думаю, що це повинно бути просто usermeta і більше нічого.
кайзер

@ m0r7if3r Thx для спроби рішення. Я щойно опублікував коментар у відповідь на відповідь кайзера, з побоюванням щодо можливих питань масштабування. Будь ласка, врахуйте це.
Джон

1
@kaiser Не заперечуй принаймні, адже я радше ціную це :)
mor7ifer

@ m0r7if3r Дякую Такі хлопці, як ви, у спільноті скелі :)
кайзер

1
Ви повинні поставити функцію у своє functions.phpі зробити add_filter()правильно перед query()викликом методу WP_Query. Відразу після цього слід remove_filter()зробити так, щоб він не впливав на інші запити. Я не впевнений, у чому проблема з переписуванням URL-адрес, я posts_whereбагато разів використовував і ніколи цього не бачив ...
mor7ifer

6

Тег шаблонів

Просто розмістіть обидві функції у вашому functions.phpфайлі. Потім відрегулюйте першу функцію та додайте назву власної таблиці. Тоді вам знадобиться кілька спроб / помилок, щоб позбутися поточного ідентифікатора користувача всередині отриманого масиву (див. Коментар).

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

Всередині шаблону

Тут ви можете робити все, що завгодно, зі своїми результатами.

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

Примітка Ми do not є TestData і т.д. , так що вище трохи угадайку. Переконайтесь, що ви відредагували цю відповідь тим, що працювало для вас, щоб ми отримали задоволення для наступних читачів. Я схвалюю редагування у випадку, якщо у вас занадто низька кількість представників. Потім ви можете також видалити цю примітку. Спасибі.


2
JOINце набагато дорожче. Плюс: Як я вже згадував, у нас немає даних тестів, тому будь ласка, протестуйте обидві відповіді та просвітіть нас своїми результатами.
кайзер

1
Сам WP_Query працює з JOIN між таблицею повідомлень і postmeta при запиті. Я бачив накопичення використання PHP-пам’яті до 70 Мб - 200 Мб на завантаження сторінки. Для роботи подібного з багатьма одночасними користувачами знадобиться надзвичайна інфраструктура. Я гадаю, що оскільки WordPress вже застосовує подібну методику, JOIN має бути меншою мірою оподаткування порівняно з роботою з масивом ідентифікаторів.
Джон

1
@John приємно чути. дуже хочеться знати результат.
кайзер

4
Ок, ось результати тесту. Для цього я додав близько 47K користувачів із CSV-файлу. Пізніше було запущено цикл a для того, щоб перші 45 користувачів перейшли за кожним іншим користувачем. Це призвело до 3,704,951 записів, збережених у моїй спеціальній таблиці. Спочатку рішення @ m0r7if3r дало мені час запиту 95 секунд, який знизився до 0,020 мс після включення індексації в стовпці leader_id. Загальна споживана пам'ять PHP становила близько 20 Мб. З іншого боку, на ваше запитання знадобилося приблизно 2 - 5 секунд для запиту з індексуванням ON. Загальна споживана пам'ять PHP становила близько 117 Мб.
Джон

1
Я додаю ще одну відповідь (ми можемо обробляти та змінювати / редагувати на цьому), оскільки форматування коду в коментарях просто відстійне: P
kaiser

3

Примітка. Ця відповідь тут - щоб уникнути розширеного обговорення в коментарях

  1. Ось ОП ОЗ з коментарів, щоб додати перший набір тестових користувачів. Мені належить змінити приклад із реального світу.

    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 Для цього я додав близько 47K користувачів із CSV-файлу. Пізніше було запущено цикл a для того, щоб перші 45 користувачів підписалися на кожного іншого користувача.

    • Це призвело до 3,704,951 записів, збережених у моїй спеціальній таблиці.
    • Спочатку рішення @ m0r7if3r дало мені час запиту 95 секунд, який знизився до 0,020 мс після включення індексації в стовпці leader_id. Загальна споживана пам'ять PHP становила близько 20 Мб.
    • З іншого боку, на ваше запитання знадобилося приблизно 2 - 5 секунд для запиту з індексуванням ON. Загальна споживана пам'ять PHP становила близько 117 Мб.
  2. Моя відповідь на цей ↑ тест:

    більш тест "реального життя": нехай кожен користувач слідує за a, $leader_amount = rand( 0, 5 );а потім додасть кількість $leader_amount x $random_ids = rand( 0, 47000 );кожного користувача. Поки що ми знаємо: моє рішення було б дуже погано, якщо користувач слідкує за одним користувачем. Далі: Ви покажете, як ви зробили тест і де саме ви додали таймери.

    Я також повинен зазначити, що ↑ вище відстеження часу не може бути дійсно виміряним, оскільки це також потребує часу для обчислення циклу разом. Краще було б провести цикл через отриманий набір ідентифікаторів у другому циклі.

подальший процес тут


2
Примітка для тих, хто дотримувався цього запитання: я перебуваю в процесі вимірювання продуктивності за різних умов і опублікую результат через день або 3. Це було надзвичайно трудомістким завданням через масштаб даних тестів, які потребують згенеровано.
Джон
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.