深入理解 WordPress `dbDelta()` 函数的源码:如何处理数据库表的创建和更新。

咳咳,各位!调试器准备好了吗?咱们今天要扒的可是WordPress的骨头架子——dbDelta()函数。别看名字这么希腊范儿,它干的活儿可接地气了:创建和更新数据库表。

这玩意儿的重要性,我就不多说了。没有它,你的插件、主题想搞点自定义数据存储,那就只能望表兴叹了。

开场白:为什么要有 dbDelta()

想象一下,你写了一个超酷的WordPress插件,需要创建一个新的数据库表来存储用户的自定义设置。你可能会直接写SQL语句,比如 CREATE TABLE my_plugin_settings ...

但是!问题来了。如果你的插件被安装在不同的WordPress站点上,它们的数据库版本可能不一样。有的站点可能已经存在同名的表,有的站点可能不支持你使用的某些SQL特性。更糟糕的是,如果你的插件升级了,需要修改表的结构,你怎么保证所有用户的数据库都得到正确更新?

这就是 dbDelta() 大显身手的地方了。它就像一个数据库界的“版本控制系统”,能够智能地创建、修改数据库表,并兼容不同的数据库版本。

dbDelta() 的基本用法

dbDelta() 函数位于 wp-admin/includes/upgrade.php 文件中。它的基本用法很简单:

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

$sql = "CREATE TABLE my_plugin_settings (
    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 NOT NULL,
    PRIMARY KEY  (id)
);";

dbDelta( $sql );

这段代码会检查数据库中是否已经存在 my_plugin_settings 表。如果不存在,就创建它。如果存在,dbDelta() 会比较表结构,并根据需要进行修改。

dbDelta() 的工作原理:源码剖析

好了,光说不练假把式。让我们深入 dbDelta() 的源码,看看它到底是怎么工作的。

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

    // If not running install, exit.
    if ( ! defined( 'WP_INSTALLING' ) ) {
        return;
    }

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

    $cqueries = array();

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

        $cqueries[] = $query;
    }

    $queries = $cqueries;
    unset( $cqueries );

    /**
     * Fires before database table changes are performed.
     *
     * @since 4.6.0
     *
     * @param array $queries An array of database queries being run.
     */
    do_action( 'dbdelta_queries', $queries );

    require_once( ABSPATH . 'wp-admin/includes/schema.php' );

    $current_db_version = get_option( 'db_version' );

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

        // Extract the table name from the CREATE TABLE statement.
        if ( ! preg_match( '/CREATE TABLE ([^ ]*)/', $query, $matches ) ) {
            continue;
        }

        $table_name = trim( $matches[1], '`' );

        // Check if the table already exists.
        $table_exists = $wpdb->get_var( $wpdb->prepare( "SHOW TABLES LIKE %s", $table_name ) );

        if ( ! $table_exists ) {
            // Table doesn't exist, create it.
            $wpdb->query( $query );

            // Check if there were any errors creating the table.
            if ( $wpdb->last_error ) {
                error_log( "Error creating table {$table_name}: {$wpdb->last_error}" );
            }
        } else {
            // Table exists, compare the current table schema with the new schema.
            $c = new WP_Schema();
            $c->compare_fields( $table_name, $query );
        }
    }

    update_option( 'db_version', $current_db_version );
}

我们一步步来解读这段代码:

  1. 参数处理:

    • $queries: 可以是一个包含 SQL 语句的字符串,也可以是一个字符串数组。dbDelta() 会将字符串拆分成数组,并去除空语句。
    • $execute: 一个布尔值,指示是否实际执行 SQL 语句。默认为 true
    • $query_string: 一个字符串,用于在执行查询时添加到SQL语句后面。
  2. 安全性检查:

    • if ( ! defined( 'WP_INSTALLING' ) ) { return; } 这个检查确保 dbDelta() 只在安装或升级 WordPress 时运行,避免恶意代码利用它来修改数据库。
  3. 提取表名:

    • preg_match( '/CREATE TABLE ([^ ]*)/', $query, $matches ) 使用正则表达式从 CREATE TABLE 语句中提取表名。这步非常关键,因为 dbDelta() 需要知道要操作哪个表。
  4. 检查表是否存在:

    • $table_exists = $wpdb->get_var( $wpdb->prepare( "SHOW TABLES LIKE %s", $table_name ) ) 使用 SHOW TABLES 语句检查表是否已经存在。$wpdb->prepare() 用于防止 SQL 注入攻击。
  5. 创建表:

    • if ( ! $table_exists ) { $wpdb->query( $query ); } 如果表不存在,就直接执行 CREATE TABLE 语句创建它。
  6. 更新表结构:

    • else { $c = new WP_Schema(); $c->compare_fields( $table_name, $query ); } 如果表已经存在,就使用 WP_Schema 类来比较新旧表结构,并执行必要的 ALTER TABLE 语句来更新表结构。这部分是 dbDelta() 最复杂的地方。

