WordPress源码深度解析之:`WordPress`的`main query`:`WP_Query`对象是如何在`wp-includes/class-wp-query.php`中构建和执行的。

各位观众老爷,大家好!我是今天的主讲人,咱们今天来聊聊WordPress里那个神秘又强大的WP_Query对象,看看它是如何在wp-includes/class-wp-query.php这个文件里诞生的,又是如何施展魔法,把数据库里的数据搬到我们眼前的。

准备好了吗?咱们开始吧!

第一部分:WP_Query的身世之谜——构造函数概览

话说这WP_Query啊,是WordPress的心脏之一,它负责从数据库里捞取各种各样的文章(posts)。要了解它,首先得看看它的构造函数,也就是__construct()方法。

public function __construct( $query = '' ) {
    $this->query = wp_parse_args( $query ); // 解析查询参数
    $this->query_vars_hash = md5( serialize( $this->query ) ); // 生成查询参数哈希值

    /**
     * Fires before the query variable object is set up.
     *
     * @since 2.1.0
     *
     * @param WP_Query &$this The WP_Query instance (passed by reference).
     */
    do_action_ref_array( 'pre_get_posts', array( &$this ) );

    $this->parse_query(); // 解析查询

    /**
     * Fires after the query variable object is set up, but before the actual query is run.
     *
     * @since 2.0.0
     *
     * @param WP_Query &$this The WP_Query instance (passed by reference).
     */
    do_action_ref_array( 'posts_selection', array( &$this ) );
}

这个构造函数看似简单,其实暗藏玄机:

  1. wp_parse_args():参数解析专家

    wp_parse_args()就像个翻译官,把我们传递给WP_Query的各种参数(比如category_nameposts_per_page等等)整理成一个标准的数组。例如:

    $args = array(
        'category_name' => 'news',
        'posts_per_page' => 5
    );
    $query = new WP_Query( $args );

    wp_parse_args()会把$args变成$this->query,方便后续处理。

  2. query_vars_hash:缓存小能手

    为了提高效率,WordPress会缓存查询结果。query_vars_hash就是用来生成缓存Key的。它把查询参数序列化后,再用MD5加密,确保相同的查询参数对应相同的缓存Key。

  3. pre_get_posts:改变命运的机会

    pre_get_posts是一个非常重要的action hook。它允许我们在WP_Query执行之前,修改查询参数。这意味着我们可以根据自己的需求,完全改变WordPress的查询行为。比如,我们可以让首页显示所有分类的文章,而不是默认的最新文章:

    function my_pre_get_posts( $query ) {
        if ( $query->is_home() && $query->is_main_query() ) {
            $query->set( 'category_name', '' ); // 显示所有分类的文章
        }
    }
    add_action( 'pre_get_posts', 'my_pre_get_posts' );

    is_home()判断是否是首页,is_main_query()判断是否是主查询。确保我们只修改首页的主查询。

  4. parse_query():解析查询指令

    parse_query()WP_Query的核心方法之一,它负责把$this->query中的参数,转换成SQL查询语句。这个过程非常复杂,涉及到各种条件判断和参数处理。我们稍后会详细讲解。

  5. posts_selection:最后的润色

    posts_selection是另一个action hook,它允许我们在查询执行之后,但在文章被返回之前,对查询结果进行最后的修改。

第二部分:parse_query()的奇妙旅程——SQL语句的诞生

parse_query()方法是WP_Query的心脏,它负责把我们提供的查询参数转换成SQL查询语句。这个过程非常复杂,涉及到各种条件判断和参数处理。

public function parse_query( $query = '' ) {
    if ( ! empty( $query ) ) {
        $this->query = wp_parse_args( $query, $this->query );
    }

    /**
     * Fires before the WP_Query->parse_query() method runs.
     *
     * @since 2.8.0
     *
     * @param WP_Query &$this The WP_Query instance (passed by reference).
     */
    do_action_ref_array( 'parse_query', array( &$this ) );

    $this->query_vars = array_merge( $this->query_vars, $this->extra_query_vars );

    foreach ( $this->query_vars as $qkey => $qvar ) {
        if ( isset( $this->query[ $qkey ] ) ) {
            $this->$qvar = $this->query[ $qkey ];
        }
    }

    // 处理各种查询参数,生成SQL语句...
    // ...
}

这个方法的主要流程如下:

  1. 合并查询参数

    把传递进来的查询参数和默认的查询参数合并。

  2. parse_query action hook

    允许我们在解析查询参数之前,对WP_Query对象进行修改。

  3. 设置查询变量

    $this->query中的参数赋值给$this的属性,方便后续使用。

  4. 处理各种查询参数,生成SQL语句

    这是最复杂的部分,涉及到各种条件判断和参数处理。下面我们重点讲解。

重点参数解析:

parse_query方法会根据不同的查询参数,生成不同的SQL语句。下面我们列举一些常用的查询参数,并解释它们是如何影响SQL语句的生成的:

