阐述 `wpdb` 类的 `query()` 方法源码,它是如何执行 SQL 语句并返回结果的?

大家好,今天咱们来聊聊 WordPress 的“心脏”——wpdb 类的 query() 方法!

(清清嗓子)咳咳,各位 WordPress 开发者,今天我们来扒一扒 WordPress 数据库操作的核心武器:wpdb 类的 query() 方法。 别看它名字简单,这可是个狠角色,咱们的博客文章、用户数据、设置选项,都得靠它才能从数据库里拿出来或者存进去。 准备好了吗?咱们这就深入源码,看看它到底是怎么工作的!

第一回合:query() 方法的“庐山真面目”

首先,我们先来看看 query() 方法的定义(基于 WordPress 6.x 版本)。 别怕,代码虽然长,但咱们会一步一步拆解它。

<?php
/**
 * Performs a database query, using current database connection.
 *
 * @since 0.71
 *
 * @global WP_Error $wp_error WordPress error object.
 *
 * @param string $query Database query.
 * @return int|false Number of rows affected/selected or false on error.
 */
public function query( $query ) {
    global $wp_error;

    // Filter the query.
    $query = apply_filters( 'query', $query );

    // For database errors.
    $this->last_error  = '';
    $this->col_info    = null;
    $return_val        = 0;
    $this->num_queries++;

    // If a transaction is active, keep track of how many queries are run.
    if ( $this->in_transaction ) {
        $this->queries_in_transaction++;
    }

    // Do call time tracking.
    if ( WP_DEBUG ) {
        $this->timer_start();
    }

    // Kill embedded linebreaks.
    $query = trim( preg_replace( '/[rn]+/', ' ', $query ) );

    // Bail if there is a missing database connection.
    if ( ! $this->dbh ) {
        $this->db_connect();
    }

    if ( ! $this->dbh ) {
        $this->last_error = 'No database connection.';
        $this->bail( $this->last_error );
        return false;
    }

    // Some queries can be made directly to the server rather than buffered.
    if ( defined( 'SAVEQUERIES' ) && SAVEQUERIES ) {
        $this->queries[] = array(
            $query,
            microtime( true ) - WP_START_TIMESTAMP,
            null,
            $this->get_caller(),
        );
    }

    // Take note of the query.
    $this->last_query = $query;

    // Do query.
    if ( preg_match( '/^s*(create|alter|truncate|drop)s+/i', $query ) ) {
        $this->result = @mysqli_query( $this->dbh, $query );
    } elseif ( preg_match( '/^s*inserts+/i', $query ) ) {
        $this->result = @mysqli_query( $this->dbh, $query );
        $return_val   = $this->insert_id = @mysqli_insert_id( $this->dbh );
    } elseif ( preg_match( '/^s*(delete|update(?! ignore)|replace)s+/i', $query ) ) {
        $this->result   = @mysqli_query( $this->dbh, $query );
        $return_val     = @mysqli_affected_rows( $this->dbh );
    } else {
        $this->result = @mysqli_query( $this->dbh, $query );
        if ( $this->result ) {
            if ( is_object( $this->result ) ) {
                $i               = 0;
                $this->col_info = array();

                while ( $i < mysqli_num_fields( $this->result ) ) {
                    $this->col_info[ $i ] = @mysqli_fetch_field_direct( $this->result, $i );
                    $i++;
                }

                if ( $this->col_info ) {
                    mysqli_field_seek( $this->result, 0 );
                }

                $return_val = mysqli_num_rows( $this->result );
            } else {
                $return_val = 0;
            }
        }
    }

    // Log errors.
    if ( $this->dbh && mysqli_error( $this->dbh ) ) {
        $this->last_error = mysqli_error( $this->dbh );
        $this->bail( $this->last_error );
        if ( WP_DEBUG ) {
            $wp_error->errors['db_error'][] = $this->last_error;
        }
        return false;
    }

    // Do call time tracking.
    if ( WP_DEBUG ) {
        $this->timer_stop();
    }

    return $return_val;
}

第二回合:代码拆解,逐行解读

