剖析 WordPress `WP_Term_Meta_Query` 类源码:分类术语元数据查询条件的实现。

各位观众老爷,晚上好!今天咱们来聊聊 WordPress 里一个可能大家不太常用,但其实挺有意思的类:WP_Term_Meta_Query。这玩意儿专门负责处理分类术语(Term)的元数据查询,说白了,就是给你提供一个更灵活的方式,按照分类术语的自定义字段来检索你想要的分类。

咱们先来个开胃小菜,简单介绍一下它的作用,然后深入源码,看看它是怎么运作的,最后再来点实际例子,保证大家听完之后,下次再遇到分类术语元数据查询的需求,心里有数,手上有招。

1. WP_Term_Meta_Query 是个啥?

在 WordPress 里,分类术语(比如文章的分类、标签)可以拥有自己的元数据(也就是自定义字段)。 假设你有一个“书籍分类”,你想给每个分类添加一个“封面颜色”的自定义字段。 那么,你就可以用 WP_Term_Meta_Query 来查找所有“封面颜色”是“红色”的分类。

听起来有点绕? 没关系,我们用表格来整理一下:

概念 解释
分类术语 (Term) WordPress里的分类、标签等,用于组织文章的内容。
元数据 (Meta) 附加在分类术语上的额外信息,可以理解为自定义字段。比如,分类的“封面颜色”、“推荐指数”等等。
WP_Term_Meta_Query 一个类,专门用来构建复杂的分类术语元数据查询条件。你可以用它来查找满足特定元数据条件的分类术语。

所以,WP_Term_Meta_Query 的作用就是:让你可以更精确地筛选分类术语,基于它们的元数据!

2. 源码剖析:WP_Term_Meta_Query 的内部世界

好了,废话不多说,咱们直接跳进源码,看看 WP_Term_Meta_Query 到底是怎么实现的。 源码位置在 wp-includes/class-wp-term-meta-query.php

(1) 构造函数 __construct()

首先,我们看看构造函数,它负责初始化查询的一些基本参数:

public function __construct( $meta_query = array(), $table_prefix = '' ) {
    $this->table_prefix = $table_prefix;
    if ( ! empty( $table_prefix ) ) {
        $this->meta_table = $this->table_prefix . 'termmeta';
    } else {
        $this->meta_table = _wp_get_term_meta_table(); // 获得 termmeta 表名
    }

    if ( isset( $meta_query['relation'] ) && in_array( strtoupper( $meta_query['relation'] ), array( 'AND', 'OR' ), true ) ) {
        $this->relation = strtoupper( $meta_query['relation'] );
    } else {
        $this->relation = 'AND'; // 默认关系是 AND
    }

    $this->queries = $this->sanitize_query( $meta_query ); // 清理查询参数
}
  • $meta_query: 这是最重要的参数,一个数组,定义了你要查询的元数据条件。 稍后我们会详细讲解这个数组的结构。
  • $table_prefix: 数据库表前缀,一般情况下可以忽略,WordPress 会自动处理。
  • $this->relation: 定义了多个元数据条件之间的关系,可以是 AND (必须同时满足) 或者 OR (满足其中一个即可)。 默认是 AND
  • $this->queries: 经过 sanitize_query() 函数处理后的查询条件,目的是为了安全起见,清理一些不必要的参数。

(2) sanitize_query(): 清理查询参数

这个函数负责对 $meta_query 数组进行清理和格式化:

protected function sanitize_query( $queries ) {
    $cleaned_queries = array();

    if ( ! is_array( $queries ) ) {
        return $cleaned_queries;
    }

    foreach ( $queries as $key => $query ) {
        if ( 'relation' === $key ) {
            continue;
        }

        if ( is_array( $query ) ) {
            $cleaned_queries[] = $this->sanitize_query( $query ); // 递归调用,处理嵌套的查询
            continue;
        }

        if ( ! is_array( $query ) || ! isset( $query['key'] ) || ! isset( $query['value'] ) ) {
            continue; // 忽略不合法的查询条件
        }

        $cleaned_queries[] = $query; // 添加合法的查询条件
    }

    return $cleaned_queries;
}

