深入理解 `meta_query` 参数在 `WP_Query` 中的源码实现,解释它如何将数组参数转换为 SQL 的 `JOIN` 和 `WHERE` 子句。

大家好,我是老码农李狗蛋,今天咱们来聊聊 WordPress 里 WP_Querymeta_query 参数,这玩意儿看着简单,水可是深的很呐! 咱们的目标是:彻底搞懂它怎么把一个 PHP 数组,变成 SQL 里复杂的 JOINWHERE 子句,让它彻底在咱们面前裸奔!

一、meta_query 是个啥?

简单来说,meta_query 允许你根据文章的自定义字段(meta fields)来查询文章。 比如,你想找所有颜色是“蓝色”,价格大于 100 的商品,就可以用 meta_query 来实现。

meta_query 的基本结构是一个数组,数组里可以包含多个子数组,每个子数组代表一个查询条件。每个子数组又包含 key(字段名)、value(字段值)、compare(比较操作符)等参数。

例如:

$args = array(
    'post_type' => 'product',
    'meta_query' => array(
        'relation' => 'AND', // 多个条件之间的关系,可选 'AND' 或 'OR'
        array(
            'key'     => 'color',
            'value'   => 'blue',
            'compare' => '='
        ),
        array(
            'key'     => 'price',
            'value'   => 100,
            'compare' => '>='
        )
    )
);

$query = new WP_Query( $args );

这段代码的意思是:查找所有文章类型为 product,且 color 字段等于 blueprice 字段大于等于 100 的文章。

二、WP_Query 源码里 meta_query 的处理流程

WP_Query 在处理 meta_query 的时候,主要涉及以下几个关键函数:

  1. WP_Query::get_posts():这是 WP_Query 的核心函数,负责获取文章。它会调用其他函数来处理各种查询参数,包括 meta_query

  2. WP_Query::parse_query():这个函数负责解析查询参数,并将它们存储在 WP_Query 对象的属性中。 它会把传进来的 meta_query 参数处理成规范化的形式。

  3. WP_Query::get_meta_sql():这个函数是今天的主角!它负责根据 meta_query 生成 SQL 的 JOINWHERE 子句。

咱们重点分析 WP_Query::get_meta_sql() 这个函数。

三、WP_Query::get_meta_sql() 源码剖析

WP_Query::get_meta_sql() 函数的源码比较复杂,咱们把它拆解成几个部分来分析:

/**
 * Generate SQL for meta query clauses.
 *
 * @since 3.1.0
 *
 * @param array  $meta_query Array of meta query clauses.
 * @param string $primary_table Primary table.
 * @param string $primary_id_column Primary column.
 * @return array Array containing the 'join' and 'where' sub-clauses.
 */
public function get_meta_sql( $meta_query, $primary_table, $primary_id_column ) {
    global $wpdb;

    $meta_query = new WP_Meta_Query( $meta_query );

    $sql = $meta_query->get_sql( $primary_table, $primary_id_column, $this->query['suppress_filters'] );

    return array(
        'join'  => $sql['join'],
        'where' => $sql['where'],
    );
}

看起来代码很短,实际上它把主要工作都委托给了 WP_Meta_Query 类。 咱们继续深入 WP_Meta_Query

四、WP_Meta_Query 类的运作

WP_Meta_Query 类负责解析和构建 meta query 的 SQL。 它的核心方法是 get_sql()

/**
 * Get the fully-formed SQL JOIN and WHERE clauses for the query.
 *
 * @since 3.2.0
 *
 * @param string $primary_table The table being joined onto.
 * @param string $primary_id_column The column to join on.
 * @param bool   $suppress Whether to suppress filters.
 * @return array An associative array of SQL JOIN and WHERE clauses.
 */
public function get_sql( $primary_table, $primary_id_column, $suppress = false ) {
    $sql = $this->get_sql_clauses();

    if ( ! empty( $sql['where'] ) ) {
        $sql['where'] = 'AND ' . $sql['where'];
    }

    if ( $suppress ) {
        return $sql;
    }

    /**
     * Filters the fully-formed SQL JOIN clause for the query.
     *
     * @since 3.2.0
     *
     * @param string $join  SQL JOIN clause.
     * @param array  $query An array representing the meta query.
     */
    $sql['join'] = apply_filters( 'get_meta_sql_join', $sql['join'], $this->queries );

    /**
     * Filters the fully-formed SQL WHERE clause for the query.
     *
     * @since 3.2.0
     *
     * @param string $where SQL WHERE clause.
     * @param array  $query An array representing the meta query.
     */
    $sql['where'] = apply_filters( 'get_meta_sql_where', $sql['where'], $this->queries );

    return $sql;
}

