クエリにカスタムテーブルを含めるためにWP_Queryを拡張する方法は?


31

私は今、この問題について何日も過ごしています。最初は、ユーザーのフォロワーデータをデータベースに保存する方法でしたが、WordPress Answersでいくつかの素晴らしい推奨事項を取得しました。その後、推奨事項に従って、次のような新しいテーブルを追加しました。

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

上記の表では、最初の行にはIDが2のユーザーがおり、その後にIDが4のユーザーが続きます。2番目の行には、IDが3のユーザーにIDが付いたユーザーが続きます同じロジックが3行目に適用されます。

WP_Queryを拡張して、ユーザーのリーダーのみがフェッチする投稿を制限できるようにします。したがって、上記の表を考慮して、ユーザーID 10をWP_Queryに渡す場合、結果にはユーザーID 2とユーザーID 3による投稿のみが含まれます。

私は答えを見つけようとして多くのことを検索しました。また、WP_Queryクラスを拡張する方法を理解するのに役立つチュートリアルを見たこともありません。同様の質問に対するマイクシンケルの回答(WP_Queryの拡張)を見てきましたが、それを自分のニーズに適用する方法を本当に理解していません。誰かがこれで私を助けることができたら素晴らしいでしょう。

マイクの回答へのリンクはリクエストどおり: リンク1リンク2


マイクの回答へのリンクを追加してください。
カイザー

1
クエリ対象の例を挙げていただけますか?WP_Query投稿を取得するためのものであり、これが投稿とどのように結びついているのか理解していない。
mor7ifer

@kaiserマイクの回答へのリンクで質問を更新しました。
ジョン

@ m0r7if3r»WP_Queryを拡張して、ユーザーのリーダーのみが取得した投稿に制限できるようにしたい«
カイザー

2
@ m0r7if3r投稿はまさに私が照会する必要があるものです。ただし、取得する投稿は、カスタムテーブルで特定のユーザーのリーダーとしてリストされているユーザーが行う必要があります。つまり、WP_Queryに伝えたいのは、カスタムテーブルにID「10」を持つユーザーのリーダーとしてリストされているすべてのユーザーによるすべての投稿を取得することです。
ジョン

回答:


13

重要な免責事項:これを行う適切な方法は、テーブル構造を変更するのではなく、wp_usermetaを使用することです。その後、投稿を照会するためのカスタムSQLを作成する必要はありません(ただし、特定のスーパーバイザーに報告する全員のリストを取得するには、たとえばAdminセクションでカスタムSQLが必要です)。ただし、OPがカスタムSQLの作成について質問したため、既存のWordPressクエリにカスタムSQLを挿入するための現在のベストプラクティスを以下に示します。

複雑な結合を行っている場合は、posts_whereフィルターを使用することはできません。クエリの結合、選択、および場合によってはgroup byまたはorder byセクションも変更する必要があるためです。

最善の方法は、「posts_clauses」フィルターを使用することです。これは非常に有用なフィルターであり(悪用されるべきではありません!)、WordPressコア内の多くのコード行によって自動的に生成されるSQLのさまざまな部分を追加/変更できます。フィルターコールバックのシグネチャは次のとおり function posts_clauses_filter_cb( $clauses, $query_object ){ }です$clauses

条項

$clauses次のキーを含む配列です。各キーは、データベースに送信される最終的なSQLステートメントで直接使用されるSQL文字列です。

  • どこで
  • groupby
  • 参加する
  • 注文する
  • 明確な
  • 限界

データベースにテーブルを追加する場合(post_meta、user_meta、またはtaxonomiesを絶対に活用できない場合にのみこれを実行してください)、たとえば、fields( "SELECT" SQLステートメントの一部)、join(「FROM」句のテーブル以外のすべてのテーブル)、およびorderby

句の変更

これを行う最良の方法は$clauses、フィルターから取得した配列から関連するキーをサブリファレンスすることです。

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

これで、を変更すると$join、実際に直接変更される$clauses['join']ため、変更$clausesを返すときに変更が反映されます。

元の条項の保持

WordPressが生成した既存のSQLを保持する可能性があります(いや、真剣に聞いてください)。そうでない場合は、posts_request代わりにフィルターを確認する必要があります。これは、データベースに送信される直前の完全なmySQLクエリであるため、独自のフィルターで完全に上書きできます。なぜあなたはこれをしたいのですか?おそらくないでしょう。

