Como estender o WP_Query para incluir tabela personalizada na consulta?

31

Já passei dias com esse problema agora. Inicialmente, era como armazenar os dados de seguidores de um usuário no banco de dados, para os quais recebi algumas boas recomendações aqui no WordPress Answers. Depois, seguindo as recomendações, adicionei uma nova tabela como esta:

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

Na tabela acima, a primeira linha possui um usuário com um ID 2 que está sendo seguido por um usuário com um ID 4. Na segunda linha, um usuário com um ID 3 é seguido por um usuário com um ID de 10. A mesma lógica se aplica à terceira linha.

Agora, basicamente quero estender o WP_Query para limitar as postagens buscadas, apenas pelo (s) líder (es) de um usuário. Portanto, levando em consideração a tabela acima, se eu passar o ID do usuário 10 para WP_Query, os resultados deverão conter apenas postagens pelo ID do usuário 2 e pelo ID do usuário 3.

Eu pesquisei muito tentando encontrar uma resposta. Também não vi nenhum tutorial para me ajudar a entender como estender a classe WP_Query. Vi as respostas de Mike Schinkel (estendendo o WP_Query) para perguntas semelhantes, mas realmente não entendi como aplicá-las às minhas necessidades. Seria ótimo se alguém pudesse me ajudar com isso.

Links para respostas de Mike, conforme solicitado: Link 1 , Link 2

John
fonte
Adicione um link para as respostas do Mikes, por favor.
Kaiser
11
você pode dar um exemplo do que você estaria consultando? WP_Queryé para obter postagens, e não estou conseguindo entender como isso está relacionado às postagens.
mor7ifer
@kaiser Atualizei a pergunta com links para as respostas de Mike.
John
@ m0r7if3r »Quero estender o WP_Query para limitar as postagens buscadas, apenas pelo (s) líder (es) de um usuário«, tão semelhante a "obter postagens do autor".
kaiser
2
@ m0r7if3r Posts é exatamente o que eu preciso consultar. Mas as postagens a serem buscadas devem ser de usuários listados como líderes de um determinado usuário na tabela personalizada. Portanto, em outras palavras, quero dizer ao WP_Query, vá buscar todas as postagens de todos os usuários listados como líderes de um usuário com um ID '10' na tabela personalizada.
John

Respostas:

13

Isenção de responsabilidade importante: a maneira correta de fazer isso NÃO é modificar a estrutura da tabela, mas usar wp_usermeta. Então você não precisará criar nenhum SQL personalizado para consultar suas postagens (embora ainda precise de um SQL personalizado para obter uma lista de todos os que se reportam a um supervisor específico - na seção Admin, por exemplo). No entanto, como o OP perguntou sobre a criação de SQL personalizado, eis a melhor prática atual para injetar SQL personalizado em uma Consulta WordPress existente.

Se você estiver fazendo junções complexas, não poderá usar apenas o filtro posts_where, porque precisará modificar a junção, a seleção e, possivelmente, o grupo ou as seções da consulta.

Sua melhor aposta é usar o filtro 'posts_clauses'. Este é um filtro muito útil (que não deve ser abusado!) Que permite acrescentar / modificar as várias partes do SQL que são geradas automaticamente pelas muitas linhas de código no núcleo do WordPress. A assinatura de retorno de chamada do filtro é: function posts_clauses_filter_cb( $clauses, $query_object ){ }e espera que você retorne $clauses.

As Cláusulas

$clausesé uma matriz que contém as seguintes chaves; cada chave é uma string SQL que será usada diretamente na instrução SQL final enviada ao banco de dados:

  • Onde
  • groupby
  • Junte-se
  • ordenar por
  • distinto
  • Campos
  • limites

Se você estiver adicionando uma tabela ao banco de dados (faça isso apenas se absolutamente não puder aproveitar post_meta, user_meta ou taxonomias), provavelmente precisará tocar em mais de uma dessas cláusulas, por exemplo, o fields(o "SELECT" parte da instrução SQL), a join(todas as suas tabelas, exceto a da cláusula "FROM") e talvez a orderby.

Modificando as cláusulas

A melhor maneira de fazer isso é sub-referenciar a chave relevante da $clausesmatriz que você obteve do filtro:

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

Agora, se você modificar $join, na verdade você estará modificando diretamente, $clauses['join']para que as alterações entrem $clausesquando você a devolver.

Preservando as cláusulas originais

