추가: 마이그레이션 러너 및 배포 가이드
- MigrationRunner 구현 (자동 DB 마이그레이션) - Program.cs에 마이그레이션 자동 실행 추가 - 마이그레이션 SQL 파일을 임베드 리소스로 설정 - 완전한 배포 가이드 작성 (DEPLOYMENT_GUIDE.md) - E2E 테스트 절차 포함 - 롤백 및 모니터링 가이드 추가 배포 준비 완료: Gitea CI/CD 자동 배포 활성화 가능 Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,133 @@
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using Npgsql;
|
||||
|
||||
namespace TaxBaik.Infrastructure.Data;
|
||||
|
||||
public class MigrationRunner
|
||||
{
|
||||
private readonly string _connectionString;
|
||||
private readonly IDbConnectionFactory _connectionFactory;
|
||||
|
||||
public MigrationRunner(string connectionString, IDbConnectionFactory connectionFactory)
|
||||
{
|
||||
_connectionString = connectionString;
|
||||
_connectionFactory = connectionFactory;
|
||||
}
|
||||
|
||||
public async Task RunAsync()
|
||||
{
|
||||
await EnsureMigrationTableAsync();
|
||||
await ExecutePendingMigrationsAsync();
|
||||
}
|
||||
|
||||
private async Task EnsureMigrationTableAsync()
|
||||
{
|
||||
using var conn = new NpgsqlConnection(_connectionString);
|
||||
await conn.OpenAsync();
|
||||
|
||||
using var cmd = conn.CreateCommand();
|
||||
cmd.CommandText = @"
|
||||
CREATE TABLE IF NOT EXISTS schema_migrations (
|
||||
version VARCHAR(50) PRIMARY KEY,
|
||||
description VARCHAR(500),
|
||||
installed_on TIMESTAMPTZ DEFAULT NOW()
|
||||
);";
|
||||
await cmd.ExecuteNonQueryAsync();
|
||||
}
|
||||
|
||||
private async Task ExecutePendingMigrationsAsync()
|
||||
{
|
||||
var executedMigrations = await GetExecutedMigrationsAsync();
|
||||
var migrations = GetAvailableMigrations();
|
||||
|
||||
foreach (var migration in migrations.OrderBy(x => x.Version))
|
||||
{
|
||||
if (!executedMigrations.Contains(migration.Version))
|
||||
{
|
||||
await ExecuteMigrationAsync(migration);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<HashSet<string>> GetExecutedMigrationsAsync()
|
||||
{
|
||||
var executed = new HashSet<string>();
|
||||
using var conn = new NpgsqlConnection(_connectionString);
|
||||
await conn.OpenAsync();
|
||||
|
||||
using var cmd = conn.CreateCommand();
|
||||
cmd.CommandText = "SELECT version FROM schema_migrations ORDER BY version;";
|
||||
|
||||
using var reader = await cmd.ExecuteReaderAsync();
|
||||
while (await reader.ReadAsync())
|
||||
{
|
||||
executed.Add(reader.GetString(0));
|
||||
}
|
||||
|
||||
return executed;
|
||||
}
|
||||
|
||||
private List<Migration> GetAvailableMigrations()
|
||||
{
|
||||
var migrations = new List<Migration>();
|
||||
var assembly = Assembly.GetExecutingAssembly();
|
||||
var resourceNames = assembly.GetManifestResourceNames()
|
||||
.Where(x => x.Contains("Migrations") && x.EndsWith(".sql"))
|
||||
.ToList();
|
||||
|
||||
foreach (var resourceName in resourceNames.OrderBy(x => x))
|
||||
{
|
||||
var parts = resourceName.Split('.');
|
||||
var fileName = parts[parts.Length - 2];
|
||||
|
||||
if (fileName.StartsWith("V"))
|
||||
{
|
||||
var version = fileName.Substring(1, fileName.IndexOf('_') - 1);
|
||||
var description = fileName.Substring(fileName.IndexOf('_') + 2);
|
||||
|
||||
using var stream = assembly.GetManifestResourceStream(resourceName);
|
||||
using var reader = new StreamReader(stream);
|
||||
var sql = reader.ReadToEnd();
|
||||
|
||||
migrations.Add(new Migration { Version = version, Description = description, Sql = sql });
|
||||
}
|
||||
}
|
||||
|
||||
return migrations;
|
||||
}
|
||||
|
||||
private async Task ExecuteMigrationAsync(Migration migration)
|
||||
{
|
||||
using var conn = new NpgsqlConnection(_connectionString);
|
||||
await conn.OpenAsync();
|
||||
|
||||
try
|
||||
{
|
||||
using var cmd = conn.CreateCommand();
|
||||
cmd.CommandText = migration.Sql;
|
||||
await cmd.ExecuteNonQueryAsync();
|
||||
|
||||
using var insertCmd = conn.CreateCommand();
|
||||
insertCmd.CommandText =
|
||||
"INSERT INTO schema_migrations (version, description) VALUES (@version, @description);";
|
||||
insertCmd.Parameters.AddWithValue("@version", migration.Version);
|
||||
insertCmd.Parameters.AddWithValue("@description", migration.Description);
|
||||
await insertCmd.ExecuteNonQueryAsync();
|
||||
|
||||
Console.WriteLine($"✓ Migration {migration.Version} executed");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"✗ Migration {migration.Version} failed: {ex.Message}");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
private class Migration
|
||||
{
|
||||
public string Version { get; set; }
|
||||
public string Description { get; set; }
|
||||
public string Sql { get; set; }
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user