这个函数的主要作用是:

  • 递归处理嵌套的查询条件。 也就是说,$meta_query 数组可以包含其他的 $meta_query 数组,形成一个树状结构。
  • 验证查询条件是否合法。 一个合法的查询条件必须包含 key (元数据键名) 和 value (元数据值)。

(3) get_sql(): 生成 SQL 查询语句

这是 WP_Term_Meta_Query 最核心的函数,它负责根据 $this->queries 里的查询条件,生成最终的 SQL 查询语句。

public function get_sql( $primary_table, $primary_id_column, $context = '' ) {
    global $wpdb;

    $sql = $this->get_sql_clauses( $primary_table, $primary_id_column, $context );

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

    return $sql;
}

get_sql() 函数调用了 get_sql_clauses() 函数来生成 SQL 的 WHEREJOIN 子句,然后将它们组合起来。

(4) get_sql_clauses(): 生成 SQL 子句

public function get_sql_clauses( $primary_table, $primary_id_column, $context = '' ) {
    global $wpdb;

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

    $sql_chunks = $this->get_sql_for_query( $this->queries, $primary_table, $primary_id_column, $context );

    if ( empty( $sql_chunks ) ) {
        return $sql;
    }

    $sql['where'] = implode( ' ' . $this->relation . ' ', $sql_chunks );

    $join = array();
    foreach ( $this->query_clauses as $query ) {
        if ( ! empty( $query['join'] ) ) {
            $join[] = $query['join'];
        }
    }

    if ( ! empty( $join ) ) {
        $sql['join'] = implode( ' ', $join );
    }

    return $sql;
}

这个函数做了以下事情:

  • 初始化 SQL 的 WHEREJOIN 子句。
  • 调用 get_sql_for_query() 函数,为每个查询条件生成 SQL 片段。
  • 将所有的 SQL 片段用 $this->relation (AND 或者 OR) 连接起来,形成最终的 WHERE 子句。
  • 将所有查询条件需要的 JOIN 子句组合起来。

(5) get_sql_for_query(): 为每个查询条件生成 SQL

protected function get_sql_for_query( $query, $primary_table, $primary_id_column, $context = '' ) {
    global $wpdb;

    $sql = array();

    if ( ! is_array( $query ) ) {
        return $sql;
    }

    foreach ( $query as $key => $clause ) {
        if ( is_array( $clause ) ) {
            $sql_chunks = $this->get_sql_for_query( $clause, $primary_table, $primary_id_column, $context );

            if ( empty( $sql_chunks ) ) {
                continue;
            }

            $sql[] = '(' . implode( ' ' . $this->relation . ' ', $sql_chunks ) . ')';
            continue;
        }

        $this->query_clauses[ $this->query_id ] = $this->get_sql_clause( $clause, $primary_table, $primary_id_column, $context );

        $sql[] = $this->query_clauses[ $this->query_id ]['where'];

        $this->query_id++;
    }

    return $sql;
}

这个函数也是一个递归函数,负责处理嵌套的查询条件。 它会调用 get_sql_clause() 函数来为每个具体的查询条件生成 SQL 片段。

(6) get_sql_clause(): 生成单个查询条件的 SQL

