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

时间:2012-04-26 作者:John

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

id  leader_id   follower_id
1   2           4
2   3           10
3   2           10
在上表中,第一行有一个ID为2的用户,后面跟着一个ID为4的用户。在第二行中,ID为3的用户后面跟着ID为10的用户。同样的逻辑适用于第三行。

现在,本质上我想扩展WP\\u查询,这样我就可以将获取的帖子限制为只由用户的领导获取。因此,考虑到上表,如果我要将用户ID 10传递给WP\\U查询,结果应该只包含用户ID 2和用户ID 3的帖子。

为了找到答案,我找了很多。我也没有看过任何教程来帮助我理解如何扩展WP\\u查询类。我看过迈克·辛克尔(MikeSchinkel)对类似问题的回答(扩展WP\\U查询),但我真的不知道如何将其应用于我的需求。如果有人能帮我解决这个问题,那就太好了。

按要求链接到Mike的答案:Link 1, Link 2

5 个回复
最合适的回答,由SO网友:John 整理而成

我很晚才回答这个问题,对此我深表歉意。我一直忙于完成最后期限,没有时间处理这个问题。

非常感谢@m0r7if3r和@kaiser提供了我可以在应用程序中扩展和实现的基本解决方案。此答案详细介绍了我对@m0r7if3r和@kaiser提供的解决方案的改编。

首先,让我解释一下为什么首先要问这个问题。从这个问题及其评论中可以看出,我正试图让WP\\u 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\\u 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\\u id列建立了索引。如果没有索引,查询时间会急剧增加。

使用基于阵列的解决方案时,内存使用量约为100-150 MB,而在运行direct SQL时,内存使用量降至20 MB。

当我需要将follower ID传递给posts\\u where filter函数时,我用@m0r7if3r的解决方案遇到了一个问题。至少,据我所知,WordPress不允许将变量传递给文件管理器函数。虽然可以使用全局变量,但我想避免使用全局变量。我最终扩展了WP\\u查询以最终解决这个问题。这是我实现的最终解决方案(基于@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。

SO网友:Tom Auger

重要免责声明:正确的方法不是修改表结构,而是使用wp\\u usermeta。然后,您将不需要创建任何自定义SQL来查询您的帖子(尽管您仍然需要一些自定义SQL来获取向特定主管报告的所有人的列表,例如在管理部分)。然而,由于OP询问如何编写自定义SQL,下面是将自定义SQL注入现有WordPress查询的当前最佳实践。

如果要进行复杂的联接,不能只使用posts\\U where过滤器,因为还需要修改查询的联接、select以及group by或order by部分。

最好使用“posts\\u子句”过滤器。这是一个非常有用的过滤器(不应该滥用!)这允许您附加/修改由WordPress核心中的多行代码自动生成的SQL的各个部分。筛选器回调签名为:function posts_clauses_filter_cb( $clauses, $query_object ){ } 它希望你回来$clauses.

条款

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

如果要向数据库中添加表(只有在绝对不能利用post\\u meta、user\\u meta或分类法的情况下才这样做),则可能需要接触这些子句中的多个,例如fields (SQL语句的“SELECT”部分)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;
这是一种风格,胜过其他任何东西。要记住的重要一点是:always leave a space BEFORE your string if you\'re appending to a clause that already has some SQL in it!

WordPress开发的第一条规则是尽可能多地使用核心功能

因此,首要的业务是利用WP_Query 尽可能多地生成基本查询。我们使用的确切方法在很大程度上取决于这个帖子列表应该出现在哪里。如果它是页面的一个子部分(不是您的主查询),您将使用get_posts(); 如果这是主查询,我想您可以使用query_posts() 但正确的方法是在主查询访问数据库(并消耗服务器周期)之前拦截它,所以使用request 滤器

好的,您已经生成了查询,即将创建SQL。事实上,它已经创建,只是没有发送到数据库。通过使用posts_clauses 过滤器中,您要将员工关系表添加到组合中。我们把这个表叫做{$wpdb->前缀}。”user\\u relationship\',它是一个交集表。(顺便说一句,我建议您将此表结构泛化,并将其转换为具有以下字段的适当交集表:“relationship\\u id”、“user\\u id”、“related\\u user\\u id”、“relationship\\u type”;这更灵活、更强大……但我离题了)。

如果我知道你想做什么,你就要传递一个领导者的ID,然后只看到该领导者追随者的帖子。我希望我没弄错。如果不正确,你必须接受我说的话,并根据你的需要加以调整。我会坚持你的桌子结构:我们有leader_id 和afollower_id. 所以连接将打开{$wpdb->posts}.post_author 作为“user\\u relationship”表上“follower\\u 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;
}

SO网友:mor7ifer

您可以使用posts_where 滤器下面是一个例子:

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查询可能会变得非常慢。

FROM THE COMMENTS:

您应该在functions.php 然后做add_filter() 就在query() 方法WP_Query 被调用。紧接着,你应该remove_filter() 这样就不会影响其他查询。

SO网友:kaiser

模板标记只需在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 我们没有测试数据等,所以上面有点猜测。确保you 用对您有用的内容编辑此答案,以便我们为以后的读者提供满意的结果。如果你的代表太低,我会批准编辑。然后你也可以删除此注释。谢谢

SO网友:kaiser

注意:这里的回答是为了避免在评论中进行过多的讨论

这里是评论中的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 About Test 为此,我从csv文件中添加了大约47K个用户。之后,运行for循环,使前45个用户跟随其他用户。

这导致3704951条记录保存到我的自定义表中

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

我还必须声明↑ 上述时间跟踪无法真正测量,因为它还需要时间来计算循环。最好在第二个循环中循环通过生成的ID集。

此处进一步处理

结束

相关推荐

使用新的WP-Query()从循环中过滤后期格式;

嗨,我目前正在为我的博客构建一个主题。下面的代码指向最新的帖子(特色帖子)。因为这将有一个不同的风格比所有其他职位。然而我想过滤掉帖子格式:链接使用我在循环中定义的WP查询,因为它给我带来了更多的灵活性。我该怎么做呢? <?php $featured = new WP_Query(); $featured->query(\'showposts=1\'); ?> <?php while ($featured->have_post