咳咳,各位!调试器准备好了吗?咱们今天要扒的可是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 );
}
我们一步步来解读这段代码:
-
参数处理:
$queries
: 可以是一个包含 SQL 语句的字符串,也可以是一个字符串数组。dbDelta()
会将字符串拆分成数组,并去除空语句。$execute
: 一个布尔值,指示是否实际执行 SQL 语句。默认为true
。$query_string
: 一个字符串,用于在执行查询时添加到SQL语句后面。
-
安全性检查:
if ( ! defined( 'WP_INSTALLING' ) ) { return; }
这个检查确保dbDelta()
只在安装或升级 WordPress 时运行,避免恶意代码利用它来修改数据库。
-
提取表名:
preg_match( '/CREATE TABLE ([^ ]*)/', $query, $matches )
使用正则表达式从CREATE TABLE
语句中提取表名。这步非常关键,因为dbDelta()
需要知道要操作哪个表。
-
检查表是否存在:
$table_exists = $wpdb->get_var( $wpdb->prepare( "SHOW TABLES LIKE %s", $table_name ) )
使用SHOW TABLES
语句检查表是否已经存在。$wpdb->prepare()
用于防止 SQL 注入攻击。
-
创建表:
if ( ! $table_exists ) { $wpdb->query( $query ); }
如果表不存在,就直接执行CREATE TABLE
语句创建它。
-
更新表结构:
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;
}
}
这个类的工作流程大致如下:
compare_fields()
: 入口函数,接收表名和CREATE TABLE
语句。get_table_schema()
: 使用SHOW COLUMNS
和SHOW INDEXES
语句获取当前数据库表的结构。parse_table_schema()
: 解析CREATE TABLE
语句,提取新表的结构信息。使用大量的正则表达式来匹配字段名、类型、是否允许为空、默认值等等。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 KEY
、ALTER TABLE DROP PRIMARY KEY
、ALTER TABLE ADD INDEX
和ALTER 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()
有一些局限性,但只要遵循最佳实践,就可以充分利用它的优势,并避免潜在的问题。
希望今天的讲座对大家有所帮助。记住,理解源码是成为编程大师的第一步!下次再见!