public function get_sql_clause( $query, $primary_table, $primary_id_column, $context = '' ) {
    global $wpdb;

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

    $meta_key = $query['key'];
    $meta_value = $query['value'];
    $compare = isset( $query['compare'] ) ? strtoupper( $query['compare'] ) : '='; // 默认比较运算符是 =
    $type = isset( $query['type'] ) ? strtoupper( $query['type'] ) : 'CHAR'; // 默认数据类型是 CHAR
    $meta_type = isset( $query['meta_type'] ) ? $query['meta_type'] : '';

    $alias = 'mt' . $this->query_id;

    $sql['join'] = " LEFT JOIN {$this->meta_table} AS {$alias} ON {$primary_table}.{$primary_id_column} = {$alias}.term_id";

    $where = "$alias.meta_key = %s";

    if($meta_type){
        $where .= " AND {$alias}.meta_type = %s";
    }

    switch ( $compare ) {
        case '=':
            $where .= " AND {$alias}.meta_value = %s";
            $args = array( $meta_key, $meta_value );
            if($meta_type){
                array_splice($args, 1, 0, $meta_type);
            }
            break;
        case '!=':
            $where .= " AND {$alias}.meta_value != %s";
            $args = array( $meta_key, $meta_value );
            if($meta_type){
                array_splice($args, 1, 0, $meta_type);
            }
            break;
        case '>':
        case '>=':
        case '<':
        case '<=':
            $where .= " AND {$alias}.meta_value {$compare} %s";
            $args = array( $meta_key, $meta_value );
            if($meta_type){
                array_splice($args, 1, 0, $meta_type);
            }
            break;
        case 'LIKE':
            $where .= " AND {$alias}.meta_value LIKE %s";
            $args = array( $meta_key, '%' . $wpdb->esc_like( $meta_value ) . '%' );
             if($meta_type){
                array_splice($args, 1, 0, $meta_type);
            }
            break;
        case 'NOT LIKE':
            $where .= " AND {$alias}.meta_value NOT LIKE %s";
            $args = array( $meta_key, '%' . $wpdb->esc_like( $meta_value ) . '%' );
             if($meta_type){
                array_splice($args, 1, 0, $meta_type);
            }
            break;
        case 'IN':
            if ( ! is_array( $meta_value ) ) {
                $meta_value = array( $meta_value );
            }

            $placeholders = array_fill( 0, count( $meta_value ), '%s' );
            $where .= " AND {$alias}.meta_value IN (" . implode( ',', $placeholders ) . ")";
            $args = array_merge( array( $meta_key ), $meta_value );
            if($meta_type){
                $args = array_merge(array($meta_key, $meta_type), $meta_value);
            }
            break;
        case 'NOT IN':
            if ( ! is_array( $meta_value ) ) {
                $meta_value = array( $meta_value );
            }

            $placeholders = array_fill( 0, count( $meta_value ), '%s' );
            $where .= " AND {$alias}.meta_value NOT IN (" . implode( ',', $placeholders ) . ")";
            $args = array_merge( array( $meta_key ), $meta_value );
            if($meta_type){
                $args = array_merge(array($meta_key, $meta_type), $meta_value);
            }
            break;
        case 'BETWEEN':
            if ( ! is_array( $meta_value ) || count( $meta_value ) !== 2 ) {
                break; // Invalid values
            }

            $where .= " AND {$alias}.meta_value BETWEEN %s AND %s";
            $args = array( $meta_key, $meta_value[0], $meta_value[1] );
            if($meta_type){
                 array_splice($args, 1, 0, $meta_type);
            }
            break;
        case 'NOT BETWEEN':
            if ( ! is_array( $meta_value ) || count( $meta_value ) !== 2 ) {
                break; // Invalid values
            }

            $where .= " AND {$alias}.meta_value NOT BETWEEN %s AND %s";
            $args = array( $meta_key, $meta_value[0], $meta_value[1] );
             if($meta_type){
                array_splice($args, 1, 0, $meta_type);
            }
            break;
        case 'EXISTS':
            $where = "{$alias}.meta_key = %s";
            $args = array( $meta_key );
            if($meta_type){
                $where .= " AND {$alias}.meta_type = %s";
                $args = array( $meta_key, $meta_type );
            }
            break;
        case 'NOT EXISTS':
            $sql['join'] = " LEFT JOIN {$this->meta_table} AS {$alias} ON {$primary_table}.{$primary_id_column} = {$alias}.term_id AND {$alias}.meta_key = %s";
            if($meta_type){
                $sql['join'] .= " AND {$alias}.meta_type = %s";
                $args = array( $meta_key, $meta_type );
                $where = "{$alias}.term_id IS NULL";
            } else {
                $args = array( $meta_key );
                $where = "{$alias}.term_id IS NULL";
            }
            break;
        default:
            $where .= " AND {$alias}.meta_value = %s"; // 默认使用 =
            $args = array( $meta_key, $meta_value );
             if($meta_type){
                array_splice($args, 1, 0, $meta_type);
            }
            break;
    }

    $sql['where'] = $wpdb->prepare( $where, $args );
    $this->query_clauses[ $this->query_id ] = $sql;
    return $sql;
}

