阐述 WordPress `WP_Query` 类中的 `tax_query` 参数源码:如何构建复杂的分类法查询。

好了,各位同学,今天咱们来聊聊 WordPress 里那个让人又爱又恨的 WP_Query 类,特别是它的 tax_query 参数。这玩意儿就像个俄罗斯方块,看起来简单,玩起来却能组合出各种花样,构建出复杂的分类法查询。准备好,我们要开始深入源码,扒开它的底裤,看看它到底是怎么工作的!

开场白:分类法查询的必要性

想象一下,你经营一家在线书店,书籍按类别(比如小说、历史、科幻)和标签(比如畅销书、新书、经典)进行分类。你想展示:

  • 所有小说和科幻类别的书籍。
  • 既是畅销书又是新书的历史类书籍。
  • 排除所有经典科幻类书籍。

如果只用简单的 category_name 或者 tag 参数,恐怕要累死你。这时候,tax_query 就闪亮登场了,它能帮你构建出各种复杂的分类法查询,满足你刁钻的需求。

tax_query 参数:基本结构

tax_query 本质上是一个数组,数组中的每个元素代表一个分类法查询条件。最简单的 tax_query 看起来像这样:

$args = array(
    'post_type' => 'book',
    'tax_query' => array(
        array(
            'taxonomy' => 'category',  // 分类法名称
            'field'    => 'slug',      // 字段类型 (term_id, name, slug)
            'terms'    => array( 'fiction' ), // 查询的分类法术语
        ),
    ),
);

$query = new WP_Query( $args );

这段代码的意思是:查询所有 book 类型的文章,且分类为 fiction 的。

让我们拆解一下这个数组:

  • taxonomy: 指定你要查询的分类法,比如 category(分类)、post_tag(标签)或者自定义分类法。
  • field: 指定你用来匹配术语的字段。常用的有 term_id(术语 ID)、name(术语名称)和 slug(术语别名)。
  • terms: 一个数组,包含你想要匹配的术语。

field 参数的详细解释

  • term_id: 这是最直接的匹配方式,直接使用术语的 ID。 效率最高,但你需要知道每个术语的 ID。

    array(
        'taxonomy' => 'category',
        'field'    => 'term_id',
        'terms'    => array( 1, 5, 10 ), // 分类 ID 为 1, 5, 10 的文章
    ),
  • name: 使用术语的名称进行匹配。 注意,术语名称可能不是唯一的。

    array(
        'taxonomy' => 'category',
        'field'    => 'name',
        'terms'    => array( '小说', '科幻' ), // 分类名称为 "小说" 或 "科幻" 的文章
    ),
  • slug: 使用术语的别名进行匹配。 别名通常是唯一的,并且更适合在 URL 中使用。

    array(
        'taxonomy' => 'category',
        'field'    => 'slug',
        'terms'    => array( 'fiction', 'sci-fi' ), // 分类别名为 "fiction" 或 "sci-fi" 的文章
    ),

组合多个分类法查询:relation 参数

如果想同时查询多个分类法,就需要用到 relation 参数。 relation 参数有两个值:ANDOR

  • AND: 所有条件都必须满足。
  • OR: 至少一个条件满足即可。

例如,查询既是“畅销书”又是“新书”的历史类书籍:

$args = array(
    'post_type' => 'book',
    'tax_query' => array(
        'relation' => 'AND', // 所有条件都必须满足
        array(
            'taxonomy' => 'category',
            'field'    => 'slug',
            'terms'    => array( 'history' ),
        ),
        array(
            'taxonomy' => 'post_tag',
            'field'    => 'slug',
            'terms'    => array( 'bestseller' ),
        ),
        array(
            'taxonomy' => 'post_tag',
            'field'    => 'slug',
            'terms'    => array( 'new' ),
        ),
    ),
);

$query = new WP_Query( $args );

这段代码会查询所有 book 类型的文章,并且:

  1. 分类必须是 history
  2. 标签必须包含 bestseller
  3. 标签必须包含 new

如果把 relation 改成 OR,那么查询结果就会变成:分类是 history 或者标签是 bestseller 或者标签是 new 的文章。

高级用法:operator 参数

operator 参数允许你更精确地控制分类法查询的行为。 它可以用于数组里的单独一项, 具有以下几个值:

  • IN: 术语必须在 terms 数组中。 (默认值)
  • NOT IN: 术语不能在 terms 数组中。
  • AND: 文章必须包含 terms 数组中的 所有 术语。(只适用于分层分类法,比如分类)
  • EXISTS: 文章必须与该分类法关联。(忽略 terms 数组)
  • NOT EXISTS: 文章不能与该分类法关联。(忽略 terms 数组)

