Как расширить WP_Query для включения настраиваемой таблицы в запрос?

31

Я был дни над этой проблемой сейчас. Первоначально это было, как хранить данные о подписчиках пользователя в базе данных, для чего я получил пару хороших рекомендаций здесь, на WordPress Ответы. После, следуя рекомендациям, я добавил новую таблицу, подобную этой:

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 для получения списка всех, кто отчитывается перед конкретным супервизором - например, в разделе Admin). Тем не менее, поскольку OP спросил о написании пользовательского SQL, в настоящее время рекомендуется внедрить пользовательский SQL в существующий запрос WordPress.

Если вы выполняете сложные объединения, вы не можете просто использовать фильтр posts_where, потому что вам нужно будет изменить объединение, выбор и, возможно, группирование или упорядочение по разделам запроса.

Лучше всего использовать фильтр «posts_clauses». Это очень полезный фильтр (которым не следует злоупотреблять!), Который позволяет вам добавлять / изменять различные части 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-> prefix}. 'user_relationship', и это таблица пересечений. (Кстати, я рекомендую вам обобщить эту структуру таблицы и превратить ее в правильную таблицу пересечений со следующими полями: 'отношение_идентификатора', 'идентификатор_пользователя', 'связанный_пользователь_идентификатор', 'тип_отношения'; это гораздо более гибкий и мощный инструмент. .. но я отвлекся)

Если я понимаю, что вы хотите сделать, вы хотите передать идентификатор лидера, а затем просматривать только сообщения от подписчиков этого лидера. Я надеюсь, что понял это правильно. Если это неправильно, вам придется взять то, что я говорю, и адаптировать его к вашим потребностям. Я придерживаюсь вашей структуры таблицы: у нас есть leader_idи follower_id. Таким образом, JOIN будет {$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 использовать 100–250 МБ памяти при каждой загрузке страницы и в конечном итоге привести к сбою сайта. Решением проблемы является запуск 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 не позволяет передавать переменные в функции filer. Вы можете использовать глобальные переменные, но я хотел избежать глобальных. В итоге я расширил 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
Я никогда не говорил вам, насколько я ценю обсуждение этого вопроса. Теперь, когда я узнал, что я пропустил это, я добавил upvote :)
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()так, чтобы это не повлияло на другие запросы.

mor7ifer
источник
1
Отредактировал свой А и добавил prepare(). Надеюсь, вы не против редактирования. И да: Производительность имеет быть измерено ОП. Во всяком случае: я все еще думаю, что это должно быть просто usermeta и ничего больше.
Кайзер
@ m0r7if3r Спасибо за попытку решения. Я только что оставил комментарий в ответ на ответ kaiser, с опасениями по поводу возможных проблем масштабируемости. Пожалуйста, примите это во внимание.
Джон
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
}

Примечание Мы don't есть TestData и т.д. , так что выше немного угадайку. Убедитесь, что вы отредактировали этот ответ в соответствии с тем, что сработало для вас, чтобы у нас был хороший результат для последующих читателей. Я одобрю редактирование, если у вас слишком низкое число повторений. Затем вы также можете удалить эту заметку. Спасибо.

кайзер
источник
2
JOINэто гораздо дороже. Плюс: как я уже говорил, у нас нет данных испытаний, поэтому, пожалуйста, протестируйте оба ответа и ознакомьте нас с вашими результатами.
Кайзер
1
Сам WP_Query работает при соединении между таблицей постов и постметой при запросах. Я видел увеличение использования памяти PHP до 70 - 200 МБ на загрузку страницы. Запуск чего-то подобного со многими одновременными пользователями потребовал бы экстремальной инфраструктуры. Я предполагаю, что поскольку WordPress уже реализует подобную технику, JOIN-файлы должны быть менее обременительными по сравнению с работой с массивом идентификаторов.
Джон
1
@Джон, приятно слышать. очень хочу узнать исходящие.
Кайзер
4
Хорошо, вот результаты теста. Для этого я добавил около 47K пользователей из CSV-файла. Позже запустили цикл for, чтобы первые 45 пользователей следовали за каждым другим пользователем. В результате в моей пользовательской таблице было сохранено 3 704 951 запись. Первоначально решение @ m0r7if3r дало мне время запроса 95 секунд, которое уменьшилось до 0,020 мс после включения индексации для столбца leader_id. Общий объем используемой PHP-памяти составил около 20 МБ. С другой стороны, ваше решение заняло от 2 до 5 секунд для запроса с включенной индексацией. Общий объем используемой PHP-памяти составил около 117 МБ.
Джон
1
Я добавляю еще один ответ (мы можем обработать и изменить / отредактировать его), поскольку форматирование кода в комментариях просто отстой: P
kaiser
3

Примечание: этот ответ здесь, чтобы избежать расширенного обсуждения в комментариях

  1. Вот код OPs из комментариев, чтобы добавить первый набор тестовых пользователей. Я должен быть изменен для примера реального мира.

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

    ОП О тесте Для этого я добавил около 47K пользователей из CSV-файла. Позже запустили цикл for, чтобы первые 45 пользователей следовали за каждым другим пользователем.

    • В результате в моей пользовательской таблице было сохранено 3 704 951 запись.
    • Первоначально решение @ m0r7if3r дало мне время запроса 95 секунд, которое уменьшилось до 0,020 мс после включения индексации для столбца leader_id. Общий объем используемой PHP-памяти составил около 20 МБ.
    • С другой стороны, ваше решение заняло от 2 до 5 секунд для запроса с включенной индексацией. Общий объем используемой PHP-памяти составил около 117 МБ.
  2. Мой ответ на этот тест::

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

    Я также должен заявить, что вышеприведенное отслеживание времени не может быть реально измерено, так как это также потребовало бы времени для совместного расчета цикла. Лучше было бы перебрать полученный набор идентификаторов во втором цикле.

дальнейший процесс здесь

кайзер
источник
2
Примечание для тех, кто следовал этому вопросу: я занимаюсь измерением производительности в различных условиях и опубликую результаты в течение дня или 3. Это было чрезвычайно трудоемкой задачей из-за масштаба тестовых данных, которые необходимо генерироваться.
Джон