get_sql() 又调用了 get_sql_clauses() 来生成 SQL。 咱们继续深入!

五、WP_Meta_Query::get_sql_clauses() 源码分析

这个函数是核心中的核心,它负责遍历 meta_query 数组,并根据每个条件生成对应的 SQL 子句。

/**
 * Get the SQL clauses for the query.
 *
 * @since 4.2.0
 * @return array An associative array of SQL clauses.
 */
public function get_sql_clauses() {
    global $wpdb;

    $sql = array(
        'where' => '1=1',
        'join'  => '',
    );

    $this->tables = array();
    $this->clauses = array();

    if ( empty( $this->queries ) ) {
        return $sql;
    }

    $sql_chunks = $this->get_sql_for_query( $this->queries );

    if ( is_wp_error( $sql_chunks ) ) {
        return $sql_chunks;
    }

    $sql['where'] = implode( ' ', array_filter( $sql_chunks['where'] ) );
    $sql['join']  = implode( ' ', array_unique( $sql_chunks['join'] ) );

    return $sql;
}

可以看到,这个函数初始化了一个 $sql 数组,包含了 wherejoin 两个子元素。然后,它调用 get_sql_for_query() 来处理实际的查询条件。

六、WP_Meta_Query::get_sql_for_query() 源码分析

这个函数递归地处理 meta_query 数组,并根据每个条件生成 SQL。

/**
 * Generate SQL clauses for a single query array.
 *
 * @since 4.2.0
 *
 * @param array $query Query array.
 * @param int   $depth Optional. The level of the query within the nested array.
 *                     Used to generate unique table aliases. Default 0.
 * @return array|WP_Error An associative array containing the 'join' and 'where'
 *                        SQL clauses, or a WP_Error object if there is a problem.
 */
protected function get_sql_for_query( $query, $depth = 0 ) {
    global $wpdb;

    $sql = array(
        'where' => array(),
        'join'  => array(),
    );

    $table_name = $wpdb->postmeta;
    $alias = 'mt' . absint( $depth );

    foreach ( $query as $key => $clause ) {
        if ( 'relation' === $key ) {
            $relation = strtoupper( trim( $clause ) );

            if ( ! in_array( $relation, array( 'AND', 'OR' ), true ) ) {
                $relation = 'AND';
            }

            $sql['where'][] = $relation;
            continue;
        }

        if ( isset( $clause['relation'] ) ) {
            $recursion = $this->get_sql_for_query( $clause, $depth + 1 );

            if ( is_wp_error( $recursion ) ) {
                return $recursion;
            }

            if ( ! empty( $recursion['where'] ) ) {
                $sql['where'][] = '(' . implode( ' ', array_filter( $recursion['where'] ) ) . ')';
            }

            $sql['join'] = array_merge( $sql['join'], $recursion['join'] );
            continue;
        }

        // Stop if not a valid clause.
        if ( ! is_array( $clause ) || empty( $clause['key'] ) ) {
            continue;
        }

        $clause['key'] = trim( $clause['key'] );
        $clause['compare'] = isset( $clause['compare'] ) ? strtoupper( trim( $clause['compare'] ) ) : '=';
        $clause['value'] = isset( $clause['value'] ) ? $clause['value'] : '';
        $clause['type'] = isset( $clause['type'] ) ? strtoupper( trim( $clause['type'] ) ) : 'CHAR';

        // Cast value according to type.
        switch ( $clause['type'] ) {
            case 'NUMERIC':
            case 'DECIMAL':
            case 'SIGNED':
            case 'UNSIGNED':
                $clause['value'] = is_array( $clause['value'] ) ? array_map( 'floatval', $clause['value'] ) : floatval( $clause['value'] );
                break;
            case 'BINARY':
                $clause['value'] = is_array( $clause['value'] ) ? array_map( 'esc_sql', $clause['value'] ) : esc_sql( $clause['value'] );
                break;
            default:
                $clause['value'] = is_array( $clause['value'] ) ? array_map( 'strval', $clause['value'] ) : strval( $clause['value'] );
        }

        $field = "$alias.meta_value";
        $join = "LEFT JOIN $table_name AS $alias ON ($wpdb->posts.ID = $alias.post_id)";

        $this->tables[ $alias ] = true;
        $sql['join'][] = $join;

        $where = $this->get_meta_sql_clause( $clause, $alias, $table_name, $field );

        if ( ! empty( $where ) ) {
            $sql['where'][] = '(' . $where . ')';
        }
    }

    return $sql;
}

