大家好,欢迎来到今天的“解剖 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 专家!
感谢大家的参与,我们下期再见!