问题描述

我现在已经是这个问题了。最初是,如何将用户的跟随者数据存储在数据库中,我在 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 辅助翻译的英文资料结果。如果您对结果不满意,可以加入我们改善翻译效果:薇晓朵技术论坛。