这个函数是整个 WP_Term_Meta_Query 的灵魂所在! 它做了以下关键的事情:

  • 构建 JOIN 子句: 将 termmeta 表 (存储分类术语元数据的表) 和主表 (比如 terms 表) 连接起来。
  • 构建 WHERE 子句: 根据 keyvaluecomparetype 参数,生成具体的查询条件。 支持多种比较运算符 ( =, !=, >, <, LIKE, IN, BETWEEN 等)。
  • 使用 $wpdb->prepare() 函数,对 SQL 语句进行安全处理,防止 SQL 注入。

总结:WP_Term_Meta_Query 的工作流程

  1. 接收查询参数: 通过构造函数接收 $meta_query 数组,定义查询条件。
  2. 清理参数: 使用 sanitize_query() 函数,对查询参数进行清理和格式化。
  3. 生成 SQL: 调用 get_sql() 函数,生成最终的 SQL 查询语句。
  4. 构建 SQL 子句: get_sql() 又调用 get_sql_clauses() 函数,将查询条件分解成 SQL 的 WHEREJOIN 子句。
  5. 处理每个查询条件: get_sql_clauses() 调用 get_sql_for_query() 函数,递归处理嵌套的查询条件。
  6. 生成单个条件的 SQL: get_sql_for_query() 调用 get_sql_clause() 函数,为每个具体的查询条件生成 SQL 片段,包括 JOINWHERE 子句。
  7. 安全处理: get_sql_clause() 使用 $wpdb->prepare() 函数,对 SQL 语句进行安全处理。

3. 实战演练:如何使用 WP_Term_Meta_Query

光说不练假把式,咱们来几个实际的例子,看看怎么用 WP_Term_Meta_Query

(1) 查找 "封面颜色" 是 "红色" 的分类

$args = array(
    'taxonomy' => 'category', // 指定分类法
    'meta_query' => array(
        array(
            'key' => 'cover_color', // 元数据键名
            'value' => 'red', // 元数据值
            'compare' => '=', // 比较运算符,默认是 =
        ),
    ),
);

$terms = get_terms( $args );

if ( ! empty( $terms ) && ! is_wp_error( $terms ) ) {
    echo '<ul>';
    foreach ( $terms as $term ) {
        echo '<li>' . esc_html( $term->name ) . '</li>';
    }
    echo '</ul>';
} else {
    echo '没有找到符合条件的分类。';
}

在这个例子中,我们使用了 get_terms() 函数,并传入了 meta_query 参数。 meta_query 是一个数组,包含一个子数组,定义了我们要查询的元数据条件:

  • key: cover_color,表示我们要查询的元数据键名是 "cover_color"。
  • value: red,表示我们要查询的元数据值是 "red"。
  • compare: =,表示我们要查找 "cover_color" 等于 "red" 的分类。

(2) 查找 "推荐指数" 大于 8 的分类

$args = array(
    'taxonomy' => 'category',
    'meta_query' => array(
        array(
            'key' => 'recommendation_index',
            'value' => 8,
            'compare' => '>', // 比较运算符,大于
            'type' => 'NUMERIC', // 数据类型,数值型
        ),
    ),
);

$terms = get_terms( $args );

// ... (后续代码同上)

这个例子和上一个例子类似,但是使用了不同的比较运算符 > (大于),并且指定了 typeNUMERIC,表示我们要比较的是数值型数据。 如果不指定 type,WordPress 默认会把元数据值当做字符串来比较,可能会导致错误的结果。

(3) 查找 "作者" 是 "张三" 或者 "李四" 的分类

$args = array(
    'taxonomy' => 'category',
    'meta_query' => array(
        'relation' => 'OR', // 多个条件之间的关系是 OR
        array(
            'key' => 'author',
            'value' => '张三',
            'compare' => '=',
        ),
        array(
            'key' => 'author',
            'value' => '李四',
            'compare' => '=',
        ),
    ),
);

