如何扩展WP_Query以在查询中包含自定义表?


31

我已经在这个问题上待了几天。最初是如何在数据库中存储用户的关注者数据,在WordPress Answers上我得到了一些不错的建议。之后,按照建议,我添加了一个新表,如下所示:

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

在上表中,第一行的ID为2的用户,其后为ID为4的用户。第二行的ID为3的用户,其后为ID为3的用户。 10。相同的逻辑适用于第三行。

现在,从本质上讲,我想扩展WP_Query,以便可以限制仅由用户的领导者访存的帖子。因此,考虑到上表,如果我要将用户ID 10传递给WP_Query,则结果应仅包含用户ID 2和用户ID 3的帖子。

我已经进行了很多搜索,试图找到答案。同样,我也没有看过任何教程可以帮助我了解如何扩展WP_Query类。我已经看到Mike Schinkel的答案(将WP_Query扩展为类似的问题),但是我真的不明白如何将其应用于我的需求。如果有人可以帮我解决这个问题,那就太好了。

根据要求链接到Mike的答案: 链接1链接2


请添加指向Mikes答案的链接。
kaiser 2012年

1
您可以举一个您要查询的内容的例子吗?WP_Query是为了获取帖子,而我却无法理解这与帖子之间的联系。
mor7ifer 2012年

@kaiser我已使用指向Mike答案的链接更新了问题。
约翰

@ m0r7if3r»我想扩展WP_Query,以便可以限制仅由用户的领导者«提取的帖子,类似于“按作者获取帖子”。
kaiser 2012年

2
@ m0r7if3r帖子正是我需要查询的。但是要获取的帖子应该由在定制表中被列为某个用户的领导者的用户完成。因此,换句话说,我想告诉WP_Query,去获取所有在自定义表中列为ID为“ 10”的用户的领导者的所有用户的所有帖子。
约翰

Answers:


13

重要的免责声明:执行此操作的正确方法不是修改表结构,而是使用wp_usermeta。然后,您将不需要创建任何自定义SQL来查询您的帖子(例如,您仍然需要一些自定义SQL来获取向特定主管报告的每个人的列表-例如在“管理”部分)。但是,由于OP询问编写自定义SQL的问题,因此这是将自定义SQL注入现有WordPress查询的当前最佳实践。

如果您要进行复杂的联接,则不能仅使用posts_where过滤器,因为您将需要修改联接,选择以及可能的查询部分的分组或排序。

最好的选择是使用“ posts_clauses”过滤器。这是一个非常有用的过滤器(请勿滥用!),它使您可以附加/修改由WordPress核心内的许多代码行自动生成的SQL的各个部分。过滤器回调签名为: function posts_clauses_filter_cb( $clauses, $query_object ){ }并且希望您返回$clauses

条款

$clauses是包含以下键的数组;每个键都是一个SQL字符串,将直接在发送到数据库的最终SQL语句中使用:

  • 哪里
  • 通过...分组
  • 加入
  • 订购
  • 不同
  • 领域
  • 极限

如果您要向数据库中添加表(仅在绝对不能利用post_meta,user_meta或分类法的情况下才这样做),您可能需要触摸多个这些子句,例如,fields(“ 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,然后仅查看该领导者关注者的帖子。我希望我没错。如果不正确,您将不得不接受我所说的话,并使其适应您的需求。我会坚持使用您的表结构:我们有一个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;
}

13

我很晚才回答这个问题,对此我深表歉意。我太忙于截止日期,无法参加这个会议。

非常感谢@ 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的建议,我着手测试这两种实现。我从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解决方案的问题。据我所知,Atleast 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

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的建议,您可以将其分为两部分:获取领导者和进行查询。我觉得这可能不太有效,但是肯定是更容易理解的方法。您必须自己测试效率,以确定哪种方法更好,因为嵌套的SQL查询会变得很慢。

从评论:

