解析 WordPress `WP_Tax_Query` 类的源码:它如何将 `$tax_query` 参数解析为 SQL `JOIN` 和 `WHERE` 子句。

各位观众老爷,早上好!今天咱们来聊聊 WordPress 里一个非常重要的类——WP_Tax_Query。这玩意儿就像个魔法师,能把咱们定义的分类、标签查询条件,变成数据库能理解的 SQL 代码,从而筛选出我们想要的文章。

一、 啥是 WP_Tax_Query?为什么要研究它?

简单来说,WP_Tax_Query 就是 WordPress 用来处理分类法(Taxonomy)查询的类。当你用 WP_Query 查询文章,并且需要根据分类、标签、自定义分类法进行筛选时,WP_Tax_Query 就在背后默默工作。

研究它干啥?

  • 定制化查询: 深入理解 WP_Tax_Query,你就能写出更复杂、更精准的分类法查询,满足各种奇葩需求。
  • 性能优化: 了解它如何生成 SQL,你可以避免写出低效的查询条件,提升网站速度。
  • 调试问题: 当你的分类法查询出现问题时,理解 WP_Tax_Query 能帮你更快地找到问题根源。

二、 WP_Tax_Query 的基本结构

WP_Tax_Query 接收一个参数 $tax_query,这个参数是一个数组,用来描述你的分类法查询条件。 比如:

$args = array(
    'tax_query' => array(
        array(
            'taxonomy' => 'category',
            'field'    => 'slug',
            'terms'    => array( 'news', 'featured' ),
            'operator' => 'IN',
        ),
    ),
);
$query = new WP_Query( $args );

上面的代码表示:查询属于 category 分类下 slugnewsfeatured 的文章。

$tax_query 数组可以包含多个子数组,每个子数组代表一个分类法查询条件。 还可以有关联关系,表示多个分类法条件之间的 ANDOR关系。

三、 WP_Tax_Query 的源码剖析

好,现在咱们开始扒 WP_Tax_Query 的源码,看看它到底是怎么工作的。