WP_Schema 类:数据库结构的比较与更新

WP_Schema 类位于 wp-admin/includes/schema.php 文件中。它的核心功能是比较数据库表的结构,并生成 ALTER TABLE 语句来更新表结构。

class WP_Schema {
    /**
     * Processes the SQL query for creating or updating a table.
     *
     * @since 3.0.0
     *
     * @param string $table_name The name of the table to process.
     * @param string $query      The SQL query to execute.
     * @return void
     */
    public function compare_fields( $table_name, $query ) {
        global $wpdb;

        // Get the current table schema.
        $current_table_schema = $this->get_table_schema( $table_name );

        // Parse the CREATE TABLE query.
        $new_table_schema = $this->parse_table_schema( $query );

        // Compare the schemas and generate ALTER TABLE statements.
        $alter_statements = $this->diff_table_schema( $table_name, $current_table_schema, $new_table_schema );

        // Execute the ALTER TABLE statements.
        foreach ( $alter_statements as $alter_statement ) {
            $wpdb->query( $alter_statement );
        }
    }

    /**
     * Retrieves the schema of a table.
     *
     * @since 3.0.0
     *
     * @param string $table_name The name of the table.
     * @return array An array representing the table schema.
     */
    private function get_table_schema( $table_name ) {
        global $wpdb;

        $table_schema = array();

        // Get the table columns.
        $columns = $wpdb->get_results( "SHOW COLUMNS FROM {$table_name}" );

        foreach ( $columns as $column ) {
            $table_schema[ $column->Field ] = array(
                'Type'    => $column->Type,
                'Null'    => $column->Null,
                'Key'     => $column->Key,
                'Default' => $column->Default,
                'Extra'   => $column->Extra,
            );
        }

        // Get the table indexes.
        $indexes = $wpdb->get_results( "SHOW INDEXES FROM {$table_name}" );

        foreach ( $indexes as $index ) {
            if ( 'PRIMARY' === $index->Key_name ) {
                $table_schema['PRIMARY KEY'] = $index->Column_name;
            } else {
                $table_schema['INDEX'][$index->Key_name][] = $index->Column_name;
            }
        }

        return $table_schema;
    }