现在,我们把这段代码拆开,像拆盲盒一样,看看里面都藏了什么宝贝。

  1. $query = apply_filters( 'query', $query );: WordPress 的精髓在于 Hook,这里用 apply_filters 让你有机会在 SQL 语句真正执行之前,对它进行修改。 比如,你可以用这个 Hook 来记录所有的 SQL 查询,或者根据当前用户角色来修改查询条件。

  2. 错误处理和查询计数:

    $this->last_error  = '';
    $this->col_info    = null;
    $return_val        = 0;
    $this->num_queries++;
    • $this->last_error: 每次查询前,先把上次的错误信息清空,保证错误信息的准确性。
    • $this->col_info: 用于存储查询结果的列信息,也需要重置。
    • $return_val: 用于存储查询结果,默认初始化为0。
    • $this->num_queries++: 记录执行的查询次数,方便性能分析。
  3. 事务处理:

    if ( $this->in_transaction ) {
        $this->queries_in_transaction++;
    }

    如果当前处于事务中,则记录事务中的查询次数。

  4. 性能追踪 (WP_DEBUG):

    if ( WP_DEBUG ) {
        $this->timer_start();
    }

    如果开启了 WordPress 的调试模式 (WP_DEBUGtrue),则开始计时,用于追踪查询执行的时间。

  5. 清理 SQL 语句:

    $query = trim( preg_replace( '/[rn]+/', ' ', $query ) );
    • trim(): 移除 SQL 语句开头和结尾的空白字符。
    • preg_replace('/[rn]+/', ' ', $query): 将 SQL 语句中的换行符替换成空格,避免因为换行符导致的 SQL 语法错误。
  6. 数据库连接检查:

    if ( ! $this->dbh ) {
        $this->db_connect();
    }
    
    if ( ! $this->dbh ) {
        $this->last_error = 'No database connection.';
        $this->bail( $this->last_error );
        return false;
    }
    • $this->dbh: 存储数据库连接资源的变量。
    • 如果 $this->dbh 为空,说明还没有建立数据库连接,调用 $this->db_connect() 尝试建立连接。
    • 如果连接失败,记录错误信息,调用 $this->bail() 终止程序执行,并返回 false
  7. 保存查询 (SAVEQUERIES):

    if ( defined( 'SAVEQUERIES' ) && SAVEQUERIES ) {
        $this->queries[] = array(
            $query,
            microtime( true ) - WP_START_TIMESTAMP,
            null,
            $this->get_caller(),
        );
    }

    如果定义了 SAVEQUERIES 常量并且值为 true,则将 SQL 语句、执行时间、调用者等信息保存到 $this->queries 数组中,方便调试和性能分析。

  8. 记录最后一次查询:

    $this->last_query = $query;

    将当前执行的 SQL 语句保存到 $this->last_query 变量中,方便在出现错误时进行排查。

  9. 执行 SQL 语句: 这部分是核心! wpdb 会根据 SQL 语句的类型,使用不同的 mysqli_query() 方式来执行。

    if ( preg_match( '/^s*(create|alter|truncate|drop)s+/i', $query ) ) {
        $this->result = @mysqli_query( $this->dbh, $query );
    } elseif ( preg_match( '/^s*inserts+/i', $query ) ) {
        $this->result = @mysqli_query( $this->dbh, $query );
        $return_val   = $this->insert_id = @mysqli_insert_id( $this->dbh );
    } elseif ( preg_match( '/^s*(delete|update(?! ignore)|replace)s+/i', $query ) ) {
        $this->result   = @mysqli_query( $this->dbh, $query );
        $return_val     = @mysqli_affected_rows( $this->dbh );
    } else {
        $this->result = @mysqli_query( $this->dbh, $query );
        if ( $this->result ) {
            if ( is_object( $this->result ) ) {
                $i               = 0;
                $this->col_info = array();
    
                while ( $i < mysqli_num_fields( $this->result ) ) {
                    $this->col_info[ $i ] = @mysqli_fetch_field_direct( $this->result, $i );
                    $i++;
                }
    
                if ( $this->col_info ) {
                    mysqli_field_seek( $this->result, 0 );
                }
    
                $return_val = mysqli_num_rows( $this->result );
            } else {
                $return_val = 0;
            }
        }
    }
    • create|alter|truncate|drop: 如果是这些语句,直接执行,结果保存在 $this->result 中。
    • insert: 执行插入语句,并使用 mysqli_insert_id() 获取自增 ID,赋值给 $this->insert_id$return_val
    • delete|update|replace: 执行删除、更新或替换语句,并使用 mysqli_affected_rows() 获取受影响的行数,赋值给 $return_val
    • 其他语句 (select 等): 执行查询语句,如果查询成功,则获取查询结果的列信息,并使用 mysqli_num_rows() 获取结果集中的行数,赋值给 $return_val
  10. 错误处理:

    if ( $this->dbh && mysqli_error( $this->dbh ) ) {
        $this->last_error = mysqli_error( $this->dbh );
        $this->bail( $this->last_error );
        if ( WP_DEBUG ) {
            $wp_error->errors['db_error'][] = $this->last_error;
        }
        return false;
    }
    • 检查数据库连接是否有效,并且 mysqli_error() 是否返回错误信息。
    • 如果存在错误,记录错误信息,调用 $this->bail() 终止程序执行,并将错误信息添加到 $wp_error 对象中 (如果开启了 WP_DEBUG)。
    • 返回 false 表示查询失败。
  11. 停止计时 (WP_DEBUG):

    if ( WP_DEBUG ) {
        $this->timer_stop();
    }

    如果开启了 WordPress 的调试模式,则停止计时,计算查询执行的时间。

  12. 返回结果:

    return $return_val;

    返回查询结果。 根据 SQL 语句的类型,$return_val 的值可能表示:

    • 受影响的行数 (delete, update, replace)
    • 自增 ID (insert)
    • 结果集中的行数 (select)
    • 0 (其他语句)
    • false (查询失败)