查询参数 说明 SQL影响
category_name 根据分类别名查询文章。 JOIN wp_term_relationships ON (wp_posts.ID = wp_term_relationships.object_id) JOIN wp_term_taxonomy ON (wp_term_relationships.term_taxonomy_id = wp_term_taxonomy.term_taxonomy_id) JOIN wp_terms ON (wp_term_taxonomy.term_id = wp_terms.term_id) WHERE wp_term_taxonomy.taxonomy = 'category' AND wp_terms.slug IN ('category_name')
category__in 根据分类ID查询文章(多个ID)。 JOIN wp_term_relationships ON (wp_posts.ID = wp_term_relationships.object_id) JOIN wp_term_taxonomy ON (wp_term_relationships.term_taxonomy_id = wp_term_taxonomy.term_taxonomy_id) WHERE wp_term_taxonomy.taxonomy = 'category' AND wp_term_taxonomy.term_id IN (1,2,3)
tag 根据标签别名查询文章。 JOIN wp_term_relationships ON (wp_posts.ID = wp_term_relationships.object_id) JOIN wp_term_taxonomy ON (wp_term_relationships.term_taxonomy_id = wp_term_taxonomy.term_taxonomy_id) JOIN wp_terms ON (wp_term_taxonomy.term_id = wp_terms.term_id) WHERE wp_term_taxonomy.taxonomy = 'post_tag' AND wp_terms.slug IN ('tag_name')
tag__in 根据标签ID查询文章(多个ID)。 JOIN wp_term_relationships ON (wp_posts.ID = wp_term_relationships.object_id) JOIN wp_term_taxonomy ON (wp_term_relationships.term_taxonomy_id = wp_term_taxonomy.term_taxonomy_id) WHERE wp_term_taxonomy.taxonomy = 'post_tag' AND wp_term_taxonomy.term_id IN (4,5,6)
author 根据作者ID查询文章。 WHERE wp_posts.post_author = 1
author_name 根据作者别名查询文章。 JOIN wp_users ON (wp_posts.post_author = wp_users.ID) WHERE wp_users.user_nicename = 'author_name'
post_type 查询指定文章类型的文章。 WHERE wp_posts.post_type = 'post'
s 搜索关键词。 AND ((wp_posts.post_title LIKE '%keyword%') OR (wp_posts.post_excerpt LIKE '%keyword%') OR (wp_posts.post_content LIKE '%keyword%'))
posts_per_page 每页显示的文章数量。 LIMIT 0, 10 (取决于offsetposts_per_page)
offset 从第几篇文章开始显示。 LIMIT 10, 10 (取决于offsetposts_per_page)
orderby 排序方式。 ORDER BY wp_posts.post_date DESC
order 排序方向(ASC或DESC)。 ORDER BY wp_posts.post_date ASCORDER BY wp_posts.post_date DESC
meta_key 自定义字段的键名。 JOIN wp_postmeta ON (wp_posts.ID = wp_postmeta.post_id) WHERE wp_postmeta.meta_key = 'meta_key'
meta_value 自定义字段的值。 JOIN wp_postmeta ON (wp_posts.ID = wp_postmeta.post_id) WHERE wp_postmeta.meta_key = 'meta_key' AND wp_postmeta.meta_value = 'meta_value'
meta_query 更复杂的自定义字段查询。 会生成更复杂的JOINWHERE子句,取决于meta_query的具体内容。
date_query 日期查询。 会生成WHERE子句,根据日期范围查询文章。

代码示例:parse_tax_query()

为了更深入地了解parse_query()的工作原理,我们来看一个具体的例子:parse_tax_query()方法。这个方法负责处理分类和标签的查询。

protected function parse_tax_query( $qv ) {
    if ( ! empty( $qv['tax_query'] ) && is_array( $qv['tax_query'] ) ) {
        $tax_query = new WP_Tax_Query( $qv['tax_query'], $this->table_prefix );
        $this->tax_query = $tax_query;
    } else {
        $this->tax_query = new WP_Tax_Query();
    }

    $this->tax_query->queries = $this->tax_query->get_sql( $this->posts_table, 'ID' );
}

这个方法做了以下几件事:

  1. 创建WP_Tax_Query对象

    WP_Tax_Query是专门用来处理分类和标签查询的类。它接收一个数组,描述了分类和标签的查询条件。

  2. 调用WP_Tax_Query::get_sql()

    get_sql()方法会根据查询条件,生成SQL语句的JOINWHERE子句。

  3. 把SQL语句保存到$this->tax_query->queries

    这些SQL语句会在后续的查询过程中被用到。

第三部分:get_posts()的终极使命——执行查询并返回结果

get_posts()方法是WP_Query的最后一个环节,它负责执行SQL查询,并把查询结果返回给我们。

public function get_posts() {
    /**
     * Fires before the query is run.
     *
     * @since 2.0.0
     *
     * @param WP_Query &$this The WP_Query instance (passed by reference).
     */
    do_action_ref_array( 'pre_get_posts', array( &$this ) );

    // Convert query vars to something usable
    $this->parse_query();

    /**
     * Fires after the query is parsed, but before the query is executed.
     *
     * @since 2.0.0
     *
     * @param WP_Query &$this The WP_Query instance (passed by reference).
     */
    do_action_ref_array( 'posts_selection', array( &$this ) );

    return $this->query();
}

