跳到內容

數據庫遷移

數據庫遷移提供版本控制、可逆的數據庫架構更改。它們確保開發、暫存和生產環境之間的一致數據庫狀態。

migrations/
├── 001_create_articles_table.php
├── 002_add_status_column.php
├── 003_create_categories_table.php
├── 004_add_indexes.php
└── 005_add_foreign_keys.php
migrations/001_create_articles_table.php
<?php
declare(strict_types=1);
return new class {
public function up(\XoopsDatabase $db): void
{
$table = $db->prefix('mymodule_articles');
$sql = "CREATE TABLE IF NOT EXISTS `{$table}` (
`id` VARCHAR(26) NOT NULL COMMENT 'ULID identifier',
`title` VARCHAR(255) NOT NULL,
`content` MEDIUMTEXT,
`status` ENUM('draft', 'published', 'archived') DEFAULT 'draft',
`author_id` INT UNSIGNED NOT NULL,
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
`updated_at` DATETIME DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
KEY `idx_status` (`status`),
KEY `idx_author` (`author_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci";
$db->queryF($sql);
}
public function down(\XoopsDatabase $db): void
{
$table = $db->prefix('mymodule_articles');
$db->queryF("DROP TABLE IF EXISTS `{$table}`");
}
};
public function up(\XoopsDatabase $db): void
{
$table = $db->prefix('mymodule_articles');
// 添加單列
$db->queryF("ALTER TABLE `{$table}` ADD COLUMN `views` INT UNSIGNED DEFAULT 0 AFTER `status`");
// 添加多列
$db->queryF("ALTER TABLE `{$table}`
ADD COLUMN `summary` TEXT AFTER `content`,
ADD COLUMN `featured` TINYINT(1) DEFAULT 0 AFTER `views`");
}
public function down(\XoopsDatabase $db): void
{
$table = $db->prefix('mymodule_articles');
$db->queryF("ALTER TABLE `{$table}` DROP COLUMN `views`, DROP COLUMN `summary`, DROP COLUMN `featured`");
}
public function up(\XoopsDatabase $db): void
{
$table = $db->prefix('mymodule_articles');
// 更改列類型
$db->queryF("ALTER TABLE `{$table}` MODIFY COLUMN `title` VARCHAR(500) NOT NULL");
// 重命名列
$db->queryF("ALTER TABLE `{$table}` CHANGE `summary` `excerpt` TEXT");
}
public function up(\XoopsDatabase $db): void
{
$table = $db->prefix('mymodule_articles');
// 單列索引
$db->queryF("CREATE INDEX `idx_created` ON `{$table}` (`created_at`)");
// 複合索引
$db->queryF("CREATE INDEX `idx_status_date` ON `{$table}` (`status`, `created_at`)");
// 唯一索引
$db->queryF("CREATE UNIQUE INDEX `idx_slug` ON `{$table}` (`slug`)");
// 全文索引
$db->queryF("CREATE FULLTEXT INDEX `idx_search` ON `{$table}` (`title`, `content`)");
}
public function down(\XoopsDatabase $db): void
{
$table = $db->prefix('mymodule_articles');
$db->queryF("DROP INDEX `idx_created` ON `{$table}`");
$db->queryF("DROP INDEX `idx_status_date` ON `{$table}`");
$db->queryF("DROP INDEX `idx_slug` ON `{$table}`");
$db->queryF("DROP INDEX `idx_search` ON `{$table}`");
}
public function up(\XoopsDatabase $db): void
{
$articles = $db->prefix('mymodule_articles');
$categories = $db->prefix('mymodule_categories');
$db->queryF("ALTER TABLE `{$articles}`
ADD CONSTRAINT `fk_article_category`
FOREIGN KEY (`category_id`) REFERENCES `{$categories}` (`id`)
ON DELETE SET NULL ON UPDATE CASCADE");
}
public function down(\XoopsDatabase $db): void
{
$articles = $db->prefix('mymodule_articles');
$db->queryF("ALTER TABLE `{$articles}` DROP FOREIGN KEY `fk_article_category`");
}
include/onupdate.php
function xoops_module_update_mymodule(\XoopsModule $module, $previousVersion)
{
$db = \XoopsDatabaseFactory::getDatabaseConnection();
$migrator = new MigrationRunner($db, $module->dirname());
try {
$migrator->migrate();
return true;
} catch (\Exception $e) {
$module->setErrors($e->getMessage());
return false;
}
}
class MigrationRunner
{
private string $migrationsPath;
private string $table;
public function __construct(
private \XoopsDatabase $db,
private string $moduleName
) {
$this->migrationsPath = XOOPS_ROOT_PATH . "/modules/{$moduleName}/migrations";
$this->table = $db->prefix("{$moduleName}_migrations");
}
public function migrate(): void
{
$this->createMigrationsTable();
$executed = $this->getExecutedMigrations();
foreach ($this->getPendingMigrations($executed) as $file) {
$this->runMigration($file);
}
}
private function runMigration(string $file): void
{
$migration = require $this->migrationsPath . '/' . $file;
$migration->up($this->db);
$this->db->queryF(
"INSERT INTO `{$this->table}` (migration, executed_at) VALUES (?, NOW())",
[$file]
);
}
public function rollback(int $steps = 1): void
{
$migrations = $this->getExecutedMigrations();
$toRollback = array_slice(array_reverse($migrations), 0, $steps);
foreach ($toRollback as $file) {
$migration = require $this->migrationsPath . '/' . $file;
$migration->down($this->db);
$this->db->queryF(
"DELETE FROM `{$this->table}` WHERE migration = ?",
[$file]
);
}
}
}
  1. 一個更改一個遷移 - 保持遷移專注
  2. 始終編寫 Down 方法 - 啟用回滾
  3. 雙向測試 - 驗證 up() 和 down()
  4. 使用事務 - 包裝複雜遷移
  5. 不修改舊遷移 - 創建新的代替
  6. 備份再運行 - 特別是在生產環境中
  • Database-Schema - 架構設計
  • Database-Operations - 查詢執行
  • ../xoops_version.php - 模塊清單
  • ../../07-XOOPS-4.0/XOOPS-4.0-Architecture - 現代架構