探究 WordPress `WP_Meta_Query` 类的源码:它如何将 `$meta_query` 参数解析为 SQL `JOIN` 和 `WHERE` 子句。

大家好,欢迎来到今天的“解剖 WordPress 灵魂:WP_Meta_Query 的 SQL 炼金术”讲座。今天我们不谈情怀,只啃代码,看看这个 WP_Meta_Query 到底是个什么东西,又是如何把我们看似人畜无害的 $meta_query 参数,变成一条条冷冰冰的 SQL JOINWHERE 子句的。

准备好了吗?系好安全带,我们要开始“扒皮”了!

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 JOINWHERE 子句,方便我们进行查询。

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 );
}

这段代码主要做了两件事:

  1. 设置关系运算符 relation 如果 $meta_query 中指定了 relation,就使用指定的 relation,否则默认使用 AND。并且会确保 relation 的值是 AND 或者 OR,防止出现意外的值。
  2. 规范化查询条件 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;
}

这段代码做了以下事情:

  1. 设置默认值: 为每个查询条件设置默认值,例如 compare 默认是 =type 默认是 CHAR
  2. 递归处理: 如果查询条件中包含子数组,就递归调用 normalize_query() 方法,处理子数组。
  3. 合并参数: 使用 wp_parse_args() 函数,将查询条件和默认值合并,确保每个查询条件都包含必要的字段。
  4. 移除不必要的字段: 会移除数组键名为字符串的元素,因为 WP_Meta_Query 只处理索引数组和 ‘relation’ 键。

4.3 get_sql():生成 SQL 子句

get_sql() 方法是 WP_Meta_Query 的核心方法,它负责将 $meta_query 参数转换成 SQL JOINWHERE 子句。

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 JOINWHERE 子句。

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

这个方法做了以下事情:

  1. 初始化 SQL 子句: 初始化 joinwhere 子句。
  2. 遍历查询条件: 遍历 $meta_query 中的每个查询条件。
  3. 生成 JOIN 子句: 为每个查询条件生成一个 LEFT JOIN 子句,将主表和元数据表连接起来。 这里假设我们正在查询文章的元数据,所以使用 wp_postmeta 表。
  4. 生成 WHERE 子句: 调用 get_sql_for_clause() 方法,为每个查询条件生成对应的 WHERE 子句。
  5. 拼接 SQL 子句: 将生成的 JOINWHERE 子句拼接起来。

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

这个方法做了以下事情:

  1. 处理不同的比较运算符: 根据 compare 字段的值,生成不同的 WHERE 子句。 例如,如果 compare=,就生成 {$alias}.meta_value = %s;如果 compareLIKE,就生成 {$alias}.meta_value LIKE %s
  2. 处理 NULL 值: 如果 meta_valueNULL,就生成 {$alias}.meta_value IS NULL 或者 {$alias}.meta_value IS NOT NULL
  3. 处理数组值: 如果 compareIN 或者 NOT IN,就将 meta_value 转换成数组,并生成 {$alias}.meta_value IN (value1, value2, ...)
  4. 使用 $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_Queryget_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 JOINWHERE 子句,方便我们进行查询。

6. 总结:WP_Meta_Query 的强大之处

WP_Meta_Query 是 WordPress 中一个非常强大的类,它可以让我们轻松地根据元数据进行查询。 它的强大之处在于:

  • 灵活的查询条件: 可以支持各种各样的查询条件,包括等于、不等于、大于、小于、LIKE、IN、BETWEEN 等。
  • 复杂的条件组合: 可以支持多个条件的组合,包括 ANDOR
  • 防止 SQL 注入: 使用 $wpdb->prepare() 函数,对 meta_value 进行转义,防止 SQL 注入。
  • 可扩展性: 可以通过 callback 字段,自定义比较逻辑。

掌握了 WP_Meta_Query,你就可以轻松地玩转 WordPress 的元数据,实现各种各样的查询需求。

希望今天的讲座对你有所帮助。 记住,代码才是真理! 多看源码,多实践,你也能成为 WordPress 专家!

感谢大家的参与,我们下期再见!

发表回复

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