分析 WordPress `get_adjacent_post()` 函数的源码:如何查询相邻文章的 ID,并解释其性能。

大家好,欢迎来到今天的WordPress源码解剖课!今天我们要聊聊一个WordPress开发者经常会用到的函数——get_adjacent_post(),它能帮助我们找到文章的前一篇和后一篇。别看它名字简单,里面的门道可不少,搞清楚了能让你对WordPress的查询机制理解更上一层楼。

一、get_adjacent_post():你是谁?从哪里来?要到哪里去?

首先,咱们得认识一下get_adjacent_post()这个函数。简单来说,它的作用就是获取与当前文章相邻的文章对象(可以理解为包含了文章所有信息的数组)。相邻的定义可以是:

  • 基于发布日期:这是最常见的用法,按照文章的发布时间先后顺序来确定前一篇和后一篇。
  • 基于分类目录:只在同一个分类目录下的文章中寻找相邻文章。

这个函数藏身于wp-includes/link-template.php文件中。

二、源码解剖:一层一层扒开它的心

我们先来看看get_adjacent_post()函数的原型:

function get_adjacent_post( $in_same_term = false, $excluded_terms = '', $taxonomy = 'category', $previous = true, $excluded_categories = '' ) {
    global $post;

    if ( empty( $post ) ) {
        return null;
    }

    $post = get_post( $post ); // Ensure $post is a WP_Post object.

    return _get_adjacent_post( $in_same_term, $excluded_terms, $taxonomy, $post, $previous, $excluded_categories );
}

看起来挺唬人,但其实它只是个“代理”,真正干活的是_get_adjacent_post()。咱们来看看这个“真身”:

function _get_adjacent_post( $in_same_term = false, $excluded_terms = '', $taxonomy = 'category', $post, $previous = true, $excluded_categories = '' ) {
    global $wpdb;

    if ( empty( $post ) ) {
        return null;
    }

    $op = $previous ? '<' : '>';
    $order = $previous ? 'DESC' : 'ASC';

    $join = '';
    $where = '';

    if ( $in_same_term ) {
        $term_object = get_the_terms( $post->ID, $taxonomy );

        if ( is_array( $term_object ) ) {
            $term_ids = wp_list_pluck( $term_object, 'term_id' );

            $join = " INNER JOIN {$wpdb->term_relationships} AS tr ON p.ID = tr.object_id INNER JOIN {$wpdb->term_taxonomy} AS tt ON tr.term_taxonomy_id = tt.term_taxonomy_id";
            $where = 'AND tt.taxonomy = '' . $taxonomy . '' AND tt.term_id IN (' . implode( ',', array_map( 'intval', $term_ids ) ) . ')';
        }
    }

    if ( ! empty( $excluded_terms ) ) {
        if ( ! is_array( $excluded_terms ) ) {
            $excluded_terms = preg_split( '/[s,]+/', $excluded_terms );
        }

        $excluded_terms = array_map( 'intval', $excluded_terms );
        $excluded_terms = array_filter( $excluded_terms );

        if ( ! empty( $excluded_terms ) ) {
            $join .= " INNER JOIN {$wpdb->term_relationships} AS tr2 ON p.ID = tr2.object_id INNER JOIN {$wpdb->term_taxonomy} AS tt2 ON tr2.term_taxonomy_id = tt2.term_taxonomy_id";
            $where .= ' AND tt2.taxonomy = '' . $taxonomy . '' AND tt2.term_id NOT IN (' . implode( ',', $excluded_terms ) . ')';
        }
    }

    if ( ! empty( $excluded_categories ) ) {
        if ( ! is_array( $excluded_categories ) ) {
            $excluded_categories = preg_split( '/[s,]+/', $excluded_categories );
        }

        $excluded_categories = array_map( 'intval', $excluded_categories );
        $excluded_categories = array_filter( $excluded_categories );

        if ( ! empty( $excluded_categories ) ) {
            $join .= " INNER JOIN {$wpdb->term_relationships} AS tr3 ON p.ID = tr3.object_id INNER JOIN {$wpdb->term_taxonomy} AS tt3 ON tr3.term_taxonomy_id = tt3.term_taxonomy_id";
            $where .= ' AND tt3.taxonomy = 'category' AND tt3.term_id NOT IN (' . implode( ',', $excluded_categories ) . ')';
        }
    }

    $current_post_date = $post->post_date;

    $where .= $wpdb->prepare( " AND p.post_date $op %s", $current_post_date );
    $where .= " AND p.post_type = '$post->post_type'";
    $where .= " AND p.post_status = 'publish'";

    $sort = "ORDER BY p.post_date $order LIMIT 1";

    $query = "SELECT p.* FROM {$wpdb->posts} AS p $join WHERE 1=1 $where $sort";

    $result = $wpdb->get_row( $query );

    if ( ! empty( $result ) ) {
        $result = sanitize_post( $result );
    }

    return $result;
}