第三回合:返回值,究竟代表什么?

query() 方法的返回值非常重要,它告诉你 SQL 语句执行的结果。 不同的 SQL 语句类型,返回值的含义也不同。 我们用一个表格来总结一下:

SQL 语句类型 返回值
CREATE, ALTER, TRUNCATE, DROP 成功时返回 true (实际上是 1,PHP 会将 true 转换为 1 进行数值运算),失败时返回 false。注意:由于使用了 @ 抑制错误,所以通常需要通过 $wpdb->last_error 来判断是否执行成功。
INSERT 成功时返回插入行的自增 ID (AUTO_INCREMENT),失败时返回 false
UPDATE, DELETE, REPLACE 成功时返回受影响的行数,失败时返回 false。 注意:如果没有行受到影响,也会返回 0,这和 false 不同。
SELECT 成功时返回结果集中的行数,失败时返回 false。 如果查询没有返回任何行,则返回 0,这和 false 不同。
其他 成功时返回 true (实际上是 1),失败时返回 false

第四回合:错误处理,不能掉以轻心!

wpdb 类的错误处理机制非常重要。 因为默认情况下,query() 方法使用了 @ 符号来抑制错误,这意味着如果 SQL 语句执行失败,PHP 不会直接抛出错误信息。 我们需要通过 $wpdb->last_error 来获取错误信息,并进行相应的处理。

<?php
global $wpdb;

$result = $wpdb->query( "SELECT * FROM non_existent_table" );

if ( false === $result ) {
    echo "SQL 查询失败: " . $wpdb->last_error;
}

第五回合:性能优化,让你的网站飞起来!