したがって、句の既存のSQLを保持するために、それらに割り当てるのではなく、句に追加することを忘れないでください(つまり:$join .= ' {NEW SQL STUFF}';notを使用します$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」、およびそれは交差テーブルです。(ところで、このテーブル構造を一般化し、次のフィールドを持つ適切な交差テーブルに変換することをお勧めします: 'relationship_id'、 'user_id'、 'related_user_id'、 'relationship_type'。これははるかに柔軟で強力です。 ..しかし、私は脱線します)。

あなたが何をしたいのか理解できたら、リーダーのIDを渡し、そのリーダーのフォロワーによる投稿のみを表示します。私はそれが正しいことを願っています。それが正しくない場合、あなたは私が言うことを受け入れ、あなたのニーズに合わせなければなりません。私はあなたのテーブル構造に固執します:a leader_idとaがありfollower_idます。したがって、JOINは{$wpdb->posts}.post_author、「user_relationship」テーブルの「follower_id」への外部キーとしてオンになります。

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

私はこの質問に非常に遅く答えており、同じことをおologiesびします。私はこれに注意を払うのに忙しすぎました。

@ m0r7if3rと@kaiserに、アプリケーションに拡張して実装できる基本ソリューションを提供していただき、ありがとうございます。この回答は、@ m0r7if3rと@kaiserが提供するソリューションの私の適応に関する詳細を提供します。

まず、この質問が最初に尋ねられた理由を説明させてください。質問とそのコメントから、特定のユーザー(フォロワー)がフォローするすべてのユーザー(リーダー)による投稿を取得するためにWP_Queryを取得しようとしていることがわかります。フォロワーとリーダーの関係は、カスタムテーブルに保存されますfollow。この問題の最も一般的な解決策は、フォロワーのすべてのリーダーのユーザーIDをフォローテーブルから取得し、配列に配置することです。下記参照:

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

上記のソリューションは、私の希望する結果を達成する最も簡単な方法です。ただし、スケーラブルではありません。数万人のリーダーをフォローしているフォロワーがいる瞬間、リーダーIDの配列は非常に大きくなり、WordPressサイトが各ページの読み込みで100MBから250MBのメモリを使用し、最終的にサイトをクラッシュさせます。この問題の解決策は、データベースでSQLクエリを直接実行し、関連する投稿を取得することです。そのとき、@ m0r7if3rのソリューションが助けになりました。@kaiserの推奨に従って、両方の実装をテストすることにしました。WordPressの新規テストインストールで登録するために、約47KのユーザーをCSVファイルからインポートしました。インストールでは、Twenty Elevenテーマが実行されていました。これに続いて、forループを実行して、約50人のユーザーが他のすべてのユーザーをフォローするようにしました。@kaiserと@ m0r7if3rの両方のソリューションのクエリ時間の差は驚異的でした。@kaiserのソリューションは、通常、クエリごとに約2〜5秒かかりました。私が推測するバリエーションは、WordPressがクエリをキャッシュして後で使用できるようにするためです。一方、@ m0r7if3rのソリューションは、平均で0.02ミリ秒のクエリ時間を示しました。両方のソリューションをテストするために、leader_id列のインデックスをオンにしました。インデックスを作成しないと、クエリ時間が劇的に増加しました。

アレイベースのソリューションを使用する場合のメモリ使用量は約100〜150 MBでしたが、ダイレクトSQLを実行すると20 MBに低下しました。

フォロワーIDをposts_whereフィルター関数に渡す必要があるときに、@ m0r7if3rのソリューションでバンプを見つけました。少なくとも私の知る限り、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 );

注:最終的に、次の表に120万のエントリがある上記のソリューションを試しました。平均クエリ時間は約0.060ミリ秒でした。


3
この質問についての議論にどれほど感謝したかは一度も話しませんでした。私はそれを逃したことがわかったので、私は賛成票を追加しました:)
kaiser 14

8

これは、posts_whereフィルターを使用した完全なSQLソリューションで実行できます。以下に例を示します。

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が提案したように、リーダーを取得することとクエリを実行することの2つの部分に分割できます。私はこれは効率が悪いかもしれないと感じていますが、それは確かにより理解しやすい方法です。ネストされたSQLクエリは非常に遅くなる可能性があるため、どちらの方法が優れているかを判断するには、効率を自分でテストする必要があります。

コメントから:

関数をに配置し、のメソッドが呼び出される直前にfunctions.php実行する必要があります。その直後に、他のクエリに影響を与えないようにする必要があります。add_filter()query()WP_Queryremove_filter()


1
Aを編集して追加しましたprepare()。編集を気にしないでください。はい、パフォーマンス OPで測定する必要があります。とにかく、私はまだこれが単にユーザーメタであるべきだと思います。
カイザー

