大家好!今天咱们来聊聊 WordPress 里一个相当重要的家伙,WP_Tax_Query
。这家伙专门负责构建复杂的分类法查询,让你在 WordPress 世界里按照各种奇葩的方式筛选文章,简直是分类法查询的瑞士军刀。
开场白:分类法的烦恼
想象一下,你经营着一个美食博客。你的文章不仅有“菜系”(比如川菜、粤菜),还有“食材”(比如猪肉、牛肉),甚至还有“烹饪方式”(比如炒、炸、蒸)。现在你想找到所有:
- 属于川菜,并且用了猪肉的菜谱。
- 属于粤菜,或者用了牛肉的菜谱。
- 既不属于川菜,也没用猪肉的菜谱。
这仅仅是开始。如果再加点难度,比如“属于川菜,但不能是麻辣口味的”,你是不是已经开始头疼了?
别慌! WP_Tax_Query
就是来拯救你的。它能帮你把这些复杂的逻辑转化成 WordPress 能够理解的 SQL 查询语句。
WP_Tax_Query
的基本结构
先来看看 WP_Tax_Query
的基本结构。它本质上是一个数组,里面包含了一个或多个分类法查询的条件。每个条件都是一个关联数组,描述了你想对哪个分类法进行怎样的筛选。
$tax_query = new WP_Tax_Query(
array(
'relation' => 'AND', // 条件之间的关系,可以是 'AND' 或 'OR'
array(
'taxonomy' => 'cuisine', // 分类法名称
'field' => 'slug', // 用于匹配的字段,可以是 'term_id', 'name', 'slug'
'terms' => array( 'sichuan' ), // 要匹配的值,可以是单个值或数组
'operator' => 'IN', // 匹配操作符,可以是 'IN', 'NOT IN', 'AND', 'EXISTS', 'NOT EXISTS'
),
array(
'taxonomy' => 'ingredient',
'field' => 'slug',
'terms' => array( 'pork' ),
'operator' => 'IN',
),
)
);
这段代码构建了一个 WP_Tax_Query
对象,它表示查询所有属于“川菜”并且使用了“猪肉”的文章。
relation
: 定义了多个条件之间的关系。'AND'
表示所有条件都必须满足,'OR'
表示只要满足其中一个条件即可。taxonomy
: 指定你要查询的分类法。比如'category'
、'post_tag'
或者你自己定义的分类法。field
: 指定用哪个字段来匹配。常用的有'term_id'
(分类法 ID)、'name'
(分类法名称) 和'slug'
(分类法别名)。terms
: 指定要匹配的值。可以是一个值,也可以是一个包含多个值的数组。operator
: 指定匹配的操作符。这是最灵活的部分,决定了如何将field
和terms
进行比较。
operator
的各种用法
operator
是 WP_Tax_Query
的灵魂所在。不同的操作符可以实现各种复杂的筛选逻辑。
操作符 | 含义 |
---|---|
'IN' |
field 的值必须在 terms 数组中。 |
'NOT IN' |
field 的值不能在 terms 数组中。 |
'AND' |
field 的值必须同时包含 terms 数组中的所有值(仅当 taxonomy 支持多个 term 时有效,例如多选的自定义分类法)。 |
'EXISTS' |
文章必须关联到指定的 taxonomy 。 terms 会被忽略。 |
'NOT EXISTS' |
文章不能关联到指定的 taxonomy 。 terms 会被忽略。 |
举几个例子:
'operator' => 'IN', 'terms' => array( 'sichuan', 'cantonese' )
: 表示属于川菜 或 粤菜。'operator' => 'NOT IN', 'terms' => array( 'spicy' )
: 表示不属于“麻辣”口味。'operator' => 'EXISTS'
: 表示必须关联到该分类法。'operator' => 'NOT EXISTS'
: 表示不能关联到该分类法。
WP_Query
中的应用
WP_Tax_Query
的最终目的是为 WP_Query
提供查询条件。你可以将 WP_Tax_Query
对象放到 WP_Query
的参数中:
$args = array(
'post_type' => 'recipe',
'tax_query' => array(
'relation' => 'AND',
array(
'taxonomy' => 'cuisine',
'field' => 'slug',
'terms' => array( 'sichuan' ),
'operator' => 'IN',
),
array(
'taxonomy' => 'ingredient',
'field' => 'slug',
'terms' => array( 'pork' ),
'operator' => 'IN',
),
),
);
$query = new WP_Query( $args );
if ( $query->have_posts() ) {
while ( $query->have_posts() ) {
$query->the_post();
// 显示文章内容
echo get_the_title() . '<br>';
}
} else {
echo '没有找到符合条件的文章。';
}
wp_reset_postdata();
这段代码会查询所有类型为 recipe
(假设你自定义了一个文章类型叫 recipe
),并且属于“川菜”并且使用了“猪肉”的文章。
源码解析:get_sql()
方法
WP_Tax_Query
最核心的方法是 get_sql()
。它负责将你定义的分类法查询条件转化成 SQL 语句。
public function get_sql( $wp_query ) {
$sql = $this->get_sql_clauses( $wp_query->query_vars['distinct'], $wp_query->posts_request_select );
return array(
'where' => $sql['where'],
'join' => $sql['join'],
);
}
get_sql()
方法调用了 get_sql_clauses()
来生成 SQL 语句的 WHERE
和 JOIN
部分。
get_sql_clauses()
方法是真正的核心。它会遍历你定义的每一个分类法查询条件,并根据 operator
的不同,生成不同的 SQL 子句。
public function get_sql_clauses( $distinct, $select = '' ) {
global $wpdb;
$sql = array(
'where' => 'AND 1=1',
'join' => '',
);
$tax_query = $this->queries;
if ( ! empty( $tax_query ) && is_array( $tax_query ) ) {
$sql_chunks = $this->get_sql_for_query( $tax_query );
$sql['where'] .= ' ' . $sql_chunks['where'];
$sql['join'] .= ' ' . $sql_chunks['join'];
}
if ( is_array( $this->relation ) && count( $this->relation ) > 0 ) {
$sql['where'] = preg_replace( '/AND 1=1/', '', $sql['where'], 1 );
$sql['where'] = '(' . trim( $sql['where'] ) . ')';
}
return $sql;
}
这个方法会递归地处理嵌套的查询条件。 如果查询中包含 relation
属性 (AND 或 OR), 那么会处理多个分类法查询之间的关系。 get_sql_for_query
方法是用来生成单个分类法查询的SQL代码的。
让我们深入 get_sql_for_query()
看看它是如何工作的:
protected function get_sql_for_query( $query ) {
global $wpdb;
$sql_chunks = array(
'where' => '',
'join' => '',
);
$sql_relation = 'AND';
if ( isset( $query['relation'] ) && in_array( strtoupper( $query['relation'] ), array( 'AND', 'OR' ), true ) ) {
$sql_relation = $query['relation'];
}
$sql_clauses = array();
foreach ( $query as $key => $clause ) {
if ( 'relation' === $key ) {
continue;
}
if ( is_array( $clause ) ) {
$clause_sql = $this->get_sql_for_clause( $clause, $sql_relation );
if ( ! empty( $clause_sql ) ) {
$sql_clauses[] = $clause_sql;
}
}
}
if ( ! empty( $sql_clauses ) ) {
$sql_chunks['where'] = 'AND ( ' . implode( " {$sql_relation} ", $sql_clauses ) . ' )';
}
return $sql_chunks;
}
这个方法遍历查询数组中的每个条件,并调用 get_sql_for_clause()
来获取每个条件的 SQL 代码。然后,它使用 relation
属性(AND 或 OR)将这些 SQL 代码连接起来。
最后,我们来看看 get_sql_for_clause()
:
protected function get_sql_for_clause( $clause, $op ) {
global $wpdb;
$sql = '';
if ( ! isset( $clause['taxonomy'] ) || ! taxonomy_exists( $clause['taxonomy'] ) ) {
return $sql;
}
$taxonomy = $clause['taxonomy'];
$tax_obj = get_taxonomy( $taxonomy );
$table = _wp_term_relevance_prefixed_table_name( 'term_relationships' );
$field = isset( $clause['field'] ) ? $clause['field'] : 'term_id';
$terms = isset( $clause['terms'] ) ? $clause['terms'] : array();
$operator = isset( $clause['operator'] ) ? strtoupper( $clause['operator'] ) : 'IN';
if ( ! is_array( $terms ) ) {
$terms = array( $terms );
}
if ( empty( $terms ) && ! in_array( $operator, array( 'EXISTS', 'NOT EXISTS' ), true ) ) {
return $sql;
}
$terms = array_map( 'sanitize_term_field', array_fill( 0, count( $terms ), $field ), $terms, array_fill( 0, count( $terms ), $taxonomy ), 'db' );
$sql_where = '';
$sql_join = '';
switch ( $field ) {
case 'term_id':
$term_column = 'term_id';
break;
case 'name':
$term_column = 'name';
break;
case 'slug':
$term_column = 'slug';
break;
case 'term_taxonomy_id':
$term_column = 'term_taxonomy_id';
break;
default:
return $sql;
}
$tt_id_column = 'term_taxonomy_id';
switch ( $operator ) {
case 'IN':
$ids = implode( ',', array_map( 'intval', $terms ) );
$sql_where = "tt.term_id IN ($ids)";
break;
case 'NOT IN':
$ids = implode( ',', array_map( 'intval', $terms ) );
$sql_where = "tt.term_id NOT IN ($ids)";
break;
case 'AND':
$clauses = array();
foreach ( $terms as $term ) {
$term = intval( $term );
$clauses[] = "FIND_IN_SET( {$term}, t.term_id )";
}
$sql_where = implode( ' AND ', $clauses );
break;
case 'EXISTS':
$sql_where = "1=1"; // Always true, just ensures the join happens
break;
case 'NOT EXISTS':
$sql_where = "1=1"; // Always true, just ensures the NOT EXISTS clause
break;
default:
return $sql;
}
$sql_join = " INNER JOIN {$wpdb->term_taxonomy} AS tt ON {$table}.term_taxonomy_id = tt.term_taxonomy_id INNER JOIN {$wpdb->terms} AS t ON tt.term_id = t.term_id";
if ( 'NOT EXISTS' === $operator ) {
$sql = "AND NOT EXISTS ( SELECT 1 FROM {$wpdb->term_taxonomy} AS tt INNER JOIN {$wpdb->terms} AS t ON tt.term_id = t.term_id WHERE tt.taxonomy = '$taxonomy' AND {$table}.term_taxonomy_id = tt.term_taxonomy_id )";
} else {
$sql = "AND EXISTS ( SELECT 1 FROM {$wpdb->term_taxonomy} AS tt INNER JOIN {$wpdb->terms} AS t ON tt.term_id = t.term_id WHERE tt.taxonomy = '$taxonomy' AND {$table}.term_taxonomy_id = tt.term_taxonomy_id AND {$sql_where} )";
}
return array( 'where' => $sql, 'join' => $sql_join );
}
这个方法做了以下事情:
- 验证分类法是否存在: 确保你指定的分类法是有效的。
- 确定要使用的表和字段: 根据
taxonomy
和field
的值,确定要查询的数据库表和字段。 - 构建 SQL 子句: 根据
operator
的值,构建不同的 SQLWHERE
子句。 例如,如果operator
是'IN'
,它会生成tt.term_id IN (1, 2, 3)
这样的 SQL 子句。 - 构建 JOIN 子句: 生成必要的
JOIN
子句,将文章表和分类法表连接起来。
一个更复杂的例子
让我们回到美食博客的例子,构建一个更复杂的查询:
$args = array(
'post_type' => 'recipe',
'tax_query' => array(
'relation' => 'AND',
array(
'taxonomy' => 'cuisine',
'field' => 'slug',
'terms' => array( 'sichuan' ),
'operator' => 'IN',
),
array(
'relation' => 'OR',
array(
'taxonomy' => 'ingredient',
'field' => 'slug',
'terms' => array( 'pork' ),
'operator' => 'IN',
),
array(
'taxonomy' => 'cooking_method',
'field' => 'slug',
'terms' => array( 'fried' ),
'operator' => 'IN',
),
),
),
);
这个查询表示:查找所有属于“川菜”,并且使用了“猪肉” 或者 烹饪方式是“炸”的文章。
总结
WP_Tax_Query
是一个强大的工具,可以让你构建复杂的分类法查询。理解它的基本结构和 operator
的用法,可以让你在 WordPress 中灵活地筛选文章。 记住,WP_Tax_Query
的核心是 get_sql()
方法,它负责将你的查询条件转化成 SQL 语句。 深入理解 get_sql()
方法的实现,可以让你更好地掌握 WP_Tax_Query
的工作原理。
希望今天的讲解对你有所帮助! 以后遇到复杂的分类法查询,不要害怕,拿起 WP_Tax_Query
这把瑞士军刀,勇敢地去战斗吧!