阐述 WordPress `dbDelta()` 函数源码:如何安全地创建和更新自定义插件数据库表。

好的,各位听众,欢迎来到今天的WordPress插件开发高级研讨会。今天我们要聊的是一个重量级选手,也是很多插件开发者容易忽略,但又至关重要的函数:dbDelta()。 别害怕,名字听着像三角洲部队,其实它非常友好,能帮你安全高效地创建和更新插件的数据库表。

开场白:为什么我们需要 dbDelta()

想象一下,你辛辛苦苦写了个牛逼的插件,用户装上去发现,哎呀,数据库表没建好,一片空白,功能用不了! 这绝对是噩梦。 手动建表? 万一用户之前装过老版本,表结构不一样,更新升级的时候直接崩溃? 更恐怖。

dbDelta() 就是来解决这些问题的,它能:

  1. 自动创建数据库表: 如果表不存在,它帮你创建。
  2. 安全更新数据库表: 如果表存在,但结构和你的插件要求的不同,它会安全地更新表结构,不会丢失数据。
  3. 避免重复创建: 它能判断表是否已经存在,避免重复创建导致错误。

dbDelta() 源码解析:步步深入

dbDelta() 函数的源码位于 wp-admin/includes/upgrade.php 文件中。 让我们拆解它,看看它到底是怎么工作的。

首先,我们来看下dbDelta()函数的签名:

function dbDelta( $queries, $execute = true ) {
    global $wpdb;

    if ( ! is_array( $queries ) ) {
        $queries = explode( ';', $queries );
    }

    $queries = array_filter( $queries );

    if ( empty( $queries ) ) {
        return;
    }

    require_once ABSPATH . 'wp-admin/includes/upgrade.php';

    $cqueries = array();

    foreach ( $queries as $query ) {
        $query = trim( $query );
        if ( empty( $query ) ) {
            continue;
        }

        $cqueries[] = $query;
    }

    /**
     * Filters the array of database queries to execute.
     *
     * @since 2.5.0
     *
     * @param string[] $cqueries An array of database queries.
     */
    $cqueries = apply_filters( 'dbdelta_queries', $cqueries );

    if ( $execute ) {
        foreach ( $cqueries as $query ) {
            maybe_add_column( $query );
            $wpdb->query( $query );
        }
    }

    return $cqueries;
}

第一步:参数处理与准备

  • $queries:这是最重要的参数,它是一个包含 SQL 创建表语句的字符串或数组。 注意,必须包含完整的 CREATE TABLE 语句,包括字段名、类型、长度、索引等等。
  • $execute:可选参数,默认为 true,表示立即执行 SQL 语句。 如果设置为 false,则只返回处理后的 SQL 语句,不实际执行。

函数首先会把传入的$queries 转换成数组,然后过滤掉空的查询语句。

第二步:核心逻辑:表结构比较与更新

dbDelta() 的核心逻辑并不在函数自身,而是依赖于 maybe_add_column() 函数。 让我们看看 maybe_add_column() 函数的源码(简化版):

function maybe_add_column( $query ) {
    global $wpdb;

    if ( strpos( $query, 'CREATE TABLE' ) === false ) {
        return;
    }

    $table_name = preg_replace( '/.*CREATE TABLE [`]?([^` ]*)[`]? .*/', '$1', $query );

    if ( $wpdb->get_var( $wpdb->prepare( "SHOW TABLES LIKE %s", $table_name ) ) != $table_name ) {
        return; // Table doesn't exist.
    }

    $cqueries = array();

    // Parse the query to get the column definitions.
    $matches = array();
    preg_match_all( '/`([^`]+)`s+([^s,]+)s*(.*)(,|DEFAULT|$)/U', $query, $matches, PREG_SET_ORDER );

    if ( empty( $matches ) ) {
        return;
    }

    foreach ( $matches as $match ) {
        $column_name = $match[1];
        $column_type = $match[2];
        $column_attributes = trim( $match[3] );

        $exists = $wpdb->get_results( $wpdb->prepare( "SHOW COLUMNS FROM `$table_name` LIKE %s", $column_name ) );
        if ( ! $exists ) {
            // Column doesn't exist, add it.
            $cqueries[] = "ALTER TABLE `$table_name` ADD `$column_name` $column_type $column_attributes";
        } else {
            // Column exists, check if the definition matches.
            // (This part is simplified for brevity)
        }
    }

    foreach ( $cqueries as $alter_query ) {
        $wpdb->query( $alter_query );
    }
}

