問題描述

我現在已經是這個問題了。最初是,如何將使用者的跟隨者資料儲存在資料庫中,我在 WordPress 答案中有幾個很好的建議。之後,按照建議,我新增了一個這樣的新表:

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

在上表中,第一行的 ID 為 2 的使用者正在跟著 ID 為 4 的使用者。在第二行中,ID 為 3 的使用者正在跟隨有 ID 的使用者相同的邏輯適用於第三行。

現在,本質上我想擴充套件 WP_Query,以便我可以限制只能由一個使用者的領導者提取的帖子。所以,考慮到上述表格,如果我將使用者 ID 10 傳遞給 WP_Query,結果應該只包含使用者 ID 2 和使用者 ID 3 的帖子。

我搜尋了很多試圖找到答案。也沒有看到任何教程來幫助我瞭解如何擴充套件 WP_Query 類。我已經看到 Mike Schinkel 的答案 (擴充套件 WP_Query) 到類似的問題,但我真的不明白如何應用於我的需要。如果有人可以幫助我,這將是巨大的。

連結到 Mike’s answer’s 請求:Link 1Link 2

最佳解決方案

Important disclaimer: the proper way to do this is NOT to modify your table structure, but to use wp_usermeta. Then you will not need to create any custom SQL to query your posts (though you’ll still need some custom SQL to get a list of everyone that reports to a particular supervisor – in the Admin section, for instance). However, since the OP asked about writing custom SQL, here is the current best practice for injecting custom SQL into an existing WordPress Query.

如果您正在進行復雜的連線,則不能僅僅使用 posts_where 過濾器,因為您還需要修改連線,選擇,可能的組,或按照查詢的各個部分進行排序。

你最好的辦法是使用’posts_clauses’ 過濾器。這是一個非常有用的過濾器 (不應該被濫用!),允許您附加/修改由 WordPress 核心中許多行程式碼自動生成的 SQL 的各個部分。過濾器回撥簽名是:function posts_clauses_filter_cb( $clauses, $query_object ){ },它期望您返回 $clauses

條款

 $clauses 是一個包含以下鍵的陣列; 每個鍵是一個 SQL 字串,將直接用於傳送到資料庫的最終 SQL 語句:

  • where

  • group by

  • join

  • order by

  • distinct

  • fields

  • limits

如果您在資料庫中新增表 (只有在絕對不能使用 post_meta,user_meta 或分類法的情況下才可以),您可能需要觸控這些子句中的一個以上,例如 fields(“SELECT” 部分的 SQL 語句),join(所有的表,除了”FROM” 子句中的表),也可能是 orderby

修改條款

執行此操作的最佳方法是從過濾器獲取的 $clauses 陣列中引用相關的鍵:

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

現在,如果修改 $join,您將直接修改 $clauses['join'],以便在返回 $clauses 時進行更改。

保留原條

有可能 (不,認真,傾聽),您將要保留 WordPress 為您生成的現有 SQL 。如果沒有,你應該看看 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 或其他資料庫語言。任何 hand-written mySQL 可能會變得無效,破壞你的外掛或主題!更好地讓 WP 自己生成儘可能多的 SQL,只需新增所需的位。

所以第一個業務順序是利用 WP_Query 生成儘可能多的基本查詢。我們使用的確切方法在很大程度上取決於這個帖子列表應該顯示在哪裡。如果是頁面的 sub-section(不是主要查詢),您將使用 get_posts(); 如果是主要查詢,我想您可以使用 query_posts()並完成它,但正確的方法是在查詢資料庫之前攔截主查詢 (並消耗伺服器週期),以便使用 request 過濾器。

好的,所以你已經生成了你的查詢,而 SQL 即將被建立。那麼事實上,它已經被建立,只是沒有傳送到資料庫。透過使用 posts_clauses 過濾器,您將將員工關係表新增到組閤中。我們來呼叫這個表 {$ wpdb-> 字首} 。 ‘user_relationship’,它是一個交叉表。 (順便說一下,我建議您將此表格結構進行泛型,並將其轉換為具有以下欄位的適當交集表:’relationship_id’,’user_id’,’related_user_id’,’relationship_type’; 這樣更靈活和強大。 .. 但我離題) 。