首先,找到 wp-includes/class-wp-tax-query.php 文件。

  1. 构造函数 __construct( $tax_query )

    构造函数主要负责初始化 WP_Tax_Query 对象,并解析传入的 $tax_query 参数。

    public function __construct( $tax_query ) {
        if ( isset( $tax_query['relation'] ) && in_array( strtoupper( $tax_query['relation'] ), array( 'AND', 'OR' ), true ) ) {
            $this->relation = strtoupper( $tax_query['relation'] );
        } else {
            $this->relation = 'AND';
        }
    
        if ( isset( $tax_query[0] ) && is_array( $tax_query[0] ) ) {
            $this->queries = $this->normalize_queries( $tax_query );
        } else {
            $this->queries = array( $tax_query );
        }
    
        $this->queries = $this->fill_query_defaults( $this->queries );
    }
    • $this->relation 记录查询条件之间的关系,默认为 AND。 就像SQL语句中的ANDOR
    • $this->queries 存储解析后的查询条件数组。
    • normalize_queries() 标准化查询条件数组,确保每个条件都是一个数组。
    • fill_query_defaults() 为每个查询条件填充默认值,比如默认的 operatorIN
  2. normalize_queries( $queries )

    这个函数负责将 $tax_query 参数标准化,确保每个查询条件都是一个数组。 它的作用是处理嵌套的 $tax_query 数组。

    private function normalize_queries( $queries ) {
        $normalized = array();
    
        foreach ( $queries as $key => $query ) {
            if ( 'relation' === $key ) {
                $normalized['relation'] = strtoupper( $query );
                continue;
            }
    
            if ( ! is_array( $query ) ) {
                continue;
            }
    
            $keys = array_keys( $query );
            sort( $keys );
    
            if ( $keys === array( 'field', 'taxonomy', 'terms' ) || $keys === array( 'field', 'operator', 'taxonomy', 'terms' ) ) {
                $normalized[] = $query;
            } elseif ( isset( $query[0] ) && is_array( $query[0] ) ) {
                // It's an embedded array, parse each query.
                $normalized[] = $this->normalize_queries( $query );
            }
        }
    
        return $normalized;
    }
  3. fill_query_defaults( $queries )

    这个函数为每个查询条件填充默认值。 如果用户没有指定 operator,就默认为 IN

    private function fill_query_defaults( $queries ) {
        $defaults = array(
            'taxonomy' => '',
            'field'    => 'term_id',
            'terms'    => array(),
            'operator' => 'IN',
        );
    
        foreach ( $queries as $key => $query ) {
            if ( isset( $query['relation'] ) ) {
                continue;
            }
    
            $queries[ $key ] = array_merge( $defaults, $query );
        }
    
        return $queries;
    }
  4. get_sql( $table_prefix, $query, $type = 'AND' )

    这个函数是 WP_Tax_Query 的核心,它负责生成 SQL 的 JOINWHERE 子句。

    public function get_sql( $table_prefix, $query, $type = 'AND' ) {
        // 省略一部分代码,关注核心逻辑
    
        $sql = $this->get_sql_clauses( $table_prefix, $query );
    
        $sql['where'] = 'AND ' . $sql['where'];
    
        return $sql;
    }

    get_sql() 函数调用了 get_sql_clauses() 函数来生成 SQL 子句。

  5. get_sql_clauses( $table_prefix, $query )

    这个函数递归地处理查询条件,生成 SQL 的 JOINWHERE 子句。

    public function get_sql_clauses( $table_prefix, $query ) {
        $sql = array(
            'where'  => 'AND 1=1',
            'join'   => '',
        );
    
        $sql_chunks = array(
            'relation' => 'AND',
            'where'  => array(),
            'join'   => array(),
        );
    
        $taxonomies = array();
    
        $i = 0;
        foreach ( $this->queries as $tax_query ) {
            $i++;
    
            if ( ! is_array( $tax_query ) ) {
                continue;
            }
    
            if ( isset( $tax_query['relation'] ) ) {
                $sql_chunks['relation'] = $tax_query['relation'];
                continue;
            }
    
            $taxonomy = $tax_query['taxonomy'];
            $field    = $tax_query['field'];
            $terms    = $tax_query['terms'];
            $operator = $tax_query['operator'];
    
            if ( ! taxonomy_exists( $taxonomy ) ) {
                continue;
            }
    
            if ( empty( $terms ) ) {
                continue;
            }
    
            $taxonomies[] = $taxonomy;
    
            $base_table = "{$table_prefix}term_relationships";
            $alias = 'tr' . $i; // 别名
            $terms_table = "{$table_prefix}terms";
            $term_taxonomy_table = "{$table_prefix}term_taxonomy";
    
            $sql_chunks['join'][] = "INNER JOIN {$base_table} AS {$alias} ON ($query.ID = {$alias}.object_id)";
            $sql_chunks['join'][] = "INNER JOIN {$term_taxonomy_table} AS tt{$i} ON ({$alias}.term_taxonomy_id = tt{$i}.term_taxonomy_id)";
            $sql_chunks['join'][] = "INNER JOIN {$terms_table} AS t{$i} ON (tt{$i}.term_id = t{$i}.term_id)";
    
            switch ( $field ) {
                case 'term_id':
                    $terms_placeholder = implode( ',', array_map( 'intval', $terms ) );
                    break;
                case 'name':
                case 'slug':
                    $terms_placeholder = "'" . implode( "', '", array_map( 'esc_sql', $terms ) ) . "'";
                    break;
                case 'term_taxonomy_id':
                    $terms_placeholder = implode( ',', array_map( 'intval', $terms ) );
                    break;
                default:
                    continue 2; // Skip this query.
            }
    
            switch ( $operator ) {
                case 'IN':
                    $sql_chunks['where'][] = "tt{$i}.taxonomy = '$taxonomy' AND t{$i}.{$field} IN ($terms_placeholder)";
                    break;
                case 'NOT IN':
                    $sql_chunks['where'][] = "tt{$i}.taxonomy = '$taxonomy' AND t{$i}.{$field} NOT IN ($terms_placeholder)";
                    break;
                case 'AND':
                    $terms_placeholder = "'" . implode( "' AND t{$i}.{$field} = '", array_map( 'esc_sql', $terms ) ) . "'";
                    $sql_chunks['where'][] = "tt{$i}.taxonomy = '$taxonomy' AND t{$i}.{$field} = $terms_placeholder";
                    break;
                case 'EXISTS':
                    $sql_chunks['where'][] = "tt{$i}.taxonomy = '$taxonomy'";
                    break;
                case 'NOT EXISTS':
                    $sql_chunks['where'][] = "tt{$i}.taxonomy <> '$taxonomy'";
                    break;
                default:
                    continue 2; // Skip this query.
            }
        }
    
        $sql['join'] = array_unique( $sql_chunks['join'] );
        $sql['join'] = implode( "n", $sql['join'] );
    
        $sql['where'] = implode( " {$sql_chunks['relation']} ", $sql_chunks['where'] );
    
        return $sql;
    }

    这个函数做了以下几件事:

    • 循环处理每个查询条件: 遍历 $this->queries 数组,处理每个分类法查询条件。
    • 生成 JOIN 子句: 根据分类法和文章的关系,生成 JOIN 子句,连接 wp_term_relationships, wp_term_taxonomy, wp_terms 表。 关键在于给每个JOIN的表定义别名,例如 tr1tt1t1, 以便在WHERE子句中引用。
    • 生成 WHERE 子句: 根据 fieldtermsoperator,生成 WHERE 子句,筛选出符合条件的文章。 这里会根据不同的fieldoperator 生成不同的SQL语句。
    • 处理 ANDOR 关系: 如果 $tax_query 中有 relation 字段,则根据 ANDOR 关系,将多个 WHERE 子句连接起来。

四、 举例说明

咱们还是用最开始的例子:

$args = array(
    'tax_query' => array(
        array(
            'taxonomy' => 'category',
            'field'    => 'slug',
            'terms'    => array( 'news', 'featured' ),
            'operator' => 'IN',
        ),
    ),
);
$query = new WP_Query( $args );

