大家好,我是老码农李狗蛋,今天咱们来聊聊 WordPress 里 WP_Query
的 meta_query
参数,这玩意儿看着简单,水可是深的很呐! 咱们的目标是:彻底搞懂它怎么把一个 PHP 数组,变成 SQL 里复杂的 JOIN
和 WHERE
子句,让它彻底在咱们面前裸奔!
一、meta_query
是个啥?
简单来说,meta_query
允许你根据文章的自定义字段(meta fields)来查询文章。 比如,你想找所有颜色是“蓝色”,价格大于 100 的商品,就可以用 meta_query
来实现。
meta_query
的基本结构是一个数组,数组里可以包含多个子数组,每个子数组代表一个查询条件。每个子数组又包含 key
(字段名)、value
(字段值)、compare
(比较操作符)等参数。
例如:
$args = array(
'post_type' => 'product',
'meta_query' => array(
'relation' => 'AND', // 多个条件之间的关系,可选 'AND' 或 'OR'
array(
'key' => 'color',
'value' => 'blue',
'compare' => '='
),
array(
'key' => 'price',
'value' => 100,
'compare' => '>='
)
)
);
$query = new WP_Query( $args );
这段代码的意思是:查找所有文章类型为 product
,且 color
字段等于 blue
,price
字段大于等于 100 的文章。
二、WP_Query
源码里 meta_query
的处理流程
WP_Query
在处理 meta_query
的时候,主要涉及以下几个关键函数:
-
WP_Query::get_posts()
:这是WP_Query
的核心函数,负责获取文章。它会调用其他函数来处理各种查询参数,包括meta_query
。 -
WP_Query::parse_query()
:这个函数负责解析查询参数,并将它们存储在WP_Query
对象的属性中。 它会把传进来的meta_query
参数处理成规范化的形式。 -
WP_Query::get_meta_sql()
:这个函数是今天的主角!它负责根据meta_query
生成 SQL 的JOIN
和WHERE
子句。
咱们重点分析 WP_Query::get_meta_sql()
这个函数。
三、WP_Query::get_meta_sql()
源码剖析
WP_Query::get_meta_sql()
函数的源码比较复杂,咱们把它拆解成几个部分来分析:
/**
* Generate SQL for meta query clauses.
*
* @since 3.1.0
*
* @param array $meta_query Array of meta query clauses.
* @param string $primary_table Primary table.
* @param string $primary_id_column Primary column.
* @return array Array containing the 'join' and 'where' sub-clauses.
*/
public function get_meta_sql( $meta_query, $primary_table, $primary_id_column ) {
global $wpdb;
$meta_query = new WP_Meta_Query( $meta_query );
$sql = $meta_query->get_sql( $primary_table, $primary_id_column, $this->query['suppress_filters'] );
return array(
'join' => $sql['join'],
'where' => $sql['where'],
);
}
看起来代码很短,实际上它把主要工作都委托给了 WP_Meta_Query
类。 咱们继续深入 WP_Meta_Query
。
四、WP_Meta_Query
类的运作
WP_Meta_Query
类负责解析和构建 meta query 的 SQL。 它的核心方法是 get_sql()
。
/**
* Get the fully-formed SQL JOIN and WHERE clauses for the query.
*
* @since 3.2.0
*
* @param string $primary_table The table being joined onto.
* @param string $primary_id_column The column to join on.
* @param bool $suppress Whether to suppress filters.
* @return array An associative array of SQL JOIN and WHERE clauses.
*/
public function get_sql( $primary_table, $primary_id_column, $suppress = false ) {
$sql = $this->get_sql_clauses();
if ( ! empty( $sql['where'] ) ) {
$sql['where'] = 'AND ' . $sql['where'];
}
if ( $suppress ) {
return $sql;
}
/**
* Filters the fully-formed SQL JOIN clause for the query.
*
* @since 3.2.0
*
* @param string $join SQL JOIN clause.
* @param array $query An array representing the meta query.
*/
$sql['join'] = apply_filters( 'get_meta_sql_join', $sql['join'], $this->queries );
/**
* Filters the fully-formed SQL WHERE clause for the query.
*
* @since 3.2.0
*
* @param string $where SQL WHERE clause.
* @param array $query An array representing the meta query.
*/
$sql['where'] = apply_filters( 'get_meta_sql_where', $sql['where'], $this->queries );
return $sql;
}
get_sql()
又调用了 get_sql_clauses()
来生成 SQL。 咱们继续深入!
五、WP_Meta_Query::get_sql_clauses()
源码分析
这个函数是核心中的核心,它负责遍历 meta_query
数组,并根据每个条件生成对应的 SQL 子句。
/**
* Get the SQL clauses for the query.
*
* @since 4.2.0
* @return array An associative array of SQL clauses.
*/
public function get_sql_clauses() {
global $wpdb;
$sql = array(
'where' => '1=1',
'join' => '',
);
$this->tables = array();
$this->clauses = array();
if ( empty( $this->queries ) ) {
return $sql;
}
$sql_chunks = $this->get_sql_for_query( $this->queries );
if ( is_wp_error( $sql_chunks ) ) {
return $sql_chunks;
}
$sql['where'] = implode( ' ', array_filter( $sql_chunks['where'] ) );
$sql['join'] = implode( ' ', array_unique( $sql_chunks['join'] ) );
return $sql;
}
可以看到,这个函数初始化了一个 $sql
数组,包含了 where
和 join
两个子元素。然后,它调用 get_sql_for_query()
来处理实际的查询条件。
六、WP_Meta_Query::get_sql_for_query()
源码分析
这个函数递归地处理 meta_query
数组,并根据每个条件生成 SQL。
/**
* Generate SQL clauses for a single query array.
*
* @since 4.2.0
*
* @param array $query Query array.
* @param int $depth Optional. The level of the query within the nested array.
* Used to generate unique table aliases. Default 0.
* @return array|WP_Error An associative array containing the 'join' and 'where'
* SQL clauses, or a WP_Error object if there is a problem.
*/
protected function get_sql_for_query( $query, $depth = 0 ) {
global $wpdb;
$sql = array(
'where' => array(),
'join' => array(),
);
$table_name = $wpdb->postmeta;
$alias = 'mt' . absint( $depth );
foreach ( $query as $key => $clause ) {
if ( 'relation' === $key ) {
$relation = strtoupper( trim( $clause ) );
if ( ! in_array( $relation, array( 'AND', 'OR' ), true ) ) {
$relation = 'AND';
}
$sql['where'][] = $relation;
continue;
}
if ( isset( $clause['relation'] ) ) {
$recursion = $this->get_sql_for_query( $clause, $depth + 1 );
if ( is_wp_error( $recursion ) ) {
return $recursion;
}
if ( ! empty( $recursion['where'] ) ) {
$sql['where'][] = '(' . implode( ' ', array_filter( $recursion['where'] ) ) . ')';
}
$sql['join'] = array_merge( $sql['join'], $recursion['join'] );
continue;
}
// Stop if not a valid clause.
if ( ! is_array( $clause ) || empty( $clause['key'] ) ) {
continue;
}
$clause['key'] = trim( $clause['key'] );
$clause['compare'] = isset( $clause['compare'] ) ? strtoupper( trim( $clause['compare'] ) ) : '=';
$clause['value'] = isset( $clause['value'] ) ? $clause['value'] : '';
$clause['type'] = isset( $clause['type'] ) ? strtoupper( trim( $clause['type'] ) ) : 'CHAR';
// Cast value according to type.
switch ( $clause['type'] ) {
case 'NUMERIC':
case 'DECIMAL':
case 'SIGNED':
case 'UNSIGNED':
$clause['value'] = is_array( $clause['value'] ) ? array_map( 'floatval', $clause['value'] ) : floatval( $clause['value'] );
break;
case 'BINARY':
$clause['value'] = is_array( $clause['value'] ) ? array_map( 'esc_sql', $clause['value'] ) : esc_sql( $clause['value'] );
break;
default:
$clause['value'] = is_array( $clause['value'] ) ? array_map( 'strval', $clause['value'] ) : strval( $clause['value'] );
}
$field = "$alias.meta_value";
$join = "LEFT JOIN $table_name AS $alias ON ($wpdb->posts.ID = $alias.post_id)";
$this->tables[ $alias ] = true;
$sql['join'][] = $join;
$where = $this->get_meta_sql_clause( $clause, $alias, $table_name, $field );
if ( ! empty( $where ) ) {
$sql['where'][] = '(' . $where . ')';
}
}
return $sql;
}
这个函数做了很多事情,咱们一点点来看:
-
初始化 SQL 数组: 初始化
$sql
数组,包含where
和join
两个子元素。 -
遍历查询条件: 遍历
meta_query
数组中的每个查询条件。 -
处理
relation
: 如果遇到relation
键,表示多个条件之间的关系(AND
或OR
),将其添加到$sql['where']
数组中。 -
递归处理嵌套查询: 如果遇到嵌套的
meta_query
,递归调用get_sql_for_query()
处理。 -
处理单个查询条件: 对于每个单独的查询条件,执行以下操作:
-
参数规范化: 获取
key
、compare
、value
和type
等参数,并进行规范化处理(例如,将compare
转换为大写,将type
转换为大写)。 -
类型转换: 根据
type
参数,将value
转换为相应的数据类型。 -
构建
JOIN
子句: 生成LEFT JOIN
子句,将wp_posts
表和wp_postmeta
表连接起来。 这里会为每一个 meta query 生成一个唯一的表别名 (alias)mt0
,mt1
,mt2
… 避免表名冲突。 -
构建
WHERE
子句: 调用get_meta_sql_clause()
函数,根据key
、compare
和value
等参数,生成WHERE
子句。 -
添加到 SQL 数组: 将生成的
JOIN
和WHERE
子句添加到$sql
数组中。
-
-
返回 SQL 数组: 返回包含
join
和where
子句的$sql
数组。
七、WP_Meta_Query::get_meta_sql_clause()
源码分析
这个函数负责根据单个 meta query 条件生成具体的 SQL WHERE
子句。
/**
* Generate the proper SQL clause to filter a meta field.
*
* @since 5.1.0
*
* @param array $clause Meta query clause.
* @param string $alias Table alias.
* @param string $table Table name.
* @param string $field Meta field.
* @return string Filter clause.
*/
protected function get_meta_sql_clause( $clause, $alias, $table, $field ) {
global $wpdb;
$where = '';
$compare = $clause['compare'];
$key = $clause['key'];
$value = $clause['value'];
$type = $clause['type'];
switch ( $compare ) {
case 'IN':
case 'NOT IN':
if ( ! is_array( $value ) ) {
$value = preg_split( '/[,s]+/', $value );
}
if ( empty( $value ) ) {
$where = '1=0';
break;
}
$placeholders = array_fill( 0, count( $value ), '%s' );
$placeholders = implode( ', ', $placeholders );
$where = "$alias.meta_key = %s AND $field " . $compare . " (" . $placeholders . ")";
$where = $wpdb->prepare( $where, array_merge( array( $key ), $value ) );
break;
case 'BETWEEN':
case 'NOT BETWEEN':
if ( ! is_array( $value ) || 2 !== count( $value ) ) {
break;
}
$where = "$alias.meta_key = %s AND $field " . $compare . " %f AND %f";
$where = $wpdb->prepare( $where, $key, $value[0], $value[1] );
break;
case 'LIKE':
case 'NOT LIKE':
$value = '%' . $wpdb->esc_like( $value ) . '%';
$where = "$alias.meta_key = %s AND $field $compare %s";
$where = $wpdb->prepare( $where, $key, $value );
break;
case 'REGEXP':
case 'NOT REGEXP':
case 'RLIKE':
$where = "$alias.meta_key = %s AND $field $compare %s";
$where = $wpdb->prepare( $where, $key, $value );
break;
case '=':
case '!=':
case '>':
case '>=':
case '<':
case '<=':
$where = "$alias.meta_key = %s AND $field $compare %s";
$where = $wpdb->prepare( $where, $key, $value );
break;
case 'EXISTS':
$where = "$alias.meta_key = %s";
$where = $wpdb->prepare( $where, $key );
break;
case 'NOT EXISTS':
$where = "$alias.meta_key != %s"; // Different from EXISTS.
$where = $wpdb->prepare( $where, $key );
break;
default:
/**
* Filters the SQL clause for a meta field comparison.
*
* The dynamic portion of the hook name, `$compare`, refers to the
* comparison operator, such as '=', '!=', 'IN', etc.
*
* @since 5.1.0
*
* @param string $where The SQL clause.
* @param array $clause Meta query clause.
* @param string $alias Table alias.
* @param string $table Table name.
* @param string $field Meta field.
*/
$where = apply_filters( "get_meta_sql_{$compare}", '', $clause, $alias, $table, $field );
}
return $where;
}
这个函数根据不同的 compare
操作符,生成不同的 SQL WHERE
子句。 可以看到它支持了各种常见的比较操作符,例如 =
、!=
、>
、<
、IN
、NOT IN
、LIKE
、BETWEEN
等等。
八、 举个栗子:完整的 SQL 生成过程
咱们用最开始的例子,来模拟一下 SQL 的生成过程:
$args = array(
'post_type' => 'product',
'meta_query' => array(
'relation' => 'AND',
array(
'key' => 'color',
'value' => 'blue',
'compare' => '='
),
array(
'key' => 'price',
'value' => 100,
'compare' => '>='
)
)
);
$query = new WP_Query( $args );
-
WP_Query::get_posts()
调用WP_Query::get_meta_sql()
。 -
WP_Query::get_meta_sql()
创建WP_Meta_Query
对象,并调用WP_Meta_Query::get_sql()
。 -
WP_Meta_Query::get_sql()
调用WP_Meta_Query::get_sql_clauses()
。 -
WP_Meta_Query::get_sql_clauses()
调用WP_Meta_Query::get_sql_for_query()
。 -
WP_Meta_Query::get_sql_for_query()
遍历meta_query
数组:-
第一个条件:
color
=blue
- 生成表别名:
mt0
- 生成
JOIN
子句:LEFT JOIN wp_postmeta AS mt0 ON (wp_posts.ID = mt0.post_id)
- 调用
WP_Meta_Query::get_meta_sql_clause()
生成WHERE
子句:mt0.meta_key = 'color' AND mt0.meta_value = 'blue'
- 生成表别名:
-
第二个条件:
price
>=100
- 生成表别名:
mt1
- 生成
JOIN
子句:LEFT JOIN wp_postmeta AS mt1 ON (wp_posts.ID = mt1.post_id)
- 调用
WP_Meta_Query::get_meta_sql_clause()
生成WHERE
子句:mt1.meta_key = 'price' AND mt1.meta_value >= 100
- 生成表别名:
-
合并
JOIN
和WHERE
子句:JOIN
:LEFT JOIN wp_postmeta AS mt0 ON (wp_posts.ID = mt0.post_id) LEFT JOIN wp_postmeta AS mt1 ON (wp_posts.ID = mt1.post_id)
WHERE
:(mt0.meta_key = 'color' AND mt0.meta_value = 'blue') AND (mt1.meta_key = 'price' AND mt1.meta_value >= 100)
-
最终生成的 SQL (简化版) 大概是这样:
SELECT wp_posts.*
FROM wp_posts
LEFT JOIN wp_postmeta AS mt0 ON (wp_posts.ID = mt0.post_id)
LEFT JOIN wp_postmeta AS mt1 ON (wp_posts.ID = mt1.post_id)
WHERE 1=1
AND wp_posts.post_type = 'product'
AND (mt0.meta_key = 'color' AND mt0.meta_value = 'blue')
AND (mt1.meta_key = 'price' AND mt1.meta_value >= 100)
AND wp_posts.post_status = 'publish'
九、 性能优化小贴士
meta_query
功能强大,但用不好也会导致性能问题。 记住以下几点:
- 避免过度使用: 尽量减少
meta_query
的数量,复杂查询可以考虑其他方案。 - 索引优化: 确保
wp_postmeta
表的post_id
和meta_key
列有索引。 - 缓存: 使用对象缓存或 Redis 等缓存机制,缓存查询结果。
- 复杂查询: 对于非常复杂的查询,可以考虑编写自定义 SQL 查询,或者使用专门的搜索插件。
十、总结
今天咱们深入剖析了 WP_Query
的 meta_query
参数的源码实现,了解了它如何将 PHP 数组转换为 SQL 的 JOIN
和 WHERE
子句。 掌握了这些知识,你就可以更灵活地使用 meta_query
,并避免一些常见的性能问题。
希望今天的分享对大家有所帮助! 记住, 编程就像谈恋爱,只有深入了解,才能更好地驾驭它! 下次再见!