剖析 WordPress `meta_query` 参数的源码实现:在 `WP_Query` 中,如何将数组参数转换为 SQL `JOIN` 和 `WHERE` 子句。

WordPress meta_query 参数的秘密花园:从数组到 SQL 的奇妙之旅

大家好,我是你们的老朋友,今天咱们来聊聊 WordPress 里一个既强大又有点让人摸不着头脑的东西:meta_query。 咱们这次要做的,就是深入 WP_Query 的源码,看看这个小家伙是怎么把一个看起来人畜无害的数组,变成一段复杂的 SQL JOINWHERE 子句的。准备好了吗?让我们开始这场探险吧!

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_Querymeta_query 的核心处理器

WP_Meta_Query 类是处理 meta_query 参数的核心。它的职责就是将我们传入的数组,转换成 SQL 的 JOINWHERE 子句。

打开 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;
}

这个方法做了以下几件事:

  • 设置默认值: 如果查询条件中没有指定 valuecomparetype,则设置默认值。
  • 清理 key 使用 trim() 函数去除 key 前后的空格。
  • 转换大小写:comparetype 转换为大写。

6. get_sql():生成 SQL 查询语句

现在,我们已经将 meta_query 数组解析成了 WP_Meta_Query 类的 queries 属性。接下来,我们需要将这些查询条件转换成 SQL 的 JOINWHERE 子句。这个任务由 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():构建 JOINWHERE 子句的核心

get_sql_clauses() 方法是构建 JOINWHERE 子句的核心。它遍历 queries 属性,并根据每个查询条件的 keyvaluecomparetype 生成相应的 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 属性(ANDOR)组合起来。

8. get_sql_for_query():生成单个查询条件的 SQL 子句

get_sql_for_query() 方法是生成单个查询条件的 SQL 子句的关键。它根据查询条件的 keyvaluecomparetype 生成不同的 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;
}

这个方法非常长,因为它需要处理各种不同的 comparetype。简单来说,它做了以下几件事:

  • 构建字段: 根据 type 属性,将 meta_value 字段转换为相应的类型(例如 NUMERICDECIMAL)。
  • 构建比较条件: 根据 compare 属性,生成不同的比较条件(例如 INBETWEENLIKE)。
  • 转义数据: 使用 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 的 JOINWHERE 子句。接下来,我们需要将这些子句添加到 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 的神秘面纱。 让我们简单回顾一下:

  1. WP_Query: meta_query 的起点,负责接收参数并创建 WP_Meta_Query 实例。
  2. WP_Meta_Query: meta_query 的核心处理器,负责解析数组并生成 SQL 子句。
  3. parse_query_vars(): 递归解析数组,将查询条件存储在 queries 属性中。
  4. sanitize_query(): 清洗和验证查询条件,确保数据的安全性。
  5. get_sql()get_sql_clauses(): 构建 SQL 的 JOINWHERE 子句。
  6. get_sql_for_query(): 生成单个查询条件的 SQL 子句,根据 comparetype 生成不同的 SQL 语句。
  7. 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 数组,提取查询条件和关系(ANDOR)。
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_queryget_sql_for_query() 方法根据每个查询条件的参数生成具体的 SQL WHERE 子句,例如使用 LIKE, =, IN, BETWEEN 等运算符。
6. 组合 SQL WP_Meta_Query::get_sql_clauses() 将生成的 SQL WHERE 子句根据 relation (ANDOR) 组合起来。 同时,也生成必要的 JOIN 子句,将 wp_posts 表和 wp_postmeta 表连接起来。
7. 应用到查询 WP_Query::get_posts() WP_Query 对象将 WP_Meta_Query 生成的 JOINWHERE 子句添加到主查询的 SQL 语句中。
8. 执行查询 WP_Query::get_posts() WP_Query 对象执行最终的 SQL 查询,并返回符合条件的文章。

希望这次探险能让你对 meta_query 有更深入的了解。下次遇到复杂的自定义字段查询需求时,不妨回忆一下我们今天的旅程,相信你一定能找到解决问题的办法!

好了,今天的讲座就到这里。谢谢大家!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注