콘텐츠로 이동

데이터베이스 마이그레이션

데이터베이스 마이그레이션은 데이터베이스 스키마에 대한 버전 제어 및 되돌릴 수 있는 변경 사항을 제공합니다. 개발, 스테이징 및 프로덕션 환경 전반에서 일관된 데이터베이스 상태를 보장합니다.

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');
// Add single column
$db->queryF("ALTER TABLE `{$table}` ADD COLUMN `views` INT UNSIGNED DEFAULT 0 AFTER `status`");
// Add multiple columns
$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');
// Change column type
$db->queryF("ALTER TABLE `{$table}` MODIFY COLUMN `title` VARCHAR(500) NOT NULL");
// Rename column
$db->queryF("ALTER TABLE `{$table}` CHANGE `summary` `excerpt` TEXT");
}
public function up(\XoopsDatabase $db): void
{
$table = $db->prefix('mymodule_articles');
// Single column index
$db->queryF("CREATE INDEX `idx_created` ON `{$table}` (`created_at`)");
// Composite index
$db->queryF("CREATE INDEX `idx_status_date` ON `{$table}` (`status`, `created_at`)");
// Unique index
$db->queryF("CREATE UNIQUE INDEX `idx_slug` ON `{$table}` (`slug`)");
// Fulltext index
$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. 항상 메소드 기록 - 롤백 활성화
  3. 양방향 테스트 - up() 및 down() 확인
  4. 트랜잭션 사용 - 복잡한 마이그레이션 래핑
  5. 이전 마이그레이션을 수정하지 마세요 - 대신 새 마이그레이션을 만드세요.
  6. 실행 전 백업 - 특히 프로덕션 환경에서
  • 데이터베이스-스키마 - 스키마 설계
  • 데이터베이스 작업 - 쿼리 실행 -../xoops_version.php - 모듈 매니페스트 -../../07-XOOPS-4.0/XOOPS-4.0-Architecture - 현대 건축