INNOT IN 的例子

IN 是默认值,所以我们之前的例子都隐含地使用了 INNOT IN 用于排除某些术语。 例如,排除所有经典科幻类书籍:

$args = array(
    'post_type' => 'book',
    'tax_query' => array(
        'relation' => 'AND',
        array(
            'taxonomy' => 'category',
            'field'    => 'slug',
            'terms'    => array( 'sci-fi' ),
        ),
        array(
            'taxonomy' => 'post_tag',
            'field'    => 'slug',
            'terms'    => array( 'classic' ),
            'operator' => 'NOT IN', // 排除包含 "classic" 标签的科幻小说
        ),
    ),
);

$query = new WP_Query( $args );

这段代码会查询所有 book 类型的文章,且:

  1. 分类必须是 sci-fi
  2. 标签不能包含 classic

AND 的例子 (分层分类法)

AND 操作符只适用于分层分类法,比如分类。 它要求文章必须同时属于 terms 数组中的 所有 术语。 为了更好地理解,我们假设分类法 genre 有以下结构:

- Genre
    - Fantasy
        - Epic Fantasy
        - Dark Fantasy
    - Science Fiction

现在,我们想查询既属于 Fantasy 又属于 Epic Fantasy 的书籍:

$args = array(
    'post_type' => 'book',
    'tax_query' => array(
        array(
            'taxonomy' => 'genre',
            'field'    => 'slug',
            'terms'    => array( 'fantasy', 'epic-fantasy' ),
            'operator' => 'AND', // 必须同时属于 "fantasy" 和 "epic-fantasy"
        ),
    ),
);

$query = new WP_Query( $args );

在这个例子中,只有同时属于 FantasyEpic Fantasy 这两个分类的书籍才会被查询出来。 如果一本书只属于 Fantasy,或者只属于 Epic Fantasy,都不会被包含在结果中。

EXISTSNOT EXISTS 的例子

EXISTS 用于查询与特定分类法关联的文章,而 NOT EXISTS 用于查询 没有 与特定分类法关联的文章。 这两个操作符忽略 terms 数组。

例如,查询所有与 genre 分类法关联的书籍:

$args = array(
    'post_type' => 'book',
    'tax_query' => array(
        array(
            'taxonomy' => 'genre',
            'operator' => 'EXISTS', // 必须与 "genre" 分类法关联
        ),
    ),
);

$query = new WP_Query( $args );

相反,查询所有 没有genre 分类法关联的书籍:

$args = array(
    'post_type' => 'book',
    'tax_query' => array(
        array(
            'taxonomy' => 'genre',
            'operator' => 'NOT EXISTS', // 不能与 "genre" 分类法关联
        ),
    ),
);

$query = new WP_Query( $args );

WP_Query 源码解析:get_tax_sql() 函数

好了,理论讲了一大堆,现在让我们深入 WP_Query 的源码,看看 tax_query 到底是怎么被处理的。 WP_Query 类中有一个非常重要的函数:get_tax_sql()。 这个函数负责根据 tax_query 参数生成 SQL 查询语句。

虽然完整的 get_tax_sql() 函数很长,但我们可以提取出关键部分,来理解它的工作原理。 以下是一个简化的版本:

/**
 * Simplified version of WP_Query::get_tax_sql()
 */