这段代码看着有点长,但主要逻辑是构建一个SQL查询语句,然后执行它,获取相邻文章的信息。我们把它拆解成几个部分:

  1. 初始化:

    • 获取全局的$wpdb对象,用于执行数据库查询。
    • 确定查询方向(前一篇还是后一篇),并设置相应的比较运算符($op)和排序方式($order)。
    • 初始化$join$where变量,用于构建SQL查询语句的JOIN和WHERE子句。
  2. 处理分类目录相关参数:

    • $in_same_term:如果设置为true,则只在相同分类目录下的文章中查找。会根据当前文章的分类目录构建JOIN和WHERE子句,确保只查询属于相同分类目录的文章。
    • $excluded_terms:排除某些分类目录。构建JOIN和WHERE子句,排除属于指定分类目录的文章。
    • $excluded_categories:排除某些分类目录(专门针对category分类法)。构建JOIN和WHERE子句,排除属于指定分类目录的文章。
  3. 构建WHERE子句:

    • $where .= $wpdb->prepare( " AND p.post_date $op %s", $current_post_date );:这是核心部分,根据发布日期进行比较。使用$wpdb->prepare()函数来防止SQL注入。
    • $where .= " AND p.post_type = '$post->post_type'";:限制文章类型,确保只查询相同类型的文章。
    • $where .= " AND p.post_status = 'publish'";:只查询已发布的文章。
  4. 构建SQL查询语句:

    • $query = "SELECT p.* FROM {$wpdb->posts} AS p $join WHERE 1=1 $where $sort";:将所有部分组合成一个完整的SQL查询语句。
  5. 执行查询并返回结果:

    • $result = $wpdb->get_row( $query );:执行SQL查询,获取结果。
    • $result = sanitize_post( $result );:对结果进行清理,防止XSS攻击。
    • 返回结果。

三、性能分析:是骡子是马,拉出来溜溜

get_adjacent_post()的性能主要取决于以下几个因素:

  • 数据库查询的复杂度:

    • 最简单的情况(只基于发布日期):只需要查询wp_posts表,速度很快。
    • 复杂的情况(基于分类目录):需要进行JOIN操作,查询速度会慢一些。
    • 排除分类目录会增加JOIN操作,性能也会下降。
  • 数据量:文章越多,查询时间越长。

  • 索引:如果wp_posts表的post_date字段没有索引,查询速度会很慢。

  • 缓存:WordPress自带对象缓存,如果之前已经查询过相同的相邻文章,可以直接从缓存中获取,速度很快。

性能优化建议:

  • 尽量避免使用$in_same_term参数:除非确实需要,否则尽量不要使用这个参数,因为它会增加查询的复杂度。
  • 使用缓存:确保WordPress的对象缓存已经启用,可以大大提高性能。
  • 检查索引:确保wp_posts表的post_date字段已经创建索引。
  • 自定义查询:如果get_adjacent_post()的性能无法满足需求,可以考虑自定义SQL查询,只获取需要的字段,减少数据传输量。
  • 使用Transient API:可以将查询结果缓存到Transient中,设置一个过期时间,可以更灵活地控制缓存。

四、举个栗子:代码示例

假设我们要在文章页面显示前一篇和后一篇的链接,可以这样写:

<?php
$prev_post = get_adjacent_post( false, '', '', true ); // 获取前一篇
$next_post = get_adjacent_post( false, '', '', false ); // 获取后一篇

if ( ! empty( $prev_post ) ) {
    echo '<a href="' . get_permalink( $prev_post->ID ) . '">&laquo; 上一篇:' . esc_html( $prev_post->post_title ) . '</a>';
}

if ( ! empty( $next_post ) ) {
    echo '<a href="' . get_permalink( $next_post->ID ) . '">下一篇:' . esc_html( $next_post->post_title ) . ' &raquo;</a>';
}
?>

这段代码会获取前一篇和后一篇文章,然后显示链接。

五、高级用法:自定义SQL查询

如果get_adjacent_post()的性能无法满足需求,我们可以自定义SQL查询。例如,我们可以只获取文章的ID和标题,减少数据传输量:

<?php
global $wpdb, $post;

$current_post_date = $post->post_date;

$query = $wpdb->prepare(
    "SELECT ID, post_title FROM {$wpdb->posts}
    WHERE post_type = %s
    AND post_status = 'publish'
    AND post_date < %s
    ORDER BY post_date DESC
    LIMIT 1",
    $post->post_type,
    $current_post_date
);

$prev_post = $wpdb->get_row( $query );

if ( ! empty( $prev_post ) ) {
    echo '<a href="' . get_permalink( $prev_post->ID ) . '">&laquo; 上一篇:' . esc_html( $prev_post->post_title ) . '</a>';
}
?>

这段代码会直接执行SQL查询,获取前一篇文章的ID和标题。

六、参数详解:表格伺候

为了方便大家理解,我们用表格来总结一下get_adjacent_post()的参数:

参数名 类型 默认值 描述
$in_same_term boolean false 是否只在相同分类目录下的文章中查找。
$excluded_terms string '' 排除的分类目录ID,多个ID用逗号分隔。
$taxonomy string 'category' 分类法名称,默认为category
$previous boolean true 是否获取前一篇文章。如果为true,则获取前一篇;如果为false,则获取后一篇。
$excluded_categories string '' 排除的分类目录ID,多个ID用逗号分隔。注意:这个参数只针对category分类法有效,如果$taxonomy参数不是category,则这个参数无效。

七、总结:学以致用,融会贯通

get_adjacent_post()是一个非常实用的函数,可以帮助我们轻松获取文章的前一篇和后一篇。但是,在实际使用中,我们需要注意它的性能问题,并根据实际情况进行优化。

希望今天的课程能帮助大家更好地理解get_adjacent_post()的源码和使用方法。记住,源码不是用来背的,而是用来理解的!理解了原理,才能灵活运用,写出更高效的代码。

今天的课程就到这里,下课!

发表回复

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