大家好,欢迎来到今天的“解剖 WordPress 灵魂:WP_Meta_Query
的 SQL 炼金术”讲座。今天我们不谈情怀,只啃代码,看看这个 WP_Meta_Query
到底是个什么东西,又是如何把我们看似人畜无害的 $meta_query
参数,变成一条条冷冰冰的 SQL JOIN
和 WHERE
子句的。
准备好了吗?系好安全带,我们要开始“扒皮”了!
1. 欢迎来到元数据世界
在 WordPress 的世界里,除了文章、页面、分类这些“显性”数据外,还有一种叫“元数据”的隐藏数据。 就像人的身份证,上面除了姓名、性别,还有籍贯、住址等额外的信息。 同样,WordPress 里的文章、用户、评论等,都可以附加各种各样的元数据,用来存储一些额外的属性和信息。
这些元数据存储在专门的元数据表中,比如 wp_postmeta
存储文章的元数据,wp_usermeta
存储用户的元数据,以此类推。 每个元数据表都有类似的结构:
字段名 | 类型 | 说明 |
---|---|---|
meta_id | bigint(20) | 元数据 ID (主键) |
*_id | bigint(20) | 对象 ID (例如:post_id, user_id) |
meta_key | varchar(255) | 元数据键名 (例如:’color’, ‘price’) |
meta_value | longtext | 元数据值 (例如:’red’, ‘19.99’) |
这里的 *_id
会根据元数据类型变化,比如文章的元数据表是 post_id
,用户的元数据表是 user_id
。
2. WP_Meta_Query
:元数据查询的瑞士军刀
如果我们想要根据这些元数据来查询文章或者其他对象,怎么办呢?一个个手写 SQL 语句,那简直是噩梦。 这时候,WP_Meta_Query
就闪亮登场了! 它是 WordPress 提供的一个类,专门用来处理元数据查询的。
WP_Meta_Query
可以接收一个复杂的 $meta_query
参数,这个参数可以包含多个元数据查询条件,甚至可以嵌套组合这些条件。 然后,WP_Meta_Query
会把这些条件解析成对应的 SQL JOIN
和 WHERE
子句,方便我们进行查询。
3. $meta_query
参数:查询的指挥棒
$meta_query
参数是一个数组,它可以包含多个子数组,每个子数组代表一个元数据查询条件。 最简单的 $meta_query
可能长这样:
$meta_query = array(
array(
'key' => 'color',
'value' => 'red',
'compare' => '='
)
);
这个例子表示我们要查询 color
字段的值等于 red
的文章。
$meta_query
还可以包含多个条件,甚至可以嵌套组合:
$meta_query = array(
'relation' => 'AND', // 或者 'OR'
array(
'key' => 'color',
'value' => 'red',
'compare' => '='
),
array(
'key' => 'price',
'value' => array(10, 20),
'compare' => 'BETWEEN',
'type' => 'NUMERIC'
)
);
这个例子表示我们要查询 color
字段的值等于 red
,并且 price
字段的值在 10 到 20 之间的文章。 relation
字段指定了多个条件之间的关系,可以是 AND
或者 OR
。
$meta_query
参数的常用字段:
字段名 | 类型 | 说明 |
---|---|---|
key |
string | 元数据键名 |
value |
string/array | 元数据值。可以是字符串,也可以是数组。 |
compare |
string | 比较运算符。常用的有:= , != , > , >= , < , <= , LIKE , NOT LIKE , IN , NOT IN , BETWEEN , NOT BETWEEN , REGEXP , NOT REGEXP , EXISTS , NOT EXISTS . |
type |
string | 元数据类型。常用的有:STRING , NUMERIC , BINARY , DATE , DATETIME , TIME . 指定类型后,WordPress 会在比较时进行类型转换。 |
relation |
string | 多个条件之间的关系。只能是 AND 或者 OR 。 |
callback |
callable | 自定义的回调函数,用于更复杂的比较逻辑。 |
field |
string | 要比较的字段。 默认是 meta_value ,也可以是 meta_value_num (数值类型) 或 meta_id 。 这在 WordPress 5.9 之后引入,允许直接比较 meta_id。 |
4. WP_Meta_Query
的内部运作:SQL 炼金术的开始
现在,我们来看看 WP_Meta_Query
是如何把 $meta_query
参数变成 SQL 语句的。
4.1 构造函数 __construct()
:初始化和参数校验
WP_Meta_Query
的构造函数接收 $meta_query
参数,并进行初始化和参数校验。
public function __construct( $meta_query = array() ) {
if ( is_array( $meta_query ) && isset( $meta_query['relation'] ) ) {
$this->relation = strtoupper( $meta_query['relation'] );
if ( ! in_array( $this->relation, array( 'AND', 'OR' ), true ) ) {
$this->relation = 'AND';
}
} else {
$this->relation = 'AND';
}
$this->queries = $this->normalize_query( $meta_query );
}
这段代码主要做了两件事:
- 设置关系运算符
relation
: 如果$meta_query
中指定了relation
,就使用指定的relation
,否则默认使用AND
。并且会确保relation
的值是AND
或者OR
,防止出现意外的值。 - 规范化查询条件
normalize_query()
: 调用normalize_query()
方法,对$meta_query
进行规范化处理,确保每个子数组都符合规范。
4.2 normalize_query()
:规范化查询条件
normalize_query()
方法递归地处理 $meta_query
,确保每个子数组都包含必要的字段,并且字段的值符合规范。
protected function normalize_query( $query ) {
$defaults = array(
'key' => '',
'value' => '',
'compare' => '=',
'type' => 'CHAR',
'relation' => 'AND',
);
if ( isset( $query['relation'] ) ) {
$query['relation'] = strtoupper( $query['relation'] );
if ( ! in_array( $query['relation'], array( 'AND', 'OR' ), true ) ) {
unset( $query['relation'] );
}
}
if ( is_array( $query ) ) {
foreach ( $query as $key => $value ) {
if ( 'relation' === $key ) {
continue;
}
if ( is_array( $value ) ) {
$query[ $key ] = $this->normalize_query( $value );
} else {
unset( $query[ $key ] );
$value = wp_parse_args( $value, $defaults );
$query[ $key ] = $value;
}
}
} else {
$query = wp_parse_args( $query, $defaults );
}
return $query;
}
这段代码做了以下事情:
- 设置默认值: 为每个查询条件设置默认值,例如
compare
默认是=
,type
默认是CHAR
。 - 递归处理: 如果查询条件中包含子数组,就递归调用
normalize_query()
方法,处理子数组。 - 合并参数: 使用
wp_parse_args()
函数,将查询条件和默认值合并,确保每个查询条件都包含必要的字段。 - 移除不必要的字段: 会移除数组键名为字符串的元素,因为
WP_Meta_Query
只处理索引数组和 ‘relation’ 键。
4.3 get_sql()
:生成 SQL 子句
get_sql()
方法是 WP_Meta_Query
的核心方法,它负责将 $meta_query
参数转换成 SQL JOIN
和 WHERE
子句。
public function get_sql( $table_prefix, $primary_table, $primary_id_column, $context = '' ) {
$this->table_prefix = $table_prefix;
$this->primary_table = $primary_table;
$this->primary_id_column = $primary_id_column;
$sql = $this->get_sql_clauses();
if ( ! empty( $sql['where'] ) ) {
$sql['where'] = ' AND ' . $sql['where'];
}
return $sql;
}
这个方法接收四个参数:
$table_prefix
: 数据库表前缀,例如wp_
。$primary_table
: 主表名,例如wp_posts
。$primary_id_column
: 主表 ID 列名,例如ID
。$context
: 查询上下文,可以影响 SQL 语句的生成。
get_sql()
方法主要调用了 get_sql_clauses()
方法来生成 SQL 子句,然后将 WHERE
子句拼接起来。
4.4 get_sql_clauses()
:递归生成 SQL 子句
get_sql_clauses()
方法递归地处理 $meta_query
,为每个查询条件生成对应的 SQL JOIN
和 WHERE
子句。
protected function get_sql_clauses() {
$sql = array(
'join' => '',
'where' => '1=1',
);
$sql_chunks = array();
$sql_joins = array();
$meta_query = $this->queries;
if ( empty( $meta_query ) ) {
return $sql;
}
$table_prefix = $this->table_prefix;
$primary_table = $this->primary_table;
$primary_id_column = $this->primary_id_column;
$join_counter = 1;
foreach ( $meta_query as $i => $query ) {
if ( is_array( $query ) ) {
$meta_table = $table_prefix . 'postmeta'; // 假设是文章元数据
$alias = 'mt' . absint( $join_counter );
$sql_joins[ $i ] = " LEFT JOIN {$meta_table} AS {$alias} ON ( {$primary_table}.{$primary_id_column} = {$alias}.post_id )";
$meta_key = esc_sql( $query['key'] );
$meta_value = $query['value'];
$compare = strtoupper( $query['compare'] );
$type = strtoupper( $query['type'] );
$field = isset( $query['field'] ) ? $query['field'] : 'meta_value';
$sql_where = $this->get_sql_for_clause( $query, $alias, $meta_key, $meta_value, $compare, $type, $field );
if ( ! empty( $sql_where ) ) {
$sql_chunks[ $i ] = $sql_where;
}
$join_counter++;
} elseif ( 'AND' === $query || 'OR' === $query ) {
$sql_chunks[ $i ] = $query;
}
}
if ( ! empty( $sql_joins ) ) {
$sql['join'] = implode( "n", $sql_joins );
}
if ( ! empty( $sql_chunks ) ) {
$sql['where'] = implode( " {$this->relation} ", $sql_chunks );
}
return $sql;
}
这个方法做了以下事情:
- 初始化 SQL 子句: 初始化
join
和where
子句。 - 遍历查询条件: 遍历
$meta_query
中的每个查询条件。 - 生成 JOIN 子句: 为每个查询条件生成一个
LEFT JOIN
子句,将主表和元数据表连接起来。 这里假设我们正在查询文章的元数据,所以使用wp_postmeta
表。 - 生成 WHERE 子句: 调用
get_sql_for_clause()
方法,为每个查询条件生成对应的WHERE
子句。 - 拼接 SQL 子句: 将生成的
JOIN
和WHERE
子句拼接起来。
4.5 get_sql_for_clause()
:生成具体的 WHERE 子句
get_sql_for_clause()
方法根据查询条件的 compare
字段,生成具体的 WHERE
子句。
protected function get_sql_for_clause( $query, $alias, $meta_key, $meta_value, $compare, $type, $field ) {
global $wpdb;
$meta_value_quoted = $this->prepare_for_like( $meta_value );
switch ( $compare ) {
case '=':
case '!=':
if ( 'NULL' === $meta_value ) {
$sql = "{$alias}.{$field} IS " . ( '=' === $compare ? 'NULL' : 'NOT NULL' );
} else {
$sql = $wpdb->prepare( "{$alias}.{$field} {$compare} %s", $meta_value );
}
break;
case '>':
case '>=':
case '<':
case '<=':
$sql = $wpdb->prepare( "{$alias}.{$field} {$compare} %s", $meta_value );
break;
case 'LIKE':
case 'NOT LIKE':
$sql = $wpdb->prepare( "{$alias}.{$field} {$compare} %s", '%' . $wpdb->esc_like( $meta_value_quoted ) . '%' );
break;
case 'IN':
case 'NOT IN':
if ( ! is_array( $meta_value ) ) {
$meta_value = preg_split( '/[,s]+/', $meta_value );
}
$placeholders = array_fill( 0, count( $meta_value ), '%s' );
$sql = $wpdb->prepare( "{$alias}.{$field} {$compare} (" . implode( ',', $placeholders ) . ')', $meta_value );
break;
case 'BETWEEN':
case 'NOT BETWEEN':
if ( ! is_array( $meta_value ) || 2 !== count( $meta_value ) ) {
return '';
}
$sql = $wpdb->prepare( "{$alias}.{$field} {$compare} %s AND %s", $meta_value[0], $meta_value[1] );
break;
case 'REGEXP':
case 'NOT REGEXP':
$sql = $wpdb->prepare( "{$alias}.{$field} {$compare} %s", $meta_value );
break;
case 'EXISTS':
$sql = "{$alias}.meta_key = '{$meta_key}'";
break;
case 'NOT EXISTS':
$sql = "{$alias}.meta_key != '{$meta_key}'";
break;
default:
$sql = '';
break;
}
return $sql;
}
这个方法做了以下事情:
- 处理不同的比较运算符: 根据
compare
字段的值,生成不同的WHERE
子句。 例如,如果compare
是=
,就生成{$alias}.meta_value = %s
;如果compare
是LIKE
,就生成{$alias}.meta_value LIKE %s
。 - 处理
NULL
值: 如果meta_value
是NULL
,就生成{$alias}.meta_value IS NULL
或者{$alias}.meta_value IS NOT NULL
。 - 处理数组值: 如果
compare
是IN
或者NOT IN
,就将meta_value
转换成数组,并生成{$alias}.meta_value IN (value1, value2, ...)
。 - 使用
$wpdb->prepare()
防止 SQL 注入: 使用$wpdb->prepare()
函数,对meta_value
进行转义,防止 SQL 注入。
5. 实例演示:从 $meta_query
到 SQL
为了更好地理解 WP_Meta_Query
的运作方式,我们来看一个实例。 假设我们有以下 $meta_query
参数:
$meta_query = array(
'relation' => 'AND',
array(
'key' => 'color',
'value' => 'red',
'compare' => '='
),
array(
'key' => 'price',
'value' => array(10, 20),
'compare' => 'BETWEEN',
'type' => 'NUMERIC'
)
);
我们调用 WP_Meta_Query
的 get_sql()
方法,传入以下参数:
$table_prefix
:wp_
$primary_table
:wp_posts
$primary_id_column
:ID
WP_Meta_Query
会生成以下 SQL 子句:
// JOIN 子句
LEFT JOIN wp_postmeta AS mt1 ON ( wp_posts.ID = mt1.post_id )
LEFT JOIN wp_postmeta AS mt2 ON ( wp_posts.ID = mt2.post_id )
// WHERE 子句
AND (
(mt1.meta_value = 'red')
AND
(mt2.meta_value BETWEEN 10 AND 20)
)
可以看到,WP_Meta_Query
成功地将 $meta_query
参数转换成了 SQL JOIN
和 WHERE
子句,方便我们进行查询。
6. 总结:WP_Meta_Query
的强大之处
WP_Meta_Query
是 WordPress 中一个非常强大的类,它可以让我们轻松地根据元数据进行查询。 它的强大之处在于:
- 灵活的查询条件: 可以支持各种各样的查询条件,包括等于、不等于、大于、小于、LIKE、IN、BETWEEN 等。
- 复杂的条件组合: 可以支持多个条件的组合,包括
AND
和OR
。 - 防止 SQL 注入: 使用
$wpdb->prepare()
函数,对meta_value
进行转义,防止 SQL 注入。 - 可扩展性: 可以通过
callback
字段,自定义比较逻辑。
掌握了 WP_Meta_Query
,你就可以轻松地玩转 WordPress 的元数据,实现各种各样的查询需求。
希望今天的讲座对你有所帮助。 记住,代码才是真理! 多看源码,多实践,你也能成为 WordPress 专家!
感谢大家的参与,我们下期再见!