@ m0r7if3rソリューションを試すためのThx。私は、カイザーの答えに対する返信として、可能性のあるスケーラビリティの問題についてのコメントを投稿しました。考慮に入れてください。
ジョン

1
@kaiser少なくとも気にしないでください、実際、私はむしろそれを感謝します:)
mor7ifer

@ m0r7if3rありがとう。コミュニティロックにあなたのような人がいる:)
kaiser

1
関数をに配置し、のメソッドが呼び出される直前にfunctions.php実行する必要があります。その直後に、他のクエリに影響を与えないようにする必要があります。URL書き換えの問題が何であるかはadd_filter()query()WP_Queryremove_filter()posts_where
わかり

6

テンプレートタグ

functions.phpファイルに両方の関数を配置するだけです。次に、最初の関数を調整し、カスタムテーブル名を追加します。次に、結果の配列内の現在のユーザーIDを取り除くために、いくつかのtry / errorが必要です(コメントを参照)。

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

NOTE我々はドント上記推測ゲームの少しがあるのでなど、テストデータを持っています。この回答をあなたの役に立つもので編集してください。後の読者に満足のいく結果をもたらします。担当者が少なすぎる場合の編集を承認します。その後、このメモを削除することもできます。ありがとう。


2
JOINあるはるかに高価。加えて:私が述べたように、我々はテストデータを持っていないので、両方の答えをテストし、あなたの結果で私たちを啓発してください。
カイザー

1
WP_Query自体は、クエリの際に投稿テーブルとpostmetaの間のJOINで動作します。PHPのメモリ使用量が70MB-ページの読み込みごとに200MBに上昇するのを見てきました。多くの同時ユーザーでそのようなものを実行するには、極端なインフラストラクチャが必要になります。私の推測では、WordPressは既に同様の手法を実装しているため、JOINはIDの配列を操作する場合に比べて負担が少ないはずです。
ジョン

1
@John聞いてよかった。本当に今後を知りたいです。
カイザー

4
ここにテスト結果があります。このために、csvファイルから約47Kユーザーを追加しました。その後、forループを実行して、最初の45人のユーザーが他のすべてのユーザーをフォローするようにしました。これにより、3,704,951レコードがカスタムテーブルに保存されました。最初は、@ m0r7if3rのソリューションによりクエリ時間は95秒でしたが、leader_id列のインデックス作成をオンにすると0.020ミリ秒になりました。消費されるPHPメモリの合計は約20MBでした。一方、ソリューションは、インデックスがオンの場合、クエリに約2〜5秒かかりました。消費されるPHPメモリの合計は約117MBでした。
ジョン

1
コメント内のコードの書式設定は単に悪いので、別の答えを追加します(処理および変更/編集できます):P
kaiser

3

注:ここでの回答は、コメントでの議論の拡大を避けるためです。

  1. テストユーザーの最初のセットを追加するためのコメントからのOPコードです。実世界の例に合わせて修正する必要があります。

    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テストについてこれのために、csvファイルから約47Kユーザーを追加しました。その後、forループを実行して、最初の45人のユーザーが他のすべてのユーザーをフォローするようにしました。

    • これにより、3,704,951レコードがカスタムテーブルに保存されました。
    • 最初は、@ m0r7if3rのソリューションによりクエリ時間は95秒でしたが、leader_id列のインデックスをオンにすると0.020ミリ秒になりました。消費されるPHPメモリの合計は約20MBでした。
    • 一方、インデックスをオンにしたクエリでは、ソリューションに約2〜5秒かかりました。消費されるPHPメモリの合計は約117MBでした。
  2. この↑テストに対する私の答え:

    より「実際の」テスト:すべてのユーザーがa $leader_amount = rand( 0, 5 );をフォローし、$leader_amount x $random_ids = rand( 0, 47000 );各ユーザーにの数を追加します。これまでのところ私たちが知っていることは、ユーザーがお互いのユーザーをフォローしている場合、私のソリューションは非常に悪いことです。さらに:テストの実行方法と、タイマーを正確に追加した場所を示します。

    また、ループを一緒に計算するのに時間がかかるため、時間追跡の上の↑を実際に測定することはできません。2番目のループでIDの結果セットをループする方が良いでしょう。

ここでのさらなるプロセス


2
このQをフォローしている人への注意:私はさまざまな条件下でパフォーマンスを測定していますが、1日または3日に結果を投稿します。これは、必要なテストデータ生成されました。
ジョン
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.