您应该将函数放入您的函数中functions.php,并在调用add_filter()query()方法之前执行正确的WP_Query操作。在此之后,您应该立即remove_filter()这样做,以免影响其他查询。


1
编辑了您的A并添加了prepare()。希望您不介意编辑。是的:性能必须由OP来衡量。无论如何:我仍然认为这应该仅仅是usermeta,而不是其他。
kaiser 2012年

@ m0r7if3r Thx,尝试解决方案。我刚刚发表了评论以回应kaiser的回答,并担心可能的可扩展性问题。请考虑一下。
约翰

1
@kaiser至少不要介意,事实上,我很感激:)
mor7ifer 2012年

@ m0r7if3r谢谢。在社区的岩石中有像您这样的人:)
kaiser 2012年

1
您应该将函数放入您的函数中functions.php,并在调用add_filter()query()方法之前执行正确的WP_Query操作。在此之后,您应该立即remove_filter()这样做,以免影响其他查询。我不确定URL重写的问题是什么,我已经使用posts_where过很多次了,但从未见过……
mor7ifer 2012年

6

模板标签

只需将两个函数都放在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
}

注意我们没有测试数据等,因此以上内容只是一个猜测游戏。确保使用对有用的内容来编辑此答案,以便为以后的读者提供满意的结果。如果您的代表人数过少,我会批准编辑。然后,您也可以删除此注释。谢谢。


2
JOIN更加昂贵。另外:正如我提到的,我们没有测试数据,因此请同时测试两个答案并用您的结果启发我们。
kaiser 2012年

1
WP_Query本身在查询时与posts表和postmeta之间的JOIN一起使用。我已经看到PHP内存使用量猛增到70MB-每页加载200MB。与许多同时用户一起运行类似的程序将需要一个极端的基础架构。我的猜测是,由于WordPress已经实现了类似的技术,因此与使用ID数组相比,JOIN的负担应该更少。
John

1
@John很高兴听到。真的很想知道结果。
kaiser 2012年

4
好的,这是测试结果。为此,我从一个csv文件中添加了约47K用户。之后,运行for循环,使前45个用户跟随其他每个用户。这导致3,704,951条记录保存到我的自定义表中。最初,@ m0r7if3r的解决方案为我提供了95秒的查询时间,在对leader_id列启用索引之后,查询时间缩短至0.020 ms。PHP消耗的总内存约为20MB。另一方面,在索引为ON的情况下,您的解决方案花费了大约2到5秒的时间进行查询。PHP消耗的总内存约为117MB。
约翰

1
我添加了另一个答案(我们可以对此进行处理和修改/编辑),因为注释中的代码格式简直太烂了:P
kaiser 2012年

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

    关于测试的操作为此,我从一个csv文件中添加了约47K用户。之后,运行for循环,使前45个用户跟随其他每个用户。

    • 这导致3,704,951条记录保存到我的自定义表中。
    • 最初,@ m0r7if3r的解决方案为我提供了95秒的查询时间,在对leader_id列启用索引之后,查询时间缩短至0.020 ms。PHP消耗的总内存约为20MB。
    • 另一方面,在索引为ON的情况下,您的解决方案花费了大约2到5秒的时间进行查询。PHP消耗的总内存约为117MB。
  2. 我对此↑测试的回答:

    更加“真实”的测试:让每个用户都遵循$leader_amount = rand( 0, 5 );,然后将的数量添加$leader_amount x $random_ids = rand( 0, 47000 );到每个用户。到目前为止,我们所知道的是:如果一个用户彼此关注,那么我的解决方案将非常糟糕。此外:您将展示如何进行测试以及在何处添加计时器。

    我还必须指出,上述时间跟踪的↑不能真正测量,因为还需要花费一些时间来共同计算循环。最好在第二个循环中循环遍历生成的ID集。

这里的进一步处理


2
对于那些一直关注以下问题的人请注意:我正在测量各种条件下的性能,并将结果在一天或3天内发布。由于需要大量的测试数据,因此这是非常耗时的任务产生。
约翰
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.