这个函数做了以下事情:

  1. 判断是否为 CREATE TABLE 语句: 如果不是,直接返回。
  2. 提取表名:CREATE TABLE 语句中提取表名。
  3. 判断表是否存在: 如果表不存在,直接返回,因为 dbDelta() 会处理表创建。
  4. 解析 SQL 语句,提取字段信息: 使用正则表达式提取字段名、类型和属性。
  5. 检查字段是否存在: 遍历每个字段,如果字段不存在,则使用 ALTER TABLE 语句添加字段。
  6. (简化版中省略)比较字段定义: 如果字段存在,则比较字段的类型、长度和属性是否与 CREATE TABLE 语句中的定义一致。 如果不一致,则使用 ALTER TABLE 语句修改字段。
  7. 执行 ALTER TABLE 语句: 依次执行添加或修改字段的 SQL 语句。

第三步:执行 SQL 语句

dbDelta() 函数会将处理后的 SQL 语句通过 $wpdb->query() 函数执行。 $wpdb 是 WordPress 的全局数据库对象,提供了访问数据库的接口。

使用 dbDelta() 的最佳实践

  1. 完整的 CREATE TABLE 语句: 确保你的 SQL 语句包含完整的 CREATE TABLE 语句,包括字段名、类型、长度、索引等等。 举个例子:

    $sql = "CREATE TABLE {$wpdb->prefix}my_plugin_data (
      id mediumint(9) NOT NULL AUTO_INCREMENT,
      time datetime DEFAULT '0000-00-00 00:00:00' NOT NULL,
      name varchar(255) NOT NULL,
      value text,
      PRIMARY KEY  (id)
    ) {$wpdb->get_charset_collate()};";

    注意:

    • {$wpdb->prefix}:使用 WordPress 的表前缀,避免和其他插件的表名冲突。
    • {$wpdb->get_charset_collate()}:指定字符集和排序规则,确保数据库支持中文等特殊字符。
  2. 插件激活时调用: 在插件激活时调用 dbDelta() 函数。 这样可以确保插件安装后,数据库表立即被创建或更新。

    function my_plugin_activate() {
      global $wpdb;
      $sql = "CREATE TABLE {$wpdb->prefix}my_plugin_data (
        id mediumint(9) NOT NULL AUTO_INCREMENT,
        time datetime DEFAULT '0000-00-00 00:00:00' NOT NULL,
        name varchar(255) NOT NULL,
        value text,
        PRIMARY KEY  (id)
      ) {$wpdb->get_charset_collate()};";
    
      require_once( ABSPATH . 'wp-admin/includes/upgrade.php' );
      dbDelta( $sql );
    }
    register_activation_hook( __FILE__, 'my_plugin_activate' );
  3. 版本控制: 每次修改数据库表结构时,都要更新插件的版本号。 并在插件更新时再次调用 dbDelta() 函数。 这样可以确保数据库表结构始终与插件代码保持同步。

    function my_plugin_update_db_check() {
      if ( get_site_option( 'my_plugin_db_version' ) != '1.1' ) {
        global $wpdb;
        $sql = "CREATE TABLE {$wpdb->prefix}my_plugin_data (
          id mediumint(9) NOT NULL AUTO_INCREMENT,
          time datetime DEFAULT '0000-00-00 00:00:00' NOT NULL,
          name varchar(255) NOT NULL,
          value text,
          new_column varchar(255) DEFAULT '' NOT NULL,  // Added new column
          PRIMARY KEY  (id)
        ) {$wpdb->get_charset_collate()};";
    
        require_once( ABSPATH . 'wp-admin/includes/upgrade.php' );
        dbDelta( $sql );
    
        update_option( 'my_plugin_db_version', '1.1' );
        update_site_option( 'my_plugin_db_version', '1.1' ); //For multisite
      }
    }
    add_action( 'plugins_loaded', 'my_plugin_update_db_check' );
    • 我们用 my_plugin_db_version 选项来存储数据库版本号。
    • 每次插件加载时,检查数据库版本号是否与当前版本号一致。 如果不一致,则调用 dbDelta() 函数更新数据库表结构,并更新数据库版本号。
    • 注意,update_site_option() 对于多站点非常重要,确保所有站点都能正确更新。
  4. 处理索引: dbDelta() 对索引的处理比较特殊。 如果你需要创建或修改索引,需要在 CREATE TABLE 语句中包含完整的索引定义。 例如:

    $sql = "CREATE TABLE {$wpdb->prefix}my_plugin_data (
      id mediumint(9) NOT NULL AUTO_INCREMENT,
      time datetime DEFAULT '0000-00-00 00:00:00' NOT NULL,
      name varchar(255) NOT NULL,
      value text,
      PRIMARY KEY  (id),
      KEY name (name)  -- Add index on 'name' column
    ) {$wpdb->get_charset_collate()};";

    如果需要删除索引,需要手动执行 DROP INDEX 语句。

  5. 错误处理: 始终检查 $wpdb->last_error 变量,以确保 SQL 语句执行成功。 如果出现错误,记录错误信息并采取相应的措施。

    global $wpdb;
    dbDelta( $sql );
    if ( ! empty( $wpdb->last_error ) ) {
      error_log( 'My Plugin: Database error - ' . $wpdb->last_error );
    }
  6. 多条SQL语句: 强烈建议将多条创建/更新表的SQL语句放到一个字符串数组中,然后一次性传递给dbDelta(), 这样可以减少数据库连接次数,提高性能。

    $sql = array();
    $sql[] = "CREATE TABLE {$wpdb->prefix}my_plugin_table1 ( ... ) {$wpdb->get_charset_collate()};";
    $sql[] = "CREATE TABLE {$wpdb->prefix}my_plugin_table2 ( ... ) {$wpdb->get_charset_collate()};";
    
    require_once( ABSPATH . 'wp-admin/includes/upgrade.php' );
    dbDelta( $sql );
  7. 小心TEXT和BLOB类型: 对于TEXTBLOB类型的字段,dbDelta() 可能无法正确检测到长度变化。 因此,尽量避免修改这些字段的长度。 如果必须修改,建议手动执行 ALTER TABLE 语句。