如果我明白你想做什麼,你想傳遞一個領導者的 ID,然後只看到該領導者的追隨者的帖子。我希望我有這個權利。如果不正確,您將不得不採取我所說的,並適應您的需要。我會堅持你的表結構:我們有一個 leader_id 和一個 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;
}

次佳解決方案

您可以使用 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 所建議的那樣,您可以將其分為兩部分:領導和進行查詢。我有一種感覺,這可能效率不高,但這當然是更容易理解的方式。您必須測試自己的效率來確定哪種方法更好,因為巢狀 SQL 查詢可能會變得相當慢。

從評論:

你應該把函式放在你的 function.php 中,然後在呼叫 WP_Query 的 query() 方法之前先做一下 add_filter() 。緊接著,你應該是 remove_filter(),所以它不影響其他查詢。

第三種解決方案

我回答這個問題太遲了,我對此表示歉意。我一直很忙於期限,以滿足這一點。

非常感謝 @ 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

上述解決方案是實現我想要的結果的最簡單的方法。但是,它是 non-scalable 。當您擁有數以千計的領導者後,隨之而來的領導者 ID 將會變得非常大,並迫使您的 WordPress 網站在每個頁面載入時使用 100MB – 250MB 的記憶體,最終使該網站崩潰。問題的解決方法是直接在資料庫上執行 SQL 查詢並獲取相關的帖子。那就是 @ m0r7if3r 的解決方案來拯救。按照 @ kaiser 的建議,我開始測試這兩個實現。我從一個 CSV 檔案匯入了大約 47K 個使用者,以便在一個新的測試安裝的 WordPress 上註冊它們。安裝正在執行二十一一主題。接下來,我執行一個 for 迴圈,使大約 50 個使用者跟隨每個其他使用者。 @kaiser 和 @ m0r7if3r 的解決方案的查詢時間差異是驚人的。 @ kaiser 的解決方案通常每個查詢大約需要 2 到 5 秒。我推測的變化發生在 WordPress 快取查詢以供以後使用。另一方面,@ m0r7if3r 的解決方案平均顯示了 0.02 ms 的查詢時間。為了測試這兩個解決方案,我已經為 leader_id 列索引了 ON 。沒有索引,查詢時間顯著增加。

當使用基於陣列的解決方案時,記憶體使用量大約在 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 ms 。

第四種方案

模板標籤

只需將這兩個功能放在您的 functions.php 檔案中。然後調整第一個功能並新增您的自定義表名稱。那麼你需要一些嘗試/錯誤來擺脫結果陣列中的當前使用者 ID(見註釋) 。

/**
 * 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 We don´t have testdata, etc. so the above is a little bit of a guessing game. Make sure that you edit this answer with what worked for you, so we have a satisfying result for later readers. I´ll approve the edit in case you got too low rep. You then can also delete this note. Thanks.

第五種方案

Note: This answer here is to avoid extended discussion in the comments

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

       OP 關於測試為此,我從 csv 檔案中新增了大約 47K 個使用者。之後,執行 for 迴圈使前 45 個使用者跟隨每個其他使用者。這導致我的自定義表格中儲存了 3,704,951 條記錄。最初,@ m0r7if3r 的解決方案給了我 95 秒的查詢時間,在 leader_id 列上開啟索引後,下降到 0.020 ms 。消耗的 PHP 記憶體大約在 20MB 左右。另一方面,您的解決方案大約需要 2 到 5 秒的時間來查詢索引。所消耗的 PHP 記憶體總數約為 117MB 。

  2. 我對這個↑測試的答案:更多”real life” 測試:讓每個使用者遵循 $leader_amount = rand( 0, 5 );,然後將 $leader_amount x $random_ids = rand( 0, 47000 ); 的數量新增到每個使用者。到目前為止,我們知道的是:如果使用者正在跟蹤對方的使用者,我的解決方案將會非常糟糕。進一步:你會看到你做了什麼測試,你在哪裡新增了計時器。我也必須指出,↑以上的時間跟蹤不能真正測量,因為它也需要時間來一起計算迴圈。更好的是在第二個迴圈中迴圈遍歷結果集。

在這裡進一步處理

參考文獻

注:本文內容整合自 Google/Baidu/Bing 輔助翻譯的英文資料結果。如果您對結果不滿意,可以加入我們改善翻譯效果:薇曉朵技術論壇。