function simplified_get_tax_sql( &$wp_query ) {
    global $wpdb;

    $tax_query = $wp_query->tax_query;

    if ( ! is_array( $tax_query->queries ) || empty( $tax_query->queries ) ) {
        return array(
            'clauses' => '',
            'join'    => '',
        );
    }

    $sql_clauses = array(
        'clauses' => '',
        'join'    => '',
    );

    $sql_relation = 'AND';
    if ( isset( $tax_query->relation ) && in_array( strtoupper( $tax_query->relation ), array( 'AND', 'OR' ), true ) ) {
        $sql_relation = $tax_query->relation;
    }

    $sql = array();
    $joins = array();

    $i = 0;
    foreach ( $tax_query->queries as $tax_query_single ) {
        $i++;
        $taxonomy = $tax_query_single['taxonomy'];
        $terms = $tax_query_single['terms'];
        $field = $tax_query_single['field'];
        $operator = isset( $tax_query_single['operator'] ) ? strtoupper( $tax_query_single['operator'] ) : 'IN';

        $table_alias = "tt{$i}"; // Generate unique table alias

        $joins[ $table_alias ] = "INNER JOIN {$wpdb->term_relationships} tr{$i} ON ($wpdb->posts.ID = tr{$i}.object_id) INNER JOIN {$wpdb->term_taxonomy} {$table_alias} ON (tr{$i}.term_taxonomy_id = {$table_alias}.term_taxonomy_id)";

        $where = '';

        switch ( $field ) {
            case 'term_id':
                $where = $table_alias . '.term_id IN (' . implode( ',', array_map( 'intval', $terms ) ) . ')';
                break;
            case 'name':
                $where = $table_alias . '.name IN (' . implode( ',', array_map( array( $wpdb, 'prepare' ), array_fill( 0, count( $terms ), '%s' ), $terms ) ) . ')';
                break;
            case 'slug':
                $where = $table_alias . '.slug IN (' . implode( ',', array_map( array( $wpdb, 'prepare' ), array_fill( 0, count( $terms ), '%s' ), $terms ) ) . ')';
                break;
        }

        if ( 'NOT IN' === $operator ) {
            $where = 'NOT (' . $where . ')';
        }

        $sql[] = $where;
    }

    if ( ! empty( $sql ) ) {
        $sql_clauses['clauses'] = 'AND (' . implode( " {$sql_relation} ", $sql ) . ')';
        $sql_clauses['join']    = implode( ' ', $joins );
    }

    return $sql_clauses;
}

这个简化版的函数做了以下事情:

  1. 获取 tax_query 对象: 从 WP_Query 实例中获取 tax_query 对象。
  2. 遍历查询条件: 遍历 tax_query->queries 数组,数组中的每个元素代表一个分类法查询条件。
  3. 构建 JOIN 子句: 根据分类法查询条件,生成 JOIN 子句,将 wp_posts 表与 wp_term_relationshipswp_term_taxonomy 表连接起来。 每个分类法查询都会生成一个唯一的表别名(例如 tt1, tt2),以避免命名冲突。
  4. 构建 WHERE 子句: 根据 fieldterms 参数,生成 WHERE 子句,用于筛选符合条件的术语。 operator 参数会影响 WHERE 子句的生成方式(例如 INNOT IN)。
  5. 组合 SQL 子句: 使用 relation 参数(ANDOR)将所有 WHERE 子句组合起来。
  6. 返回 SQL 子句: 将生成的 JOINWHERE 子句返回。

重点解释

  • 表别名: $table_alias = "tt{$i}"; 这行代码为每个分类法查询生成一个唯一的表别名。 这是避免多个分类法查询之间命名冲突的关键。 如果没有表别名,SQL 查询可能会出错,导致查询结果不正确。
  • $wpdb->prepare(): 这行代码用于安全地处理用户输入,防止 SQL 注入攻击。 它将 terms 数组中的值转义,确保它们不会被解释为 SQL 代码。
  • implode(): 这行代码将数组中的值连接成一个字符串,用于构建 IN 子句。 例如,implode( ',', array( 1, 2, 3 ) ) 会生成字符串 1,2,3

WP_Tax_Query

你可能注意到了,上面的代码中出现了 $wp_query->tax_query。 实际上,tax_query 并不是一个简单的数组,而是一个 WP_Tax_Query 类的实例。 WP_Tax_Query 类负责解析和验证 tax_query 参数,并将其转换为 SQL 查询语句。

让我们简单看一下 WP_Tax_Query 类的一些关键方法:

  • __construct( $tax_query ): 构造函数,接受 tax_query 参数,并进行解析和验证。
  • parse_query( $query ): 解析 tax_query 参数,将其转换为内部数据结构。
  • get_sql( $table_prefix, $primary_table, $primary_id_column ): 生成 SQL 查询语句。

WP_Tax_Query 类的主要作用是:

  1. 验证 tax_query 参数: 确保 tax_query 参数的格式正确,并且包含必要的参数(例如 taxonomyfieldterms)。
  2. 规范化 tax_query 参数: 将 tax_query 参数转换为一种规范的格式,方便后续处理。
  3. 生成 SQL 查询语句: 根据规范化的 tax_query 参数,生成 SQL 查询语句。

总结

WP_Querytax_query 参数是一个强大的工具,可以让你构建出各种复杂的分类法查询。 理解 tax_query 的基本结构,掌握 relationoperator 参数的用法,并深入了解 WP_QueryWP_Tax_Query 类的源码,可以让你更好地利用 tax_query 参数,实现更灵活、更精确的内容查询。

记住,tax_query 就像俄罗斯方块,熟练掌握它的规则,就能拼出你想要的任何形状!

好啦,今天的讲座就到这里。希望大家有所收获,下次再见!

发表回复

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