dbDelta() 的局限性

虽然 dbDelta() 非常强大,但它也有一些局限性:

  • 不支持复杂的 SQL 语句: dbDelta() 主要用于创建和更新表结构,不支持复杂的 SQL 语句,例如存储过程、触发器等等。
  • 索引处理有限: 如前所述,dbDelta() 对索引的处理比较特殊,需要特别注意。
  • 无法回滚: 如果 dbDelta() 在执行过程中出现错误,无法自动回滚。 因此,在执行 dbDelta() 之前,最好备份数据库。

案例分析:插件数据库表升级

假设你的插件需要从 1.0 版本升级到 1.1 版本。 在 1.1 版本中,你需要添加一个新的字段 email{$wpdb->prefix}my_plugin_data 表中。

以下是升级代码:

function my_plugin_update_db_check() {
  $current_db_version = get_site_option( 'my_plugin_db_version', '1.0' ); // Default to 1.0 if not set

  if ( version_compare( $current_db_version, '1.1', '<' ) ) {
    global $wpdb;
    $table_name = $wpdb->prefix . 'my_plugin_data';
    $charset_collate = $wpdb->get_charset_collate();

    $sql = "CREATE TABLE IF NOT EXISTS $table_name (
      id mediumint(9) NOT NULL AUTO_INCREMENT,
      time datetime DEFAULT '0000-00-00 00:00:00' NOT NULL,
      name varchar(255) NOT NULL,
      email varchar(255) DEFAULT '' NOT NULL,  -- Added new 'email' column
      value text,
      PRIMARY KEY  (id)
    ) $charset_collate;";

    require_once( ABSPATH . 'wp-admin/includes/upgrade.php' );
    dbDelta( $sql );

    update_site_option( 'my_plugin_db_version', '1.1' );
  }
}
add_action( 'plugins_loaded', 'my_plugin_update_db_check' );

总结

dbDelta() 是 WordPress 插件开发中一个非常有用的函数,它可以帮助你安全高效地创建和更新数据库表。 但是,在使用 dbDelta() 时,需要注意一些最佳实践和局限性,以确保数据库操作的正确性和可靠性。

希望今天的讲座对你有所帮助。 记住,dbDelta() 并不是万能的,但它是你插件开发工具箱中不可或缺的一员。 熟练掌握它,你的插件就能更上一层楼!

Q&A 环节

现在,欢迎大家提问,我会尽力解答。 另外,课后我会把今天讲座的示例代码上传到 GitHub,供大家参考。 谢谢大家!

发表回复

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