As chances são (não, sério, ouça) que você deseje preservar o SQL existente que o WordPress gerou para você. Caso contrário, você provavelmente deve olhar para o posts_requestfiltro - essa é a consulta mySQL completa antes de ser enviada ao banco de dados, para que você possa utilizá-lo totalmente. Por que você quer fazer isso? Você provavelmente não.

Portanto, para preservar o SQL existente nas cláusulas, lembre-se de anexar às cláusulas, não atribuí-las (por exemplo: use $join .= ' {NEW SQL STUFF}';not $join = '{CLOBBER SQL STUFF}';. Note que, como cada elemento da $clausesmatriz é uma string, se você desejar anexá-la, você provavelmente desejará inserir um espaço antes de quaisquer outros tokens de caracteres, caso contrário, provavelmente criará algum erro de sintaxe SQL.

Você pode simplesmente assumir que sempre haverá algo em cada uma das cláusulas e, portanto, lembre-se de iniciar cada nova sequência com um espaço, como em:, $join .= ' my_tableou, você sempre pode adicionar uma pequena linha que só adiciona um espaço se você precisar:

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


return $clauses;

Isso é uma coisa estilística mais do que qualquer outra coisa. O ponto importante a ser lembrado é: sempre deixe um espaço ANTES da sua string se você estiver anexando a uma cláusula que já tenha algum SQL nela!

Juntar as peças

A primeira regra do desenvolvimento do WordPress é tentar usar o máximo de funcionalidade possível. Esta é a melhor maneira de provar seu trabalho no futuro. Suponha que a equipe principal decida que o WordPress agora estará usando SQLite ou Oracle ou alguma outra linguagem de banco de dados. Qualquer mySQL escrito à mão pode se tornar inválido e quebrar seu plugin ou tema! É melhor deixar o WP gerar o máximo de SQL possível por conta própria e apenas adicionar os bits necessários.

Portanto, a primeira ordem de negócios está se aproveitando WP_Querypara gerar o máximo possível de sua consulta básica. O método exato que usamos para fazer isso depende em grande parte de onde essa lista de postagens deve aparecer. Se for uma subseção da página (não sua consulta principal), você usaria get_posts(); se for a consulta principal, suponho que você possa usar query_posts()e terminar com ela, mas a maneira correta de fazer isso é interceptar a consulta principal antes que ela atinja o banco de dados (e consuma ciclos do servidor), portanto, use o requestfiltro.

Ok, você gerou sua consulta e o SQL está prestes a ser criado. Bem, de fato, ele foi criado, apenas não enviado para o banco de dados. Usando o posts_clausesfiltro, você adicionará sua tabela de relacionamentos com funcionários à mistura. Vamos chamar esta tabela {$ wpdb-> prefix}. 'user_relationship' e é uma tabela de interseção. (A propósito, eu recomendo que você genere essa estrutura da tabela e a transforme em uma tabela de interseção adequada com os seguintes campos: 'ID_de_relacionamento', 'ID_ do usuário', 'ID_usuário_id' ',' tipo_de_relação ',' tipo_de_relação '; isso é muito mais flexível e poderoso. .. mas eu discordo).

Se eu entendo o que você quer fazer, você passa a ID do Líder e vê apenas as postagens dos Seguidores desse Líder. Espero ter acertado. Se não estiver certo, você terá que pegar o que eu digo e adaptá-lo às suas necessidades. Vou ficar com a estrutura da sua mesa: temos a leader_ide a follower_id. Portanto, o JOIN será {$wpdb->posts}.post_authorativado como uma chave estrangeira para o 'follower_id' na sua tabela '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
fonte
13

Estou respondendo a esta pergunta extremamente tarde e peço desculpas pelo mesmo. Eu estava muito ocupado com prazos para atender a isso.

Um grande obrigado a @ m0r7if3r e @kaiser por fornecer as soluções básicas que eu poderia estender e implementar em meu aplicativo. Esta resposta fornece detalhes sobre minha adaptação das soluções oferecidas por @ m0r7if3r e @kaiser.

Primeiro, deixe-me explicar por que essa pergunta foi feita em primeiro lugar. A partir da pergunta e dos comentários dela, pode-se concluir que estou tentando fazer com que o WP_Query puxe postagens de todos os usuários (líderes) que um determinado usuário (seguidor) segue. O relacionamento entre o seguidor e o líder é armazenado em uma tabela personalizada follow. A solução mais comum para esse problema é extrair da tabela a seguir os IDs de usuário de todos os líderes de um seguidor e colocá-lo em uma matriz. Ver abaixo:

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;

Depois de ter a matriz de líderes, você pode transmiti-la como argumento para o WP_Query. Ver abaixo:

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

A solução acima é a maneira mais simples de alcançar os resultados desejados. No entanto, não é escalável. No momento em que você tem um seguidor seguindo dezenas e milhares de líderes, a matriz resultante de IDs de líderes ficaria extremamente grande e forçaria seu site WordPress a usar 100 MB - 250 MB de memória em cada carregamento de página e, eventualmente, travar o site. A solução para o problema é executar a consulta SQL diretamente no banco de dados e buscar postagens relevantes. Foi quando a solução do @ m0r7if3r veio resgatar. Seguindo a recomendação do @ kaiser, decidi testar as duas implementações. Eu importei cerca de 47 mil usuários de um arquivo CSV para registrá-los em uma nova instalação de teste do WordPress. A instalação estava executando o tema Twenty Eleven. Depois disso, executei um loop for para fazer com que cerca de 50 usuários sigam todos os outros usuários. A diferença no tempo de consulta para a solução @kaiser e @ m0r7if3r foi impressionante. A solução do @ kaiser normalmente levava de 2 a 5 segundos para cada consulta. A variação que presumo acontece quando o WordPress armazena em cache as consultas para uso posterior. Por outro lado, a solução da @ m0r7if3r demonstrou um tempo de consulta de 0,02 ms em média. Para testar as duas soluções, eu tive a indexação ON na coluna leader_id. Sem indexação, houve um aumento dramático no tempo de consulta.

O uso de memória ao usar a solução baseada em matriz ficou em torno de 100-150 MB e caiu para 20 MB ao executar um SQL direto.

Eu bati com a solução do @ m0r7if3r quando precisei passar o ID do seguidor para a função de filtro posts_where. Pelo menos, de acordo com meu conhecimento, o WordPress não permite passar uma variável às funções do arquivador. Você pode usar variáveis ​​globais, mas eu queria evitar globais. Acabei estendendo o WP_Query para finalmente resolver o problema. Então, aqui está a solução final que eu implementei (com base na solução do @ 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: Eu finalmente tentei a solução acima com 1,2 milhão de entradas na tabela a seguir. O tempo médio de consulta ficou em torno de 0,060 ms.

John
fonte
3
Eu nunca te disse o quanto apreciei a discussão sobre essa questão. Agora que eu descobri que eu perdi, eu tenho adicionar um upvote :)
kaiser
8

Você pode fazer isso com uma solução totalmente SQL usando o posts_wherefiltro. Aqui está um exemplo disso:

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

Eu acho que também pode haver uma maneira de fazer isso JOIN, mas não consigo. Continuarei brincando com ele e atualizarei a resposta, se o conseguir.

Como alternativa, como o @kaiser sugeriu, você pode dividi-lo em duas partes: obter os líderes e fazer a consulta. Sinto que isso pode ser menos eficiente, mas certamente é o caminho mais compreensível a seguir. Você precisaria testar a eficiência para determinar qual método é melhor, pois as consultas SQL aninhadas podem ficar muito lentas.

DOS COMENTÁRIOS:

Você deve colocar a função no seu functions.phpe fazer o add_filter()direito antes que o query()método de WP_Queryseja chamado. Imediatamente após isso, você deve fazer remove_filter()isso para que não afete as outras consultas.

mor7ifer
fonte
11
Editou seu A e adicionou prepare(). Espero que você não se importe com a edição. E sim: o desempenho deve ser medido pelo OP. Enfim: eu ainda acho que isso deve ser simplesmente usermeta e nada mais.
Kaiser #
@ m0r7if3r Thx por tentar uma solução. Acabei de publicar um comentário em resposta à resposta do kaiser, com preocupações sobre possíveis problemas de escalabilidade. Por favor, leve isso em consideração.
John John
11
@kaiser não se importa, no mínimo, na verdade eu sim apreciá-lo :)
mor7ifer
@ m0r7if3r Obrigado. Tendo caras como você em rochas da comunidade :)
kaiser
11
Você deve colocar a função no seu functions.phpe fazer o add_filter()direito antes que o query()método de WP_Queryseja chamado. Imediatamente após isso, você deve fazer remove_filter()isso para que não afete as outras consultas. Eu não tenho certeza qual é o problema com a reescrita de URL seria, eu usei posts_whereem muitas ocasiões e nunca vi isso ...
mor7ifer
6

