各位观众老爷,大家好!今天咱们来聊聊 WordPress 里一个相当重要,但又经常被人忽视的小伙伴—— WP_Tax_Query
。 别看它名字里带着“Tax”,可它不是税务局的,而是专门负责处理分类法查询的。 咱们要把它扒个底朝天,看看它到底是怎么工作的,又是怎么跟 WP_Query
勾搭上的。
一、WP_Query
的分类法查询痛点
先说说 WP_Query
。 WP_Query
是 WordPress 里查询文章的核心类,几乎所有文章列表的展示,都离不开它。 它很强大,可以根据各种条件查询文章,比如关键词、作者、日期等等。 但是,如果要根据分类法(比如分类、标签)来查询文章,事情就变得稍微复杂了。
WP_Query
本身提供了 category_name
、tag
等参数,可以简单地根据分类名或标签名来查询。 但是,如果需要更复杂的分类法查询,比如:
- 查询同时属于 A 分类和 B 标签的文章。
- 查询属于 A 分类,但不属于 B 标签的文章。
- 查询属于多个分类中的任意一个的文章。
这些情况,WP_Query
自带的参数就有点力不从心了。 这时候,就需要 tax_query
这个参数来帮忙了。 tax_query
接收的就是一个 WP_Tax_Query
对象(或者一个数组,WordPress 会自动帮你转换成 WP_Tax_Query
对象)。
二、WP_Tax_Query
闪亮登场
WP_Tax_Query
类的主要作用就是构建复杂的分类法查询条件。 它可以将各种分类法查询条件组合起来,形成一个完整的 SQL 查询语句片段,然后嵌入到 WP_Query
的主查询中。
来看一下 WP_Tax_Query
的基本结构:
<?php
/**
* Core class used for querying taxonomy relationships.
*
* @since 3.1.0
*
* @see WP_Query::parse_query()
*/
class WP_Tax_Query {
/**
* Array of taxonomy queries.
*
* @since 3.1.0
* @var array
*/
public $queries = array();
/**
* Relation between the taxonomy queries.
*
* Possible values are 'AND', 'OR'.
*
* @since 3.1.0
* @var string
*/
public $relation = 'AND';
/**
* Taxonomy table.
*
* @since 3.1.0
* @access public
* @var string
*/
public $taxonomy_table;
/**
* Terms table.
*
* @since 3.1.0
* @access public
* @var string
*/
public $terms_table;
/**
* Term taxonomy table.
*
* @since 3.1.0
* @access public
* @var string
*/
public $term_taxonomy_table;
/**
* Hashes the value of the query, which makes it possible to identify
* duplicate queries.
*
* @since 3.7.0
* @access public
* @var string
*/
public $query_vars_hash;
/**
* Hashes the SQL that is produced by the query. Used for testing.
*
* @since 3.7.0
* @access public
* @var string
*/
public $sql_clauses_hash;
/**
* List of tables used in the query.
*
* @since 3.1.0
* @access public
* @var array
*/
public $table_aliases = array();
/**
* Constructor.
*
* @since 3.1.0
*
* @param array $tax_query {
* Array of taxonomy query clauses.
*
* @type string $relation Optional. The logical operation to perform. 'AND' or 'OR'.
* Default 'AND'.
* @type array $0 Taxonomy query clause.
* @type array ... More taxonomy query clauses.
* }
*/
public function __construct( $tax_query = array() ) {
if ( isset( $tax_query['relation'] ) && in_array( strtoupper( $tax_query['relation'] ), array( 'AND', 'OR' ), true ) ) {
$this->relation = strtoupper( $tax_query['relation'] );
}
if ( isset( $tax_query[0] ) && is_array( $tax_query[0] ) ) {
$this->queries = $tax_query;
} else {
$this->queries = array( $tax_query );
}
}
/**
* Parses a single taxonomy query array.
*
* @since 3.1.0
* @access public
*
* @param array $query Taxonomy query clause.
* @return array Fully parsed taxonomy query clause, or false on error.
*/
public function sanitize_query( $query ) {
$query = wp_parse_args( $query, array(
'taxonomy' => '',
'terms' => array(),
'field' => 'term_id',
'operator' => 'IN',
'include_children' => true,
) );
if ( empty( $query['taxonomy'] ) ) {
return false;
}
$taxonomy = get_taxonomy( $query['taxonomy'] );
if ( ! $taxonomy ) {
return false;
}
$query['taxonomy'] = $taxonomy->name;
$query['field'] = sanitize_key( $query['field'] );
$query['operator'] = strtoupper( $query['operator'] );
if ( ! is_array( $query['terms'] ) ) {
$query['terms'] = array( $query['terms'] );
}
if ( 'slug' === $query['field'] ) {
$query['terms'] = array_map( 'sanitize_title', $query['terms'] );
} elseif ( 'name' === $query['field'] ) {
$query['terms'] = array_map( 'sanitize_text_field', $query['terms'] );
} else {
$query['terms'] = array_map( 'intval', $query['terms'] );
}
if ( ! in_array( $query['operator'], array( 'IN', 'NOT IN', 'AND', 'EXISTS', 'NOT EXISTS', 'BETWEEN', 'NOT BETWEEN' ), true ) ) {
$query['operator'] = 'IN';
}
return $query;
}
/**
* Constructs the taxonomy query SQL.
*
* @since 3.1.0
* @access public
*
* @global wpdb $wpdb WordPress database abstraction object.
*
* @param string $primary_table Database table to query.
* @param string $primary_id_column ID column for the primary table.
* @return array SQL query clauses.
*/
public function get_sql( $primary_table, $primary_id_column ) {
global $wpdb;
$sql_chunks = $this->get_sql_clauses( $primary_table, $primary_id_column );
/**
* Filters the taxonomy query SQL clauses.
*
* @since 3.2.0
*
* @param array $sql_chunks Array containing the JOIN and WHERE SQL clauses.
* @param string $primary_table Database table to query.
* @param string $primary_id_column ID column for the primary table.
* @param WP_Tax_Query $this The WP_Tax_Query instance.
*/
return apply_filters( 'get_tax_sql', $sql_chunks, $primary_table, $primary_id_column, $this );
}
/**
* Generates SQL clauses to be appended to a main query.
*
* @since 3.1.0
* @access public
*
* @global wpdb $wpdb WordPress database abstraction object.
*
* @param string $primary_table Database table to query.
* @param string $primary_id_column ID column for the primary table.
* @return array {
* Array containing the JOIN and WHERE SQL clauses.
*
* @type string $join SQL fragment for the JOIN clause.
* @type string $where SQL fragment for the WHERE clause.
* }
*/
public function get_sql_clauses( $primary_table, $primary_id_column ) {
global $wpdb;
$this->taxonomy_table = _wp_get_taxonomy_table();
$this->terms_table = $wpdb->terms;
$this->term_taxonomy_table = $wpdb->term_taxonomy;
$sql = array(
'join' => array(),
'where' => array(),
);
$sql['where'][] = '1=1';
if ( empty( $this->queries ) ) {
return $sql;
}
$num_queries = count( $this->queries );
// If there is only one query, use the un-prefixed alias.
if ( 1 === $num_queries ) {
$sql_query = $this->get_sql_for_query( $this->queries[0], $primary_table, $primary_id_column );
if ( ! empty( $sql_query['where'] ) ) {
$sql['join'][] = $sql_query['join'];
$sql['where'][] = $sql_query['where'];
}
return $sql;
}
$sql['where'][] = '(';
$rels = array( 'AND', 'OR' );
$sql_chunks = array();
for ( $i = 0; $i < $num_queries; $i++ ) {
$sql_query = $this->get_sql_for_query( $this->queries[ $i ], $primary_table, $primary_id_column );
if ( ! empty( $sql_query['where'] ) ) {
$sql['join'][] = $sql_query['join'];
$sql_chunks[] = $sql_query['where'];
}
if ( $i < $num_queries - 1 ) {
if ( in_array( $this->relation, $rels, true ) ) {
$sql_chunks[] = $this->relation;
} else {
$sql_chunks[] = 'AND';
}
}
}
$sql['where'][] = implode( ' ', $sql_chunks );
$sql['where'][] = ')';
return $sql;
}
/**
* Generates SQL for a single taxonomy query.
*
* @since 3.1.0
* @access protected
*
* @global wpdb $wpdb WordPress database abstraction object.
*
* @param array $query A taxonomy query clause.
* @param string $primary_table Database table to query.
* @param string $primary_id_column ID column for the primary table.
* @return array {
* Array containing the JOIN and WHERE SQL clauses.
*
* @type string $join SQL fragment for the JOIN clause.
* @type string $where SQL fragment for the WHERE clause.
* }
*/
protected function get_sql_for_query( $query, $primary_table, $primary_id_column ) {
global $wpdb;
$sql = array(
'join' => '',
'where' => '',
);
$query = $this->sanitize_query( $query );
if ( ! $query ) {
return $sql;
}
$taxonomy = get_taxonomy( $query['taxonomy'] );
if ( ! $taxonomy || is_wp_error( $taxonomy ) ) {
return $sql;
}
$terms = $query['terms'];
$field = $query['field'];
$operator = $query['operator'];
if ( empty( $terms ) ) {
return $sql;
}
$terms = array_map( 'strval', $terms );
$this->has_terms = true;
$table_alias = 'tt';
$i = 1;
while ( isset( $this->table_aliases[ $table_alias ] ) ) {
$i++;
$table_alias = 'tt' . $i;
}
$this->table_aliases[ $table_alias ] = true;
$sql['join'] = " INNER JOIN {$this->term_taxonomy_table} AS {$table_alias} ON ( {$primary_table}.{$primary_id_column} = {$table_alias}.object_id )";
if ( 'term_id' === $field ) {
$term_ids = array_map( 'intval', $terms );
$where = wp_sprintf( '%s.term_id %s (%l)', $table_alias, $operator, $term_ids );
} elseif ( 'term_taxonomy_id' === $field ) {
$term_taxonomy_ids = array_map( 'intval', $terms );
$where = wp_sprintf( '%s.term_taxonomy_id %s (%l)', $table_alias, $operator, $term_taxonomy_ids );
} elseif ( 'slug' === $field ) {
$slugs = array_map( array( $wpdb, 'esc_like' ), $terms );
$where = wp_sprintf( '%s.slug %s (%s)', $this->terms_table, $operator, implode( ',', array_map( array( $wpdb, 'prepare' ), array_fill( 0, count( $slugs ), '%s' ), $slugs ) ) );
} elseif ( 'name' === $field ) {
$names = array_map( array( $wpdb, 'esc_like' ), $terms );
$where = wp_sprintf( '%s.name %s (%s)', $this->terms_table, $operator, implode( ',', array_map( array( $wpdb, 'prepare' ), array_fill( 0, count( $names ), '%s' ), $names ) ) );
} else {
return $sql;
}
$sql['join'] = apply_filters( 'get_tax_sql_join', $sql['join'], $query, $table_alias, $primary_table, $primary_id_column );
$sql['where'] = apply_filters( 'get_tax_sql_where', $where, $query, $table_alias, $primary_table, $primary_id_column );
return array(
'join' => $sql['join'],
'where' => $sql['where'],
);
}
/**
* Checks whether the current query has terms.
*
* @since 4.7.0
* @access public
*
* @return bool True if query has terms, false otherwise.
*/
public function has_terms() {
return ! empty( $this->has_terms );
}
/**
* Sets the value of query vars, which make it possible to identify
* duplicate queries.
*
* @since 3.7.0
* @access public
*
* @param array $query_vars The array of query variables.
*/
public function set_query_vars( $query_vars ) {
$this->query_vars_hash = md5( serialize( $query_vars ) );
}
/**
* Sets the value of the query's SQL clauses, which makes it possible to
* identify duplicate queries.
*
* @since 3.7.0
* @access public
*
* @param string $sql_clauses The query's complete SQL clauses.
*/
public function set_sql_clauses( $sql_clauses ) {
$this->sql_clauses_hash = md5( serialize( $sql_clauses ) );
}
/**
* Parses a taxonomy query.
*
* @since 4.1.0
* @access public
*
* @param array $q An array of taxonomy query parameters.
* @return array
*/
public function parse_query_vars( $q ) {
$this->queries = $this->transform_query( $q );
return $this->queries;
}
/**
* Transforms old-style taxonomy queries to the format introduced in WP 3.1.
*
* @since 4.1.0
* @access public
*
* @param array $tax_query An array of taxonomy query parameters.
* @return array
*/
public function transform_query( $tax_query ) {
$relation = 'AND';
if ( isset( $tax_query['relation'] ) ) {
$relation = strtoupper( $tax_query['relation'] );
if ( ! in_array( $relation, array( 'AND', 'OR' ), true ) ) {
$relation = 'AND';
}
}
$cleaned_query = array(
'relation' => $relation,
);
foreach ( $tax_query as $key => $clause ) {
if ( 'relation' === $key ) {
continue;
}
if ( ! is_array( $clause ) ) {
continue;
}
// 'taxonomy' is required, but may be absent for backward compatibility.
if ( ! isset( $clause['taxonomy'] ) ) {
continue;
}
$cleaned_query[] = $clause;
}
return $cleaned_query;
}
}
可以看到,WP_Tax_Query
类主要包含以下几个部分:
$queries
: 一个数组,存储了多个分类法查询条件。 每个查询条件都是一个数组,包含taxonomy
(分类法名称)、terms
(术语 ID 或 slug)、field
(字段,比如term_id
、slug
、name
)、operator
(操作符,比如IN
、NOT IN
、AND
)等信息。$relation
: 一个字符串,表示多个查询条件之间的关系。 可以是'AND'
(所有条件都必须满足) 或'OR'
(满足任意一个条件即可)。__construct()
: 构造函数,接收一个数组作为参数,用于初始化$queries
和$relation
。get_sql()
: 核心方法,负责生成 SQL 查询语句片段。 它会遍历$queries
数组,将每个查询条件转换为 SQL 片段,然后根据$relation
将这些片段组合起来。get_sql_clauses()
:get_sql()
的辅助方法,负责将 SQL 片段分为join
和where
两部分。join
部分负责连接相关的数据库表,where
部分负责添加查询条件。get_sql_for_query()
:get_sql_clauses()
的辅助方法,负责将单个查询条件转换为 SQL 片段。
三、WP_Tax_Query
的使用方法
说了这么多,还是得看例子才能理解。 假设我们要查询属于 category
分类下的 news
和 featured
这两个 slug 的文章,并且属于 post_tag
标签下的 popular
这个 slug 的文章。
<?php
$args = array(
'post_type' => 'post',
'tax_query' => array(
'relation' => 'AND', // 多个条件之间的关系是 AND
array(
'taxonomy' => 'category', // 分类法名称
'field' => 'slug', // 字段,这里使用 slug
'terms' => array( 'news', 'featured' ), // 术语 slug
),
array(
'taxonomy' => 'post_tag', // 标签
'field' => 'slug', // 字段,这里使用 slug
'terms' => array( 'popular' ), // 术语 slug
),
),
);
$query = new WP_Query( $args );
if ( $query->have_posts() ) {
while ( $query->have_posts() ) {
$query->the_post();
echo '<p>' . get_the_title() . '</p>';
}
wp_reset_postdata();
} else {
echo '<p>No posts found</p>';
}
?>
在这个例子中,我们创建了一个 WP_Query
对象,并将 tax_query
参数设置为一个数组。 这个数组包含了两个分类法查询条件,它们之间的关系是 AND
。
- 第一个查询条件: 查询属于
category
分类下的news
和featured
这两个 slug 的文章。 - 第二个查询条件: 查询属于
post_tag
标签下的popular
这个 slug 的文章。
只有同时满足这两个查询条件,文章才会被查询出来。
再来一个例子,这次我们使用 OR
关系。 假设我们要查询属于 category
分类下的 news
或 featured
这两个 slug 的文章。
<?php
$args = array(
'post_type' => 'post',
'tax_query' => array(
'relation' => 'OR', // 多个条件之间的关系是 OR
array(
'taxonomy' => 'category', // 分类法名称
'field' => 'slug', // 字段,这里使用 slug
'terms' => array( 'news', 'featured' ), // 术语 slug
),
),
);
$query = new WP_Query( $args );
if ( $query->have_posts() ) {
while ( $query->have_posts() ) {
$query->the_post();
echo '<p>' . get_the_title() . '</p>';
}
wp_reset_postdata();
} else {
echo '<p>No posts found</p>';
}
?>
在这个例子中,只要文章属于 category
分类下的 news
或 featured
这两个 slug 中的任意一个,就会被查询出来。
四、WP_Tax_Query
的参数详解
WP_Tax_Query
的每个查询条件,都包含以下几个参数:
参数名 | 类型 | 描述 |
---|---|---|
taxonomy |
string | 分类法名称。 必须是已经注册的分类法,比如 category 、post_tag 、自定义分类法等。 |
terms |
array | 术语。 可以是术语 ID、slug 或 name,具体取决于 field 参数的设置。 |
field |
string | 字段。 用于匹配 terms 参数的值。 可以是 term_id (术语 ID)、slug (术语 slug)、name (术语 name) 或 term_taxonomy_id (term taxonomy ID)。 默认为 term_id 。 |
operator |
string | 操作符。 用于指定 terms 参数和数据库中的术语之间的关系。 可以是以下值: |
* IN : 文章必须属于 terms 参数中的任意一个术语。 |
||
* NOT IN : 文章不能属于 terms 参数中的任意一个术语。 |
||
* AND : 文章必须同时属于 terms 参数中的所有术语。 注意:只有在 taxonomy 支持多个术语分配时,这个操作符才有效。 比如分类和标签都支持,但是自定义分类法可能不支持。 |
||
* EXISTS : 文章必须属于指定的分类法。 terms 参数会被忽略。 |
||
* NOT EXISTS : 文章不能属于指定的分类法。 terms 参数会被忽略。 |
||
* BETWEEN : 文章的 term_id 必须在 terms 参数指定的两个值之间。 必须是整数。 |
||
* NOT BETWEEN : 文章的 term_id 不在 terms 参数指定的两个值之间。 必须是整数。 |
||
include_children |
boolean | 是否包含子术语。 默认为 true 。 如果设置为 false ,则只会查询属于指定术语的文章,不会查询属于其子术语的文章。这个参数只对层级分类法(比如分类)有效,对非层级分类法(比如标签)无效。 |
五、WP_Tax_Query
与数据库表的关系
WP_Tax_Query
在生成 SQL 查询语句时,主要涉及到以下三个数据库表:
wp_terms
: 存储术语的信息,比如术语 ID、slug、name 等。wp_term_taxonomy
: 存储术语与分类法之间的关系,比如术语属于哪个分类法,以及术语的描述、父级术语等。wp_term_relationships
: 存储文章与术语之间的关系,比如文章属于哪个术语。
WP_Tax_Query
会根据查询条件,连接这些表,然后筛选出符合条件的文章。
六、 WP_Tax_Query
的 SQL 生成过程
我们来简化一下 SQL 生成的流程。
- 初始化: 接收
tax_query
参数(一个数组)并转换为WP_Tax_Query
对象。 - 循环处理每个查询条件: 遍历
$queries
数组,对每个查询条件调用get_sql_for_query()
方法。 - 构建单个查询条件的 SQL: 在
get_sql_for_query()
方法中,根据taxonomy
、field
、terms
和operator
参数,生成 SQL 片段。 - 连接数据库表: 根据
taxonomy
参数,确定需要连接的数据库表。 通常需要连接wp_term_relationships
表和wp_term_taxonomy
表。 如果field
参数是slug
或name
,还需要连接wp_terms
表。 - 添加 WHERE 子句: 根据
terms
和operator
参数,生成 WHERE 子句。 比如,如果operator
是IN
,则生成WHERE term_id IN (1, 2, 3)
这样的子句。 - 组合 SQL 片段: 将生成的 SQL 片段添加到
join
和where
数组中。 - 处理多个查询条件: 如果
$queries
数组中包含多个查询条件,则根据$relation
参数,将这些查询条件组合起来。 如果$relation
是AND
,则使用AND
连接这些条件;如果$relation
是OR
,则使用OR
连接这些条件。 - 返回 SQL: 将最终的
join
和where
数组返回给WP_Query
。
七、WP_Tax_Query
的高级用法
WP_Tax_Query
除了可以进行简单的分类法查询外,还可以进行一些更高级的操作。 比如:
- 嵌套查询: 可以在
tax_query
中嵌套tax_query
,实现更复杂的查询逻辑。 - 使用
EXISTS
和NOT EXISTS
操作符: 可以查询属于或不属于某个分类法的文章。 - 自定义 SQL: 可以使用
get_tax_sql_join
和get_tax_sql_where
过滤器,自定义WP_Tax_Query
生成的 SQL。
八、WP_Tax_Query
的注意事项
WP_Tax_Query
的性能可能会受到查询条件的复杂程度的影响。 尽量避免使用过于复杂的查询条件,以提高查询效率。WP_Tax_Query
的参数必须正确设置,否则可能会导致查询结果不正确。 仔细检查每个参数的值,确保它们符合预期。WP_Tax_Query
与其他查询参数可能会产生冲突。 例如,如果同时使用了category_name
和tax_query
参数,可能会导致查询结果不符合预期。 在使用WP_Tax_Query
时,最好只使用tax_query
参数,避免与其他参数产生冲突。
九、总结
WP_Tax_Query
是 WordPress 中一个非常强大的分类法查询工具。 它可以帮助我们构建复杂的分类法查询条件,实现各种各样的文章列表展示需求。 掌握 WP_Tax_Query
的使用方法,可以让我们更灵活地控制 WordPress 的文章查询。
好了,今天的 WP_Tax_Query
源码剖析就到这里。 希望大家能够有所收获,下次再见!