    /**
     * Parses the CREATE TABLE query to extract the table schema.
     *
     * @since 3.0.0
     *
     * @param string $query The CREATE TABLE query.
     * @return array An array representing the table schema.
     */
    private function parse_table_schema( $query ) {
        $table_schema = array();

        // Regular expressions to extract table schema information.
        preg_match_all( '/`([^`]+)`s+([a-zA-Z0-9()]+)s*(NOT NULL)?s*(DEFAULTs*'?([^']+)'?s*)?(AUTO_INCREMENT)?/i', $query, $matches, PREG_SET_ORDER );

        foreach ( $matches as $match ) {
            $column_name = $match[1];
            $column_type = $match[2];
            $not_null    = ! empty( $match[3 ] );
            $default     = isset( $match[5] ) ? $match[5] : null;
            $auto_increment = ! empty( $match[6] );

            $table_schema[ $column_name ] = array(
                'Type'    => $column_type,
                'Null'    => $not_null ? 'NO' : 'YES',
                'Default' => $default,
                'Extra'   => $auto_increment ? 'auto_increment' : '',
            );
        }

        // Extract primary key information.
        preg_match( '/PRIMARY KEYs*(`([^`]+)`)/i', $query, $primary_key_match );

        if ( ! empty( $primary_key_match ) ) {
            $table_schema['PRIMARY KEY'] = $primary_key_match[1];
        }

        // Extract index information.
        preg_match_all( '/INDEXs*`([^`]+)`s*(`([^`]+)`)/i', $query, $index_matches, PREG_SET_ORDER );

        foreach ( $index_matches as $index_match ) {
            $index_name   = $index_match[1];
            $column_name = $index_match[2];

            $table_schema['INDEX'][$index_name][] = $column_name;
        }

        return $table_schema;
    }

    /**
     * Compares two table schemas and generates ALTER TABLE statements.
     *
     * @since 3.0.0
     *
     * @param string $table_name          The name of the table.
     * @param array  $current_table_schema The current table schema.
     * @param array  $new_table_schema     The new table schema.
     * @return array An array of ALTER TABLE statements.
     */
    private function diff_table_schema( $table_name, $current_table_schema, $new_table_schema ) {
        $alter_statements = array();

        // Add new columns.
        foreach ( $new_table_schema as $column_name => $column_definition ) {
            if ( 'PRIMARY KEY' === $column_name || 'INDEX' === $column_name ) {
                continue;
            }

            if ( ! isset( $current_table_schema[ $column_name ] ) ) {
                $alter_statements[] = "ALTER TABLE {$table_name} ADD COLUMN `{$column_name}` {$column_definition['Type']} " .
                                      ( 'NO' === $column_definition['Null'] ? 'NOT NULL ' : '' ) .
                                      ( isset( $column_definition['Default'] ) ? "DEFAULT '{$column_definition['Default']}' " : '' ) .
                                      ( ! empty( $column_definition['Extra'] ) ? $column_definition['Extra'] : '' );
            } else {
                // Compare existing columns.
                $current_column = $current_table_schema[ $column_name ];

                if ( $current_column['Type'] !== $column_definition['Type'] ||
                     $current_column['Null'] !== $column_definition['Null'] ||
                     $current_column['Default'] !== $column_definition['Default'] ||
                     $current_column['Extra'] !== $column_definition['Extra'] ) {

                    $alter_statements[] = "ALTER TABLE {$table_name} MODIFY COLUMN `{$column_name}` {$column_definition['Type']} " .
                                          ( 'NO' === $column_definition['Null'] ? 'NOT NULL ' : '' ) .
                                          ( isset( $column_definition['Default'] ) ? "DEFAULT '{$column_definition['Default']}' " : '' ) .
                                          ( ! empty( $column_definition['Extra'] ) ? $column_definition['Extra'] : '' );
                }
            }
        }

        // Drop removed columns.
        foreach ( $current_table_schema as $column_name => $column_definition ) {
            if ( 'PRIMARY KEY' === $column_name || 'INDEX' === $column_name ) {
                continue;
            }

            if ( ! isset( $new_table_schema[ $column_name ] ) ) {
                $alter_statements[] = "ALTER TABLE {$table_name} DROP COLUMN `{$column_name}`";
            }
        }

        // Add or drop primary key.
        if ( isset( $new_table_schema['PRIMARY KEY'] ) && ( ! isset( $current_table_schema['PRIMARY KEY'] ) || $new_table_schema['PRIMARY KEY'] !== $current_table_schema['PRIMARY KEY'] ) ) {
            if ( isset( $current_table_schema['PRIMARY KEY'] ) ) {
                $alter_statements[] = "ALTER TABLE {$table_name} DROP PRIMARY KEY";
            }

            $alter_statements[] = "ALTER TABLE {$table_name} ADD PRIMARY KEY (`{$new_table_schema['PRIMARY KEY']}`)";
        }

        // Add or drop indexes.
        if ( isset( $new_table_schema['INDEX'] ) ) {
            foreach ( $new_table_schema['INDEX'] as $index_name => $columns ) {
                if ( ! isset( $current_table_schema['INDEX'][ $index_name ] ) ) {
                    $alter_statements[] = "ALTER TABLE {$table_name} ADD INDEX `{$index_name}` (" . implode( ',', array_map( function( $column ) {
                        return "`{$column}`";
                    }, $columns ) ) . ")";
                }
            }
        }

        if ( isset( $current_table_schema['INDEX'] ) ) {
            foreach ( $current_table_schema['INDEX'] as $index_name => $columns ) {
                if ( ! isset( $new_table_schema['INDEX'][ $index_name ] ) ) {
                    $alter_statements[] = "ALTER TABLE {$table_name} DROP INDEX `{$index_name}`";
                }
            }
        }

        return $alter_statements;
    }
}

这个类的工作流程大致如下:

  1. compare_fields() 入口函数,接收表名和 CREATE TABLE 语句。
  2. get_table_schema() 使用 SHOW COLUMNSSHOW INDEXES 语句获取当前数据库表的结构。
  3. parse_table_schema() 解析 CREATE TABLE 语句,提取新表的结构信息。使用大量的正则表达式来匹配字段名、类型、是否允许为空、默认值等等。
  4. diff_table_schema() 比较新旧表结构,生成 ALTER TABLE 语句。它会检查字段的添加、删除、修改,以及主键和索引的添加和删除。

diff_table_schema() 的精髓:增删改查的艺术

diff_table_schema() 是整个 WP_Schema 类的核心。它通过比较新旧表结构,智能地生成 ALTER TABLE 语句。让我们来看一些关键的比较逻辑:

  • 添加新字段:

    if ( ! isset( $current_table_schema[ $column_name ] ) ) {
        $alter_statements[] = "ALTER TABLE {$table_name} ADD COLUMN `{$column_name}` {$column_definition['Type']} " .
                              ( 'NO' === $column_definition['Null'] ? 'NOT NULL ' : '' ) .
                              ( isset( $column_definition['Default'] ) ? "DEFAULT '{$column_definition['Default']}' " : '' ) .
                              ( ! empty( $column_definition['Extra'] ) ? $column_definition['Extra'] : '' );
    }

    如果新表结构中存在一个字段,但在旧表结构中不存在,就生成 ALTER TABLE ADD COLUMN 语句。

  • 修改现有字段:

    if ( $current_column['Type'] !== $column_definition['Type'] ||
         $current_column['Null'] !== $column_definition['Null'] ||
         $current_column['Default'] !== $column_definition['Default'] ||
         $current_column['Extra'] !== $column_definition['Extra'] ) {
    
        $alter_statements[] = "ALTER TABLE {$table_name} MODIFY COLUMN `{$column_name}` {$column_definition['Type']} " .
                              ( 'NO' === $column_definition['Null'] ? 'NOT NULL ' : '' ) .
                              ( isset( $column_definition['Default'] ) ? "DEFAULT '{$column_definition['Default']}' " : '' ) .
                              ( ! empty( $column_definition['Extra'] ) ? $column_definition['Extra'] : '' );
    }

    如果新旧表结构中都存在一个字段,但它们的类型、是否允许为空、默认值或 Extra 属性不同,就生成 ALTER TABLE MODIFY COLUMN 语句。

  • 删除字段:

    if ( ! isset( $new_table_schema[ $column_name ] ) ) {
        $alter_statements[] = "ALTER TABLE {$table_name} DROP COLUMN `{$column_name}`";
    }

    如果旧表结构中存在一个字段,但在新表结构中不存在,就生成 ALTER TABLE DROP COLUMN 语句。

  • 添加/删除主键和索引:
    diff_table_schema() 还负责比较主键和索引的差异,并生成相应的 ALTER TABLE ADD PRIMARY KEYALTER TABLE DROP PRIMARY KEYALTER TABLE ADD INDEXALTER TABLE DROP INDEX 语句。

dbDelta() 的局限性

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

  • 不支持复杂的 SQL 特性: dbDelta() 主要处理的是基本的表结构变更,对于一些复杂的 SQL 特性(比如存储过程、触发器、视图等),它可能无法正确处理。
  • 依赖于 CREATE TABLE 语句: dbDelta() 依赖于 CREATE TABLE 语句来解析表结构。如果你的表结构是通过其他方式创建的(比如使用第三方库),dbDelta() 可能无法识别。
  • 性能问题: 对于大型数据库表,dbDelta() 的性能可能会受到影响,因为它需要比较整个表结构。

最佳实践

为了充分利用 dbDelta() 的优势,并避免其局限性,以下是一些最佳实践:

  • 使用标准的 CREATE TABLE 语句: 确保你的 CREATE TABLE 语句符合 SQL 标准,并且包含所有必要的表结构信息(字段名、类型、是否允许为空、默认值、主键、索引等)。
  • 逐步更新表结构: 尽量避免一次性修改大量的表结构。最好将表结构变更分解成多个小步骤,逐步更新。
  • 备份数据库: 在运行 dbDelta() 之前,务必备份数据库。以防万一出现问题,可以快速恢复。
  • 测试: 在生产环境运行 dbDelta() 之前,务必在测试环境进行充分的测试。

代码示例:更复杂的情况

让我们来看一个更复杂的例子,演示如何使用 dbDelta() 来创建包含索引和默认值的表:

global $wpdb;
$charset_collate = $wpdb->get_charset_collate();

$table_name = $wpdb->prefix . 'my_plugin_data';

$sql = "CREATE TABLE $table_name (
  id mediumint(9) NOT NULL AUTO_INCREMENT,
  user_id bigint(20) UNSIGNED NOT NULL,
  data_key varchar(255) NOT NULL,
  data_value longtext NOT NULL,
  created_at timestamp DEFAULT CURRENT_TIMESTAMP,
  UNIQUE KEY id (id),
  KEY user_id (user_id),
  KEY data_key (data_key)
) $charset_collate;";

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

在这个例子中,我们创建了一个名为 wp_my_plugin_data 的表,包含以下字段:

  • id: 自增长的唯一 ID,作为主键。
  • user_id: 关联的用户 ID,作为外键。
  • data_key: 数据的键,用于快速查找数据。
  • data_value: 数据的值,可以是任意长度的文本。
  • created_at: 数据的创建时间,默认为当前时间戳。

我们还定义了三个索引:

  • id: 唯一索引,确保 id 字段的唯一性。
  • user_id: 普通索引,用于加速根据 user_id 查找数据的速度。
  • data_key: 普通索引,用于加速根据 data_key 查找数据的速度。

总结

dbDelta() 是 WordPress 中一个非常重要的函数,用于创建和更新数据库表。它通过比较新旧表结构,智能地生成 ALTER TABLE 语句,从而实现数据库的平滑升级。虽然 dbDelta() 有一些局限性,但只要遵循最佳实践,就可以充分利用它的优势,并避免潜在的问题。

希望今天的讲座对大家有所帮助。记住,理解源码是成为编程大师的第一步!下次再见!

发表回复

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