经过 WP_Tax_Query 的处理,最终生成的 SQL 如下(简化版):

SELECT wp_posts.*
FROM wp_posts
INNER JOIN wp_term_relationships AS tr1 ON (wp_posts.ID = tr1.object_id)
INNER JOIN wp_term_taxonomy AS tt1 ON (tr1.term_taxonomy_id = tt1.term_taxonomy_id)
INNER JOIN wp_terms AS t1 ON (tt1.term_id = t1.term_id)
WHERE 1=1
AND tt1.taxonomy = 'category'
AND t1.slug IN ('news', 'featured')
AND wp_posts.post_type = 'post'
AND ((wp_posts.post_status = 'publish'))
ORDER BY wp_posts.post_date DESC

可以看到,WP_Tax_Query 成功地将分类法查询条件转换成了 SQL 的 JOINWHERE 子句。

五、 复杂查询示例

假设我们要查询同时属于 category 分类下的 newsfeatured,并且属于 post_tag 标签下的 important 的文章。

$args = array(
    'tax_query' => array(
        'relation' => 'AND',
        array(
            'taxonomy' => 'category',
            'field'    => 'slug',
            'terms'    => array( 'news', 'featured' ),
            'operator' => 'AND',
        ),
        array(
            'taxonomy' => 'post_tag',
            'field'    => 'slug',
            'terms'    => array( 'important' ),
            'operator' => 'IN',
        ),
    ),
);
$query = new WP_Query( $args );

最终生成的 SQL 如下(简化版):

SELECT wp_posts.*
FROM wp_posts
INNER JOIN wp_term_relationships AS tr1 ON (wp_posts.ID = tr1.object_id)
INNER JOIN wp_term_taxonomy AS tt1 ON (tr1.term_taxonomy_id = tt1.term_taxonomy_id)
INNER JOIN wp_terms AS t1 ON (tt1.term_id = t1.term_id)
INNER JOIN wp_term_relationships AS tr2 ON (wp_posts.ID = tr2.object_id)
INNER JOIN wp_term_taxonomy AS tt2 ON (tr2.term_taxonomy_id = tt2.term_taxonomy_id)
INNER JOIN wp_terms AS t2 ON (tt2.term_id = t2.term_id)
WHERE 1=1
AND ( tt1.taxonomy = 'category' AND t1.slug = 'news' AND t1.slug = 'featured' )
AND tt2.taxonomy = 'post_tag' AND t2.slug IN ('important')
AND wp_posts.post_type = 'post'
AND ((wp_posts.post_status = 'publish'))
ORDER BY wp_posts.post_date DESC

注意:

  • 'relation' => 'AND' 指定了多个分类法条件之间的关系。
  • categoryoperatorAND,表示文章必须同时属于 newsfeatured 这两个分类。
  • post_tagoperatorIN,表示文章属于 important 这个标签。

六、 WP_Tax_Query 的高级用法

  1. 使用 EXISTSNOT EXISTS

    EXISTSNOT EXISTS 可以用来判断文章是否属于某个分类法。

    $args = array(
        'tax_query' => array(
            array(
                'taxonomy' => 'category',
                'operator' => 'EXISTS', // 查询属于 category 分类的文章
            ),
        ),
    );
    $query = new WP_Query( $args );
  2. 使用嵌套的 tax_query

    WP_Tax_Query 支持嵌套的 tax_query,可以实现更复杂的查询逻辑。

    $args = array(
        'tax_query' => array(
            'relation' => 'OR',
            array(
                'taxonomy' => 'category',
                'field'    => 'slug',
                'terms'    => array( 'news' ),
            ),
            array(
                'relation' => 'AND',
                array(
                    'taxonomy' => 'post_tag',
                    'field'    => 'slug',
                    'terms'    => array( 'important' ),
                ),
                array(
                    'taxonomy' => 'custom_taxonomy',
                    'field'    => 'term_id',
                    'terms'    => array( 123 ),
                ),
            ),
        ),
    );
    $query = new WP_Query( $args );

    上面的代码表示:查询属于 category 分类下的 news,或者同时属于 post_tag 标签下的 important 并且属于 custom_taxonomy 分类下的 term_id 为 123 的文章。

七、 性能优化建议

  • 避免使用 name 作为 field name 字段没有索引,查询效率较低。 尽量使用 term_idslug
  • 尽量使用 IN 操作符: IN 操作符的效率通常比多个 OR 操作符要高。
  • 避免过度复杂的查询: 复杂的查询会导致 SQL 语句过于庞大,影响性能。 可以考虑将复杂的查询拆分成多个简单的查询。
  • 利用缓存: 对查询结果进行缓存,可以减少数据库的访问次数,提高网站速度。

八、 总结

WP_Tax_Query 是 WordPress 中一个非常强大的类,它能让我们灵活地控制分类法查询。 理解 WP_Tax_Query 的工作原理,可以帮助我们写出更高效、更精准的查询代码。

记住,掌握了 WP_Tax_Query,就像掌握了一门魔法,能让你的 WordPress 网站更加强大! 今天就到这里,希望大家有所收获! 下课!

发表回复

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