$terms = get_terms( $args );

// ... (后续代码同上)

在这个例子中,我们使用了 relation 参数,指定多个元数据条件之间的关系是 OR,表示只要满足其中一个条件,就返回该分类。

(4) 查找 "封面颜色" 是 "红色" 并且 "推荐指数" 大于 8 的分类

$args = array(
    'taxonomy' => 'category',
    'meta_query' => array(
        'relation' => 'AND', // 多个条件之间的关系是 AND,默认是 AND
        array(
            'key' => 'cover_color',
            'value' => 'red',
            'compare' => '=',
        ),
        array(
            'key' => 'recommendation_index',
            'value' => 8,
            'compare' => '>',
            'type' => 'NUMERIC',
        ),
    ),
);

$terms = get_terms( $args );

// ... (后续代码同上)

这个例子和上一个例子类似,但是使用了 relation 参数,指定多个元数据条件之间的关系是 AND,表示必须同时满足所有条件,才返回该分类。

(5) 嵌套查询:查找 "系列" 是 "哈利波特" 并且 ("封面颜色" 是 "红色" 或者 "封面材质" 是 "皮革") 的分类

$args = array(
    'taxonomy' => 'category',
    'meta_query' => array(
        'relation' => 'AND',
        array(
            'key' => 'series',
            'value' => '哈利波特',
            'compare' => '=',
        ),
        array(
            'relation' => 'OR',
            array(
                'key' => 'cover_color',
                'value' => 'red',
                'compare' => '=',
            ),
            array(
                'key' => 'cover_material',
                'value' => '皮革',
                'compare' => '=',
            ),
        ),
    ),
);

$terms = get_terms( $args );

// ... (后续代码同上)

这个例子展示了如何使用嵌套的 meta_query 数组,构建更复杂的查询条件。 meta_query 数组可以包含其他的 meta_query 数组,形成一个树状结构,从而实现更灵活的查询。

(6) 使用 EXISTS 和 NOT EXISTS: 查找 存在 "封面颜色" 元数据键的分类 或者 不存在 "封面颜色" 元数据键的分类

//查找存在"封面颜色"元数据键的分类
$args = array(
    'taxonomy' => 'category',
    'meta_query' => array(
        array(
            'key' => 'cover_color',
            'compare' => 'EXISTS',
        ),
    ),
);

$terms = get_terms( $args );

//查找不存在"封面颜色"元数据键的分类
$args = array(
    'taxonomy' => 'category',
    'meta_query' => array(
        array(
            'key' => 'cover_color',
            'compare' => 'NOT EXISTS',
        ),
    ),
);

$terms = get_terms( $args );

在这个例子中,我们使用了 EXISTSNOT EXISTS 比较运算符来查找是否存在某个元数据键。 这在某些情况下非常有用,比如你想查找所有设置了 "封面颜色" 的分类,或者所有没有设置 "封面颜色" 的分类。

4. 注意事项

  • 性能问题: 复杂的 meta_query 查询可能会影响性能,特别是当数据量很大时。 尽量避免使用过于复杂的查询条件,并考虑使用缓存来提高性能。
  • 数据类型: 确保 type 参数设置正确,特别是当比较数值型或日期型数据时。
  • 安全问题: 使用 $wpdb->prepare() 函数对 SQL 语句进行安全处理,防止 SQL 注入。
  • 调试: 可以使用 echo $wpdb->last_query; 来查看 WordPress 生成的 SQL 查询语句,方便调试。

5. 总结

WP_Term_Meta_Query 是一个强大的工具,可以让你更灵活地查询分类术语的元数据。 掌握了它的使用方法,你就可以构建更复杂的分类筛选条件,从而更好地组织和管理你的 WordPress 内容。

希望今天的讲座对大家有所帮助! 以后遇到分类术语元数据查询的问题,不要慌,想想 WP_Term_Meta_Query,它会是你的得力助手。

散会! 各位观众老爷,下次再见!

发表回复

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