好了,各位同学,今天咱们来聊聊 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
参数有两个值:AND
和 OR
。
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
类型的文章,并且:
- 分类必须是
history
。 - 标签必须包含
bestseller
。 - 标签必须包含
new
。
如果把 relation
改成 OR
,那么查询结果就会变成:分类是 history
或者标签是 bestseller
或者标签是 new
的文章。
高级用法:operator
参数
operator
参数允许你更精确地控制分类法查询的行为。 它可以用于数组里的单独一项, 具有以下几个值:
IN
: 术语必须在terms
数组中。 (默认值)NOT IN
: 术语不能在terms
数组中。AND
: 文章必须包含terms
数组中的 所有 术语。(只适用于分层分类法,比如分类)EXISTS
: 文章必须与该分类法关联。(忽略terms
数组)NOT EXISTS
: 文章不能与该分类法关联。(忽略terms
数组)
IN
和 NOT IN
的例子
IN
是默认值,所以我们之前的例子都隐含地使用了 IN
。 NOT 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
类型的文章,且:
- 分类必须是
sci-fi
。 - 标签不能包含
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 );
在这个例子中,只有同时属于 Fantasy
和 Epic Fantasy
这两个分类的书籍才会被查询出来。 如果一本书只属于 Fantasy
,或者只属于 Epic Fantasy
,都不会被包含在结果中。
EXISTS
和 NOT 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;
}
这个简化版的函数做了以下事情:
- 获取
tax_query
对象: 从WP_Query
实例中获取tax_query
对象。 - 遍历查询条件: 遍历
tax_query->queries
数组,数组中的每个元素代表一个分类法查询条件。 - 构建 JOIN 子句: 根据分类法查询条件,生成
JOIN
子句,将wp_posts
表与wp_term_relationships
和wp_term_taxonomy
表连接起来。 每个分类法查询都会生成一个唯一的表别名(例如tt1
,tt2
),以避免命名冲突。 - 构建 WHERE 子句: 根据
field
和terms
参数,生成WHERE
子句,用于筛选符合条件的术语。operator
参数会影响WHERE
子句的生成方式(例如IN
或NOT IN
)。 - 组合 SQL 子句: 使用
relation
参数(AND
或OR
)将所有WHERE
子句组合起来。 - 返回 SQL 子句: 将生成的
JOIN
和WHERE
子句返回。
重点解释
- 表别名:
$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
类的主要作用是:
- 验证
tax_query
参数: 确保tax_query
参数的格式正确,并且包含必要的参数(例如taxonomy
、field
、terms
)。 - 规范化
tax_query
参数: 将tax_query
参数转换为一种规范的格式,方便后续处理。 - 生成 SQL 查询语句: 根据规范化的
tax_query
参数,生成 SQL 查询语句。
总结
WP_Query
的 tax_query
参数是一个强大的工具,可以让你构建出各种复杂的分类法查询。 理解 tax_query
的基本结构,掌握 relation
和 operator
参数的用法,并深入了解 WP_Query
和 WP_Tax_Query
类的源码,可以让你更好地利用 tax_query
参数,实现更灵活、更精确的内容查询。
记住,tax_query
就像俄罗斯方块,熟练掌握它的规则,就能拼出你想要的任何形状!
好啦,今天的讲座就到这里。希望大家有所收获,下次再见!