WordPress meta_query
参数的秘密花园:从数组到 SQL 的奇妙之旅
大家好,我是你们的老朋友,今天咱们来聊聊 WordPress 里一个既强大又有点让人摸不着头脑的东西:meta_query
。 咱们这次要做的,就是深入 WP_Query
的源码,看看这个小家伙是怎么把一个看起来人畜无害的数组,变成一段复杂的 SQL JOIN
和 WHERE
子句的。准备好了吗?让我们开始这场探险吧!
1. meta_query
是个啥?为啥我们要研究它?
首先,我们得明确一下 meta_query
是干嘛的。简单来说,它是 WP_Query
类中的一个参数,允许你根据文章的自定义字段(也就是 meta data)来筛选文章。这在很多场景下都非常有用,比如你想找到所有价格在 100 到 200 元之间的商品,或者找到所有作者喜欢吃苹果的文章。
但是!meta_query
的参数形式通常是一个嵌套很深的数组,长得像这样:
$args = array(
'post_type' => 'product',
'meta_query' => array(
'relation' => 'AND', // 可选,可以是 'AND' 或 'OR'
array(
'key' => 'price',
'value' => array(100, 200),
'compare' => 'BETWEEN',
'type' => 'NUMERIC'
),
array(
'key' => 'author_likes',
'value' => 'apple',
'compare' => 'LIKE'
)
)
);
$query = new WP_Query( $args );
这么个东西,要怎么变成数据库能理解的 SQL 呢?这就是我们要解开的谜团。
研究 meta_query
的实现,不仅能让你更好地理解 WP_Query
的工作原理,还能让你在遇到复杂的自定义字段查询需求时,不再抓瞎,而是能胸有成竹地写出高效的代码。
2. 走进 WP_Query
的源码:找到 meta_query
的入口
要找到 meta_query
的处理逻辑,我们首先要进入 WP_Query
类的构造函数 __construct()
。在这个函数里,会调用 parse_query()
方法来解析我们传入的参数。
打开 wp-includes/class-wp-query.php
文件,找到 parse_query()
方法。在这个方法里,你会看到类似这样的代码:
// Meta query.
if ( ! empty( $q['meta_query'] ) && is_array( $q['meta_query'] ) ) {
$this->meta_query = new WP_Meta_Query( $q['meta_query'] );
}
看到没?关键就在这里!如果我们在 WP_Query
的参数中传入了 meta_query
,WordPress 就会创建一个 WP_Meta_Query
类的实例,并将我们的 meta_query
数组传递给它。
3. WP_Meta_Query
:meta_query
的核心处理器
WP_Meta_Query
类是处理 meta_query
参数的核心。它的职责就是将我们传入的数组,转换成 SQL 的 JOIN
和 WHERE
子句。
打开 wp-includes/meta.php
文件,找到 WP_Meta_Query
类。让我们看看它的构造函数 __construct()
:
public function __construct( $meta_query = false ) {
if ( is_array( $meta_query ) && ! empty( $meta_query ) ) {
$this->parse_query_vars( $meta_query );
}
}
这个构造函数很简单,它调用了 parse_query_vars()
方法来解析 meta_query
数组。
4. parse_query_vars()
:解析数组,构建查询条件
parse_query_vars()
方法是 WP_Meta_Query
类中最关键的方法之一。它的作用是递归地解析 meta_query
数组,并将其中的每个查询条件存储在 queries
属性中。
public function parse_query_vars( &$q ) {
$defaults = array(
'relation' => 'AND',
);
$q = wp_parse_args( $q, $defaults );
$this->relation = strtoupper( $q['relation'] );
if ( ! is_array( $q ) ) {
return;
}
$primary = false;
foreach ( $q as $key => &$query ) {
if ( 'relation' === $key ) {
continue;
}
if ( is_array( $query ) ) {
// It's a nested query.
$this->queries[] = new WP_Meta_Query( $query );
} else {
// It's a single query.
$this->queries[] = $this->sanitize_query( $query, $key );
$primary = true;
}
}
if ( ! $primary ) {
unset( $this->queries );
return;
}
}
这个方法做了以下几件事:
- 设置默认值: 如果
meta_query
数组中没有指定relation
,则默认为AND
。 - 处理嵌套查询: 如果数组中的元素本身也是一个数组,那么就递归地调用
WP_Meta_Query
类来处理这个嵌套的查询。 - 处理单个查询: 如果数组中的元素不是数组,那么就认为这是一个单个的查询条件,调用
sanitize_query()
方法进行清理和验证,然后将其添加到queries
属性中。
5. sanitize_query()
:清洗数据,确保安全
sanitize_query()
方法的作用是清洗和验证查询条件,确保数据的安全性。
protected function sanitize_query( $query, $key ) {
if ( ! is_array( $query ) ) {
return $query;
}
$query = wp_parse_args( $query, array(
'key' => '',
'value' => '',
'compare' => '=',
'type' => 'CHAR',
) );
$query['key'] = trim( $query['key'] );
if ( ! empty( $query['key'] ) ) {
$query['compare'] = strtoupper( $query['compare'] );
$query['type'] = strtoupper( $query['type'] );
}
return $query;
}
这个方法做了以下几件事:
- 设置默认值: 如果查询条件中没有指定
value
、compare
或type
,则设置默认值。 - 清理
key
: 使用trim()
函数去除key
前后的空格。 - 转换大小写: 将
compare
和type
转换为大写。
6. get_sql()
:生成 SQL 查询语句
现在,我们已经将 meta_query
数组解析成了 WP_Meta_Query
类的 queries
属性。接下来,我们需要将这些查询条件转换成 SQL 的 JOIN
和 WHERE
子句。这个任务由 get_sql()
方法完成。
public function get_sql( $table_prefix, $primary_table, $primary_id_column, $context = '' ) {
global $wpdb;
$sql = $this->get_sql_clauses( $table_prefix, $primary_table, $primary_id_column, $context );
if ( empty( $sql['where'] ) ) {
return $sql;
}
$sql['where'] = ' AND ' . $sql['where'];
return $sql;
}
这个方法调用了 get_sql_clauses()
方法来生成 SQL 子句,然后将它们组合起来。
7. get_sql_clauses()
:构建 JOIN
和 WHERE
子句的核心
get_sql_clauses()
方法是构建 JOIN
和 WHERE
子句的核心。它遍历 queries
属性,并根据每个查询条件的 key
、value
、compare
和 type
生成相应的 SQL 子句。
public function get_sql_clauses( $table_prefix, $primary_table, $primary_id_column, $context = '' ) {
global $wpdb;
$sql = array(
'join' => '',
'where' => '',
);
$sql_chunks = array(
'relation' => $this->relation,
'queries' => array(),
);
$meta_table = $table_prefix . 'postmeta';
$i = 0;
foreach ( $this->queries as $query ) {
$i++;
if ( is_a( $query, 'WP_Meta_Query' ) ) {
// It's a nested query.
$sql_array = $query->get_sql_clauses( $table_prefix, $primary_table, $primary_id_column, $context );
if ( ! empty( $sql_array['where'] ) ) {
$sql_chunks['queries'][] = '(' . $sql_array['where'] . ')';
}
$sql['join'] .= $sql_array['join'];
} else {
// It's a single query.
$sql_chunks['queries'][] = $this->get_sql_for_query( $query, $table_prefix, $primary_table, $primary_id_column, $context, $i );
}
}
if ( ! empty( $sql_chunks['queries'] ) ) {
$sql['where'] = implode( " {$sql_chunks['relation']} ", $sql_chunks['queries'] );
}
return $sql;
}
这个方法做了以下几件事:
- 处理嵌套查询: 如果
queries
属性中的元素是一个WP_Meta_Query
类的实例,那么就递归地调用get_sql_clauses()
方法来生成 SQL 子句。 - 处理单个查询: 如果
queries
属性中的元素不是一个WP_Meta_Query
类的实例,那么就认为这是一个单个的查询条件,调用get_sql_for_query()
方法来生成 SQL 子句。 - 组合 SQL 子句: 将生成的 SQL 子句按照
relation
属性(AND
或OR
)组合起来。
8. get_sql_for_query()
:生成单个查询条件的 SQL 子句
get_sql_for_query()
方法是生成单个查询条件的 SQL 子句的关键。它根据查询条件的 key
、value
、compare
和 type
生成不同的 SQL 子句。
protected function get_sql_for_query( $query, $table_prefix, $primary_table, $primary_id_column, $context, $i ) {
global $wpdb;
$meta_table = $table_prefix . 'postmeta';
$meta_id_column = 'meta_id';
$esc_id_column = esc_sql( $primary_id_column );
$sql = '';
$compare = $query['compare'];
$key = $query['key'];
$value = $query['value'];
$type = $query['type'];
$meta_key = esc_sql( $key );
$meta_value = esc_sql( $value );
// Avoid the situation where someone is passing in unsafe data like '%s'
// without properly sanitizing it first.
$meta_value = wp_kses_data( $meta_value );
$field = "$meta_table.meta_value";
// Cast to correct type
switch ( $type ) {
case 'NUMERIC':
$field = "CAST($meta_table.meta_value AS SIGNED)";
break;
case 'DECIMAL':
$field = "CAST($meta_table.meta_value AS DECIMAL(10,5))";
break;
case 'BINARY':
$field = "BINARY $meta_table.meta_value";
break;
case 'CHAR':
default:
$field = "$meta_table.meta_value";
break;
}
switch ( $compare ) {
case 'IN':
case 'NOT IN':
if ( ! is_array( $value ) ) {
$value = preg_split( '/[,s]+/', $value );
}
$value = array_map( 'esc_sql', $value );
$value = "'" . implode( "','", $value ) . "'";
$sql = "$meta_table.meta_key = '$meta_key' AND $field $compare ($value)";
break;
case 'BETWEEN':
case 'NOT BETWEEN':
$value = array_map( 'esc_sql', $value );
$sql = "$meta_table.meta_key = '$meta_key' AND $field $compare {$value[0]} AND {$value[1]}";
break;
case 'REGEXP':
case 'NOT REGEXP':
case 'RLIKE':
$sql = "$meta_table.meta_key = '$meta_key' AND $field $compare '$meta_value'";
break;
case '>=':
case '<=':
case '=':
case '!=':
case '>':
case '<':
$sql = "$meta_table.meta_key = '$meta_key' AND $field $compare '$meta_value'";
break;
case 'LIKE':
case 'NOT LIKE':
$meta_value = '%' . $wpdb->esc_like( stripslashes( $meta_value ) ) . '%';
$sql = "$meta_table.meta_key = '$meta_key' AND $field $compare '$meta_value'";
break;
case 'EXISTS':
$sql = "$meta_table.meta_key = '$meta_key'";
break;
case 'NOT EXISTS':
$sql = "$meta_table.meta_key != '$meta_key'";
break;
default:
$sql = apply_filters( 'get_meta_sql', $sql, $query, $table_prefix, $primary_table, $primary_id_column, $context );
break;
}
return $sql;
}
这个方法非常长,因为它需要处理各种不同的 compare
和 type
。简单来说,它做了以下几件事:
- 构建字段: 根据
type
属性,将meta_value
字段转换为相应的类型(例如NUMERIC
、DECIMAL
)。 - 构建比较条件: 根据
compare
属性,生成不同的比较条件(例如IN
、BETWEEN
、LIKE
)。 - 转义数据: 使用
esc_sql()
函数转义数据,防止 SQL 注入。 - 应用过滤器: 使用
apply_filters()
函数,允许开发者自定义 SQL 子句。
表格总结 compare
参数及其对应的 SQL 语句:
compare |
SQL 语句 | 说明 |
---|---|---|
= |
$meta_table.meta_key = '$meta_key' AND $field = '$meta_value' |
等于 |
!= |
$meta_table.meta_key = '$meta_key' AND $field != '$meta_value' |
不等于 |
> |
$meta_table.meta_key = '$meta_key' AND $field > '$meta_value' |
大于 |
< |
$meta_table.meta_key = '$meta_key' AND $field < '$meta_value' |
小于 |
>= |
$meta_table.meta_key = '$meta_key' AND $field >= '$meta_value' |
大于等于 |
<= |
$meta_table.meta_key = '$meta_key' AND $field <= '$meta_value' |
小于等于 |
LIKE |
$meta_table.meta_key = '$meta_key' AND $field LIKE '$meta_value' |
包含 (注意:$meta_value 会自动添加 % 符号) |
NOT LIKE |
$meta_table.meta_key = '$meta_key' AND $field NOT LIKE '$meta_value' |
不包含 (注意:$meta_value 会自动添加 % 符号) |
IN |
$meta_table.meta_key = '$meta_key' AND $field IN ($value) |
位于 $value 数组中 (例如:'apple','banana','orange' ) |
NOT IN |
$meta_table.meta_key = '$meta_key' AND $field NOT IN ($value) |
不位于 $value 数组中 (例如:'apple','banana','orange' ) |
BETWEEN |
$meta_table.meta_key = '$meta_key' AND $field BETWEEN {$value[0]} AND {$value[1]} |
介于 $value 数组的两个值之间 (例如:100 AND 200 ) |
NOT BETWEEN |
$meta_table.meta_key = '$meta_key' AND $field NOT BETWEEN {$value[0]} AND {$value[1]} |
不介于 $value 数组的两个值之间 (例如:100 AND 200 ) |
EXISTS |
$meta_table.meta_key = '$meta_key' |
存在该 meta_key |
NOT EXISTS |
$meta_table.meta_key != '$meta_key' |
不存在该 meta_key |
REGEXP |
$meta_table.meta_key = '$meta_key' AND $field REGEXP '$meta_value' |
符合正则表达式 $meta_value |
NOT REGEXP |
$meta_table.meta_key = '$meta_key' AND $field NOT REGEXP '$meta_value' |
不符合正则表达式 $meta_value |
RLIKE |
$meta_table.meta_key = '$meta_key' AND $field RLIKE '$meta_value' |
符合正则表达式 $meta_value (MySQL 特有,等同于 REGEXP ) |
9. 回到 WP_Query
:将 SQL 子句添加到查询中
现在,我们已经从 WP_Meta_Query
类中获得了 SQL 的 JOIN
和 WHERE
子句。接下来,我们需要将这些子句添加到 WP_Query
的查询中。
回到 wp-includes/class-wp-query.php
文件,找到 get_posts()
方法。在这个方法里,你会看到类似这样的代码:
$clauses = apply_filters_ref_array( 'posts_clauses', array( $clauses, &$this ) );
$this->sql = "SELECT SQL_CALC_FOUND_ROWS $found_posts_query $distinct $fields
FROM {$wpdb->posts} {$clauses['join']}
WHERE 1=1 {$clauses['where']}
{$clauses['groupby']}
{$clauses['orderby']}
{$clauses['limits']}";
看到没?$clauses['join']
和 $clauses['where']
就是我们从 WP_Meta_Query
类中获得的 SQL 子句!WordPress 将它们添加到 SELECT
语句中,从而实现了根据自定义字段筛选文章的功能。
10. 实例演示:将数组转换为 SQL
为了更好地理解 meta_query
的工作原理,让我们来看一个具体的例子。假设我们有以下 meta_query
数组:
$meta_query = array(
'relation' => 'AND',
array(
'key' => 'color',
'value' => 'red',
'compare' => '='
),
array(
'key' => 'size',
'value' => array('small', 'medium'),
'compare' => 'IN'
)
);
经过 WP_Meta_Query
类的处理,这段数组会被转换成以下的 SQL 子句:
JOIN wp_postmeta ON (wp_posts.ID = wp_postmeta.post_id)
WHERE 1=1
AND (
(wp_postmeta.meta_key = 'color' AND wp_postmeta.meta_value = 'red')
AND
(wp_postmeta.meta_key = 'size' AND wp_postmeta.meta_value IN ('small','medium'))
)
你可以看到,meta_query
数组中的每个查询条件都被转换成了 SQL 的 WHERE
子句,并且使用 AND
连接起来。JOIN
子句用于将 wp_posts
表和 wp_postmeta
表连接起来,以便根据自定义字段进行筛选。
11. 总结:meta_query
的神奇旅程
好了,经过一番探险,我们终于揭开了 meta_query
的神秘面纱。 让我们简单回顾一下:
WP_Query
:meta_query
的起点,负责接收参数并创建WP_Meta_Query
实例。WP_Meta_Query
:meta_query
的核心处理器,负责解析数组并生成 SQL 子句。parse_query_vars()
: 递归解析数组,将查询条件存储在queries
属性中。sanitize_query()
: 清洗和验证查询条件,确保数据的安全性。get_sql()
和get_sql_clauses()
: 构建 SQL 的JOIN
和WHERE
子句。get_sql_for_query()
: 生成单个查询条件的 SQL 子句,根据compare
和type
生成不同的 SQL 语句。WP_Query
(再次): 将生成的 SQL 子句添加到查询中,实现根据自定义字段筛选文章的功能。
表格总结整个流程:
步骤 | 涉及类/方法 | 描述 |
---|---|---|
1. 接收 meta_query |
WP_Query::__construct() , WP_Query::parse_query() |
WP_Query 对象接收包含 meta_query 的参数数组。 |
2. 创建 WP_Meta_Query |
WP_Query::parse_query() |
WP_Query 实例化 WP_Meta_Query 对象,并将 meta_query 数组传递给它。 |
3. 解析参数 | WP_Meta_Query::__construct() , WP_Meta_Query::parse_query_vars() |
WP_Meta_Query 对象使用 parse_query_vars() 方法递归地解析 meta_query 数组,提取查询条件和关系(AND 或 OR )。 |
4. 清理数据 | WP_Meta_Query::sanitize_query() |
对每个查询条件进行清理和验证,确保 key , value , compare , type 等参数的格式正确和安全。 |
5. 生成 SQL 子句 | WP_Meta_Query::get_sql() , WP_Meta_Query::get_sql_clauses() , WP_Meta_Query::get_sql_for_query() |
使用 get_sql() 方法开始构建 SQL 查询语句。 get_sql_clauses() 递归地处理嵌套的 meta_query 。 get_sql_for_query() 方法根据每个查询条件的参数生成具体的 SQL WHERE 子句,例如使用 LIKE , = , IN , BETWEEN 等运算符。 |
6. 组合 SQL | WP_Meta_Query::get_sql_clauses() |
将生成的 SQL WHERE 子句根据 relation (AND 或 OR ) 组合起来。 同时,也生成必要的 JOIN 子句,将 wp_posts 表和 wp_postmeta 表连接起来。 |
7. 应用到查询 | WP_Query::get_posts() |
WP_Query 对象将 WP_Meta_Query 生成的 JOIN 和 WHERE 子句添加到主查询的 SQL 语句中。 |
8. 执行查询 | WP_Query::get_posts() |
WP_Query 对象执行最终的 SQL 查询,并返回符合条件的文章。 |
希望这次探险能让你对 meta_query
有更深入的了解。下次遇到复杂的自定义字段查询需求时,不妨回忆一下我们今天的旅程,相信你一定能找到解决问题的办法!
好了,今天的讲座就到这里。谢谢大家!