这个方法的主要流程如下:

  1. pre_get_posts action hook

    再次允许我们修改查询参数。

  2. parse_query()

    重新解析查询参数,确保是最新的参数。

  3. posts_selection action hook

    再次允许我们修改查询结果。

  4. $this->query()

    执行SQL查询,并把查询结果返回给我们。

代码示例:query()

query()方法是真正执行SQL查询的方法。

public function query( $query = '' ) {
    global $wpdb;

    $this->in_the_loop = false;
    $this->query = wp_parse_args( $query, $this->query );

    /**
     * Fires before the query is run.
     *
     * @since 2.0.0
     *
     * @param string &$sql The complete SQL query.
     * @param WP_Query &$this The WP_Query instance (passed by reference).
     */
    do_action_ref_array( 'posts_pre_query', array( &$sql, &$this ) );

    $this->result = $wpdb->get_results( $this->request ); // 执行SQL查询

    /**
     * Fires after the query has been run.
     *
     * @since 2.0.0
     *
     * @param WP_Query &$this The WP_Query instance (passed by reference).
     */
    do_action_ref_array( 'posts_results', array( &$this ) );

    // Process posts array
    $this->posts = array_map( 'wp_setup_postdata', $this->result ); // 转换成WP_Post对象

    $this->post_count = count( $this->posts );

    if ( $this->post_count > 0 ) {
        $this->post = $this->posts[0];
    }

    return $this->posts;
}

这个方法做了以下几件事:

  1. posts_pre_query action hook

    允许我们在SQL查询执行之前,对SQL语句进行修改。

  2. $wpdb->get_results()

    使用$wpdb对象执行SQL查询,并把查询结果保存到$this->result

  3. posts_results action hook

    允许我们在SQL查询执行之后,对查询结果进行修改。

  4. array_map( 'wp_setup_postdata', $this->result )

    把查询结果转换成WP_Post对象,方便后续使用。

第四部分:WP_Query的实战应用——定制你的查询

了解了WP_Query的工作原理,我们就可以利用它来定制各种各样的查询。下面我们列举一些常见的应用场景:

  1. 在首页显示指定分类的文章

    function my_home_query( $query ) {
        if ( $query->is_home() && $query->is_main_query() ) {
            $query->set( 'category_name', 'featured' ); // 只显示featured分类的文章
        }
    }
    add_action( 'pre_get_posts', 'my_home_query' );
  2. 创建一个自定义的查询

    $args = array(
        'post_type' => 'book', // 查询book类型的文章
        'posts_per_page' => 10, // 每页显示10篇文章
        'orderby' => 'title', // 按照标题排序
        'order' => 'ASC' // 升序排列
    );
    $the_query = new WP_Query( $args );
    
    if ( $the_query->have_posts() ) {
        while ( $the_query->have_posts() ) {
            $the_query->the_post();
            echo '<h2>' . get_the_title() . '</h2>';
            echo '<p>' . get_the_excerpt() . '</p>';
        }
        wp_reset_postdata(); // 恢复全局的$post变量
    } else {
        echo '<p>No books found.</p>';
    }
  3. 使用meta_query进行复杂的自定义字段查询

    $args = array(
        'meta_query' => array(
            'relation' => 'AND', // 多个条件之间的关系,可以是AND或OR
            array(
                'key' => 'price', // 自定义字段的键名
                'value' => 100, // 自定义字段的值
                'compare' => '>=' // 比较方式,可以是=、!=、>、>=、<、<=、LIKE、NOT LIKE、IN、NOT IN、BETWEEN、NOT BETWEEN、EXISTS、NOT EXISTS
            ),
            array(
                'key' => 'color',
                'value' => array( 'red', 'blue' ), // 多个值
                'compare' => 'IN' // 包含在这些值中
            )
        )
    );
    $the_query = new WP_Query( $args );

第五部分:WP_Query的注意事项——性能优化

WP_Query虽然强大,但是使用不当也会导致性能问题。下面我们列举一些注意事项:

  1. 避免在循环中使用WP_Query

    在循环中使用WP_Query会导致多次查询数据库,严重影响性能。应该尽量避免这种情况。

  2. 使用缓存

    WordPress自带了缓存机制,可以缓存查询结果,提高性能。

  3. 使用pre_get_posts修改主查询

    如果只是想修改主查询,应该使用pre_get_posts action hook,而不是创建一个新的WP_Query对象。

  4. 尽量使用标准的查询参数

    标准的查询参数经过了优化,性能更好。尽量使用标准的查询参数,而不是自己手动构建SQL语句。

  5. 注意posts_per_pageoffset的配合使用

    offset参数会导致性能问题,因为它需要跳过前面的文章。尽量避免使用offset参数,或者使用paged参数代替。

总结

WP_Query是WordPress的核心组件之一,它负责从数据库里捞取各种各样的文章。了解WP_Query的工作原理,可以帮助我们更好地定制查询,提高网站的性能。希望今天的讲座能对大家有所帮助。

本次讲座到此结束,感谢大家的观看!下次再见!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注