这个函数做了很多事情,咱们一点点来看:

  • 初始化 SQL 数组: 初始化 $sql 数组,包含 wherejoin 两个子元素。

  • 遍历查询条件: 遍历 meta_query 数组中的每个查询条件。

  • 处理 relation 如果遇到 relation 键,表示多个条件之间的关系(ANDOR),将其添加到 $sql['where'] 数组中。

  • 递归处理嵌套查询: 如果遇到嵌套的 meta_query,递归调用 get_sql_for_query() 处理。

  • 处理单个查询条件: 对于每个单独的查询条件,执行以下操作:

    • 参数规范化: 获取 keycomparevaluetype 等参数,并进行规范化处理(例如,将 compare 转换为大写,将 type 转换为大写)。

    • 类型转换: 根据 type 参数,将 value 转换为相应的数据类型。

    • 构建 JOIN 子句: 生成 LEFT JOIN 子句,将 wp_posts 表和 wp_postmeta 表连接起来。 这里会为每一个 meta query 生成一个唯一的表别名 (alias) mt0, mt1, mt2… 避免表名冲突。

    • 构建 WHERE 子句: 调用 get_meta_sql_clause() 函数,根据 keycomparevalue 等参数,生成 WHERE 子句。

    • 添加到 SQL 数组: 将生成的 JOINWHERE 子句添加到 $sql 数组中。

  • 返回 SQL 数组: 返回包含 joinwhere 子句的 $sql 数组。

七、WP_Meta_Query::get_meta_sql_clause() 源码分析

这个函数负责根据单个 meta query 条件生成具体的 SQL WHERE 子句。

/**
 * Generate the proper SQL clause to filter a meta field.
 *
 * @since 5.1.0
 *
 * @param array  $clause    Meta query clause.
 * @param string $alias     Table alias.
 * @param string $table     Table name.
 * @param string $field     Meta field.
 * @return string Filter clause.
 */
protected function get_meta_sql_clause( $clause, $alias, $table, $field ) {
    global $wpdb;

    $where = '';

    $compare = $clause['compare'];
    $key     = $clause['key'];
    $value   = $clause['value'];
    $type    = $clause['type'];

    switch ( $compare ) {
        case 'IN':
        case 'NOT IN':
            if ( ! is_array( $value ) ) {
                $value = preg_split( '/[,s]+/', $value );
            }

            if ( empty( $value ) ) {
                $where = '1=0';
                break;
            }

            $placeholders = array_fill( 0, count( $value ), '%s' );
            $placeholders = implode( ', ', $placeholders );

            $where = "$alias.meta_key = %s AND $field " . $compare . " (" . $placeholders . ")";
            $where = $wpdb->prepare( $where, array_merge( array( $key ), $value ) );
            break;
        case 'BETWEEN':
        case 'NOT BETWEEN':
            if ( ! is_array( $value ) || 2 !== count( $value ) ) {
                break;
            }

            $where = "$alias.meta_key = %s AND $field " . $compare . " %f AND %f";
            $where = $wpdb->prepare( $where, $key, $value[0], $value[1] );
            break;
        case 'LIKE':
        case 'NOT LIKE':
            $value = '%' . $wpdb->esc_like( $value ) . '%';
            $where = "$alias.meta_key = %s AND $field $compare %s";
            $where = $wpdb->prepare( $where, $key, $value );
            break;
        case 'REGEXP':
        case 'NOT REGEXP':
        case 'RLIKE':
            $where = "$alias.meta_key = %s AND $field $compare %s";
            $where = $wpdb->prepare( $where, $key, $value );
            break;
        case '=':
        case '!=':
        case '>':
        case '>=':
        case '<':
        case '<=':
            $where = "$alias.meta_key = %s AND $field $compare %s";
            $where = $wpdb->prepare( $where, $key, $value );
            break;
        case 'EXISTS':
            $where = "$alias.meta_key = %s";
            $where = $wpdb->prepare( $where, $key );
            break;
        case 'NOT EXISTS':
            $where = "$alias.meta_key != %s"; // Different from EXISTS.
            $where = $wpdb->prepare( $where, $key );
            break;
        default:
            /**
             * Filters the SQL clause for a meta field comparison.
             *
             * The dynamic portion of the hook name, `$compare`, refers to the
             * comparison operator, such as '=', '!=', 'IN', etc.
             *
             * @since 5.1.0
             *
             * @param string $where   The SQL clause.
             * @param array  $clause  Meta query clause.
             * @param string $alias   Table alias.
             * @param string $table   Table name.
             * @param string $field   Meta field.
             */
            $where = apply_filters( "get_meta_sql_{$compare}", '', $clause, $alias, $table, $field );
    }

    return $where;
}