虽然 wpdb 类已经做了很多优化,但作为开发者,我们仍然需要注意一些性能问题:

  1. 避免在循环中执行查询: 这是最常见的性能问题。 尽量使用 IN 子句或者 JOIN 语句来一次性获取所有需要的数据。

    // 糟糕的代码
    foreach ( $post_ids as $post_id ) {
        $post = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$wpdb->posts} WHERE ID = %d", $post_id ) );
        // ...
    }
    
    // 更好的代码
    $post_ids_string = implode( ',', array_map( 'intval', $post_ids ) ); // 确保是整数
    $posts = $wpdb->get_results( "SELECT * FROM {$wpdb->posts} WHERE ID IN ($post_ids_string)" );
    // ...
  2. 使用预处理语句: wpdb 类的 prepare() 方法可以帮助你防止 SQL 注入,并且提高查询性能。 prepare() 方法会将 SQL 语句和参数分开处理,避免每次执行查询时都需要重新编译 SQL 语句。

    $post_id = 123;
    $title = 'My Post Title';
    
    // 使用 prepare() 方法
    $wpdb->query( $wpdb->prepare(
        "UPDATE {$wpdb->posts} SET post_title = %s WHERE ID = %d",
        $title,
        $post_id
    ) );
  3. 使用缓存: 对于一些不经常变化的数据,可以使用 WordPress 的对象缓存 API 或者 Transients API 来缓存查询结果,避免每次都从数据库中读取数据。

  4. 优化数据库结构: 合理的数据库表结构和索引可以大大提高查询性能。

第六回合:实战演练,举几个栗子

光说不练假把式,咱们来几个实际的例子:

  1. 获取所有文章的标题和内容:

    <?php
    global $wpdb;
    
    $results = $wpdb->get_results( "SELECT post_title, post_content FROM {$wpdb->posts} WHERE post_type = 'post' AND post_status = 'publish'" );
    
    if ( $results ) {
        foreach ( $results as $result ) {
            echo "<h2>" . esc_html( $result->post_title ) . "</h2>";
            echo "<p>" . wp_kses_post( $result->post_content ) . "</p>";
        }
    } else {
        echo "没有找到任何文章。";
    }
  2. 插入一条新的评论:

    <?php
    global $wpdb;
    
    $data = array(
        'comment_post_ID' => 123,
        'comment_author' => 'John Doe',
        'comment_author_email' => '[email protected]',
        'comment_content' => 'This is a great post!',
        'comment_approved' => 1, // 1 表示已批准,0 表示待审核
    );
    
    $format = array(
        '%d', // comment_post_ID
        '%s', // comment_author
        '%s', // comment_author_email
        '%s', // comment_content
        '%d', // comment_approved
    );
    
    $result = $wpdb->insert( $wpdb->comments, $data, $format );
    
    if ( $result ) {
        echo "评论已成功添加,评论 ID: " . $wpdb->insert_id;
    } else {
        echo "添加评论失败: " . $wpdb->last_error;
    }
  3. 更新文章的标题:

    <?php
    global $wpdb;
    
    $post_id = 123;
    $new_title = 'Updated Post Title';
    
    $data = array(
        'post_title' => $new_title,
    );
    
    $where = array(
        'ID' => $post_id,
    );
    
    $format = array(
        '%s', // post_title
    );
    
    $where_format = array(
        '%d', // ID
    );
    
    $result = $wpdb->update( $wpdb->posts, $data, $where, $format, $where_format );
    
    if ( $result !== false ) {
        echo "文章标题已成功更新,受影响的行数: " . $result;
    } else {
        echo "更新文章标题失败: " . $wpdb->last_error;
    }

第七回合:总结,query() 方法的“葵花宝典”

wpdb 类的 query() 方法是 WordPress 数据库操作的基石。 理解它的工作原理,可以帮助我们编写更高效、更安全的代码。 记住以下几点:

  • 使用 apply_filters( 'query', $query ) Hook 可以修改 SQL 语句。
  • 注意 query() 方法的返回值,它告诉你 SQL 语句执行的结果。
  • 使用 $wpdb->last_error 获取错误信息。
  • 避免在循环中执行查询。
  • 使用 prepare() 方法防止 SQL 注入,提高查询性能。
  • 使用缓存来减少数据库访问。
  • 优化数据库结构和索引。

好了,今天的 wpdb 类的 query() 方法源码解读就到这里。 希望大家有所收获,也希望大家能写出更棒的 WordPress 代码! 感谢各位的耐心聆听,下次再见!

发表回复

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