Tag de modelo

Basta colocar as duas funções no seu functions.phparquivo. Em seguida, ajuste a 1ª função e adicione seu nome de tabela personalizado. Então você precisa de alguma tentativa / erro para se livrar do ID do usuário atual dentro da matriz resultante (consulte o comentário).

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

Dentro do modelo

Aqui você pode fazer o que quiser com seus resultados.

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

NOTA Nós NÃO FAZEM tem testdata, etc. assim que o acima é um pouco de um jogo de adivinhação. Certifique-se que você editar esta resposta com o que funcionou para você, por isso temos um resultado satisfatório para os leitores mais tarde. Eu aprovarei a edição, caso você tenha um representante muito baixo. Você também pode excluir esta nota. Obrigado.

kaiser
fonte
2
JOINé muito mais caro. Além disso: como eu mencionei, não temos dados de teste; portanto, teste as duas respostas e nos esclareça com seus resultados.
kaiser
11
O próprio WP_Query funciona com JOINs entre a tabela de postagens e o postmeta ao consultar. Vi o uso de memória PHP aumentar para 70 MB - 200 MB por carregamento de página. A execução de algo assim com muitos usuários simultâneos exigiria uma infraestrutura extrema. Meu palpite seria que, como o WordPress já implementa uma técnica semelhante, os JOINs devem ser menos exigentes em comparação com o trabalho com uma variedade de IDs.
John
11
@ John bom ouvir isso. realmente quer saber o resultado.
Kaiser
4
Ok, aqui estão os resultados do teste. Para isso, adicionei cerca de 47 mil usuários de um arquivo csv. Mais tarde, executou um loop for para fazer com que os 45 primeiros usuários sigam todos os outros usuários. Isso resultou em 3.704.951 registros salvos na minha tabela personalizada. Inicialmente, a solução do @ m0r7if3r me deu um tempo de consulta de 95 segundos, que caiu para 0,020 ms após ativar a indexação na coluna leader_id. A memória PHP total consumida foi de cerca de 20 MB. Por outro lado, sua solução levou cerca de 2 a 5 segundos para consulta com a indexação ON. A memória total PHP consumida foi de aproximadamente 117 MB.
John
11
Eu adicionar outra resposta (que pode processar e modificar / editar em que) como formatação de código nos comentários simplesmente suga: P
kaiser
3