这个函数根据不同的 compare 操作符,生成不同的 SQL WHERE 子句。 可以看到它支持了各种常见的比较操作符,例如 =!=><INNOT INLIKEBETWEEN 等等。

八、 举个栗子:完整的 SQL 生成过程

咱们用最开始的例子,来模拟一下 SQL 的生成过程:

$args = array(
    'post_type' => 'product',
    'meta_query' => array(
        'relation' => 'AND',
        array(
            'key'     => 'color',
            'value'   => 'blue',
            'compare' => '='
        ),
        array(
            'key'     => 'price',
            'value'   => 100,
            'compare' => '>='
        )
    )
);

$query = new WP_Query( $args );
  1. WP_Query::get_posts() 调用 WP_Query::get_meta_sql()

  2. WP_Query::get_meta_sql() 创建 WP_Meta_Query 对象,并调用 WP_Meta_Query::get_sql()

  3. WP_Meta_Query::get_sql() 调用 WP_Meta_Query::get_sql_clauses()

  4. WP_Meta_Query::get_sql_clauses() 调用 WP_Meta_Query::get_sql_for_query()

  5. WP_Meta_Query::get_sql_for_query() 遍历 meta_query 数组:

    • 第一个条件:color = blue

      • 生成表别名:mt0
      • 生成 JOIN 子句:LEFT JOIN wp_postmeta AS mt0 ON (wp_posts.ID = mt0.post_id)
      • 调用 WP_Meta_Query::get_meta_sql_clause() 生成 WHERE 子句:mt0.meta_key = 'color' AND mt0.meta_value = 'blue'
    • 第二个条件:price >= 100

      • 生成表别名:mt1
      • 生成 JOIN 子句:LEFT JOIN wp_postmeta AS mt1 ON (wp_posts.ID = mt1.post_id)
      • 调用 WP_Meta_Query::get_meta_sql_clause() 生成 WHERE 子句:mt1.meta_key = 'price' AND mt1.meta_value >= 100
    • 合并 JOINWHERE 子句:

      • JOIN: LEFT JOIN wp_postmeta AS mt0 ON (wp_posts.ID = mt0.post_id) LEFT JOIN wp_postmeta AS mt1 ON (wp_posts.ID = mt1.post_id)
      • WHERE: (mt0.meta_key = 'color' AND mt0.meta_value = 'blue') AND (mt1.meta_key = 'price' AND mt1.meta_value >= 100)

最终生成的 SQL (简化版) 大概是这样:

SELECT wp_posts.*
FROM wp_posts
LEFT JOIN wp_postmeta AS mt0 ON (wp_posts.ID = mt0.post_id)
LEFT JOIN wp_postmeta AS mt1 ON (wp_posts.ID = mt1.post_id)
WHERE 1=1
AND wp_posts.post_type = 'product'
AND (mt0.meta_key = 'color' AND mt0.meta_value = 'blue')
AND (mt1.meta_key = 'price' AND mt1.meta_value >= 100)
AND wp_posts.post_status = 'publish'

九、 性能优化小贴士

meta_query 功能强大,但用不好也会导致性能问题。 记住以下几点:

  • 避免过度使用: 尽量减少 meta_query 的数量,复杂查询可以考虑其他方案。
  • 索引优化: 确保 wp_postmeta 表的 post_idmeta_key 列有索引。
  • 缓存: 使用对象缓存或 Redis 等缓存机制,缓存查询结果。
  • 复杂查询: 对于非常复杂的查询,可以考虑编写自定义 SQL 查询,或者使用专门的搜索插件。

十、总结

今天咱们深入剖析了 WP_Querymeta_query 参数的源码实现,了解了它如何将 PHP 数组转换为 SQL 的 JOINWHERE 子句。 掌握了这些知识,你就可以更灵活地使用 meta_query,并避免一些常见的性能问题。

希望今天的分享对大家有所帮助! 记住, 编程就像谈恋爱,只有深入了解,才能更好地驾驭它! 下次再见!

发表回复

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