Nota: Esta resposta aqui é para evitar discussões prolongadas nos comentários

  1. Aqui está o código de OP dos comentários, para adicionar o primeiro conjunto de usuários de teste. Eu tenho que ser modificado para um exemplo do mundo real.

    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 Sobre o teste Para isso, adicionei cerca de 47 mil usuários de um arquivo csv. Mais tarde, executou um loop for para fazer com que os 45 primeiros usuários sigam todos os outros usuários.

    • Isso resultou em 3.704.951 registros salvos na minha tabela personalizada.
    • Inicialmente, a solução do @ m0r7if3r me deu um tempo de consulta de 95 segundos, que caiu para 0,020 ms após ativar a indexação na coluna leader_id. A memória PHP total consumida foi de cerca de 20 MB.
    • Por outro lado, sua solução levou cerca de 2 a 5 segundos para consulta com a indexação ON. A memória total PHP consumida foi de aproximadamente 117 MB.
  2. Minha resposta a este teste ↑:

    um teste mais "da vida real": permita que cada usuário siga um $leader_amount = rand( 0, 5 );e adicione o número de $leader_amount x $random_ids = rand( 0, 47000 );a cada usuário. Até agora, o que sabemos é: Minha solução seria extremamente ruim se um usuário estivesse seguindo um ao outro. Além disso: você mostrará como fez o teste e onde exatamente adicionou os cronômetros.

    Eu também tenho que afirmar que o ↑ tempo acima do rastreamento não pode ser realmente medido, pois também levaria tempo para calcular o loop juntos. Melhor seria fazer um loop pelo conjunto resultante de IDs em um segundo loop.

processo adicional aqui

kaiser
fonte
2
Nota para aqueles que seguem este P: Estou em processo de medir o desempenho sob várias condições e publicarei o resultado em um dia ou 3. Essa tarefa foi extremamente demorada devido à escala de dados de teste que precisam ser gerado.
John