From b66f3452812f46826b96aee24a600a7db6f69f1d Mon Sep 17 00:00:00 2001 From: Jimmy Xue Date: Sat, 6 Dec 2025 22:03:57 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E8=BF=81=E7=A7=BB=E6=97=B6,?= =?UTF-8?q?=E6=95=B0=E6=8D=AE=E5=BA=93=E6=9C=AA=E6=8C=87=E5=AE=9A=E7=9A=84?= =?UTF-8?q?=E6=83=85=E5=86=B5=E4=B8=8B=E6=95=B0=E6=8D=AE=E5=BA=93=E8=84=9A?= =?UTF-8?q?=E6=9C=AC=E6=B7=B7=E4=B9=B1=E7=9A=84=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- MIGRATION.md | 35 ++++---- docs/migration.md | 2 +- examples/migrations/README.md | 15 ++-- factory/factory.go | 16 +++- migration/helper.go | 29 +++---- migration/migration.go | 156 +++++++++++++++++++++++++++++----- templates/migrate/main.go | 18 ++-- 7 files changed, 194 insertions(+), 77 deletions(-) diff --git a/MIGRATION.md b/MIGRATION.md index 8e50433..a9bb4c3 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -105,14 +105,10 @@ go build -o bin/migrate cmd/migrate/main.go } ``` -#### 方式2:环境变量(推荐生产环境) +#### 方式2:环境变量指定配置文件路径(推荐生产环境) ```bash -# 使用 DATABASE_URL(最简单) -export DATABASE_URL="mysql://root:password@localhost:3306/mydb" -./bin/migrate up - -# 覆盖配置路径 +# 使用环境变量指定配置文件路径 export CONFIG_FILE="/etc/app/config.json" export MIGRATIONS_DIR="/opt/app/migrations" ./bin/migrate up @@ -122,8 +118,7 @@ export MIGRATIONS_DIR="/opt/app/migrations" 1. 命令行参数 `-config` 和 `-dir`(最高) 2. 环境变量 `CONFIG_FILE` 和 `MIGRATIONS_DIR` -3. 环境变量 `DATABASE_URL` -4. 默认值 `config.json` 和 `migrations` +3. 默认值 `config.json` 和 `migrations` --- @@ -185,20 +180,20 @@ services: docker-compose exec app ./migrate up ``` -### 方式3:使用环境变量 +### 方式3:使用环境变量指定配置文件路径 -无需配置文件(适用于简单场景): +适用于多环境部署,通过环境变量指定不同环境的配置文件: ```yaml services: app: build: . environment: - DATABASE_URL: mysql://root:password@db:3306/mydb + CONFIG_FILE: /app/config.prod.json + MIGRATIONS_DIR: /app/migrations + volumes: + - ./config.prod.json:/app/config.prod.json:ro command: sh -c "./migrate up && ./server" - -# 注意:DATABASE_URL 的值应该指向你的数据库服务 -# 例如:mysql://user:pass@your-db-host:3306/dbname ``` ### Dockerfile @@ -336,10 +331,12 @@ jobs: go build -o bin/migrate cmd/migrate/main.go go build -o bin/server cmd/server/main.go + - name: Create Config File + run: | + echo '${{ secrets.CONFIG_JSON }}' > config.json + - name: Run Migrations - env: - DATABASE_URL: ${{ secrets.DATABASE_URL }} - run: ./bin/migrate up + run: ./bin/migrate up -config config.json - name: Deploy run: ./bin/server @@ -500,11 +497,11 @@ go build -o bin/migrate cmd/migrate/main.go ### 3. 生产环境 - 编译后部署,先执行迁移再启动应用 -- 使用环境变量管理敏感信息 +- 使用配置文件管理敏感信息 ```bash go build -o bin/migrate cmd/migrate/main.go -DATABASE_URL="mysql://..." ./bin/migrate up +./bin/migrate up -config config.json ./bin/server ``` diff --git a/docs/migration.md b/docs/migration.md index dec09c8..1bc1d9c 100644 --- a/docs/migration.md +++ b/docs/migration.md @@ -89,7 +89,7 @@ err := migration.RunMigrationsFromConfigWithCommand("config.json", "migrations", ``` **参数说明**: -- `configFile`: 配置文件路径,空字符串时自动查找(config.json, ../config.json)或使用环境变量 DATABASE_URL +- `configFile`: 配置文件路径,空字符串时自动查找(config.json, ../config.json) - `migrationsDir`: 迁移文件目录,空字符串时使用默认值 "migrations" - `command`: 命令,支持 "up", "down", "status" diff --git a/examples/migrations/README.md b/examples/migrations/README.md index 0fc2251..59bdd1f 100644 --- a/examples/migrations/README.md +++ b/examples/migrations/README.md @@ -110,10 +110,14 @@ func main() { } ``` -### 方式2:环境变量(Docker友好) +### 方式2:使用配置文件(推荐) ```bash -DATABASE_URL="mysql://root:password@localhost:3306/mydb" go run migrate.go up +# 使用默认配置文件 config.json +go run migrate.go up + +# 或指定配置文件路径 +go run migrate.go up -config /path/to/config.json ``` **Docker 中**: @@ -121,12 +125,13 @@ DATABASE_URL="mysql://root:password@localhost:3306/mydb" go run migrate.go up # docker-compose.yml services: app: - environment: - DATABASE_URL: mysql://root:password@db:3306/mydb + volumes: + # 挂载配置文件 + - ./config.json:/app/config.json:ro command: sh -c "go run migrate.go up && ./app" ``` -**注意**:Docker 中使用服务名(`db`),不是 `localhost` +**注意**:配置文件中的数据库主机应使用服务名(`db`),不是 `localhost` ## 更多信息 diff --git a/factory/factory.go b/factory/factory.go index 2a9074e..6ced0ab 100644 --- a/factory/factory.go +++ b/factory/factory.go @@ -789,8 +789,12 @@ func (f *Factory) RunMigrations(migrationsDir string) error { return fmt.Errorf("failed to get database: %w", err) } - // 创建迁移器 - migrator := migration.NewMigrator(db) + // 创建迁移器(传入数据库类型,性能更好) + dbType := "mysql" // 默认值 + if f.cfg.Database != nil && f.cfg.Database.Type != "" { + dbType = f.cfg.Database.Type + } + migrator := migration.NewMigratorWithType(db, dbType) // 自动发现并加载迁移文件 migrations, err := migration.LoadMigrationsFromFiles(migrationsDir, "*.sql") @@ -835,8 +839,12 @@ func (f *Factory) GetMigrationStatus(migrationsDir string) ([]migration.Migratio return nil, fmt.Errorf("failed to get database: %w", err) } - // 创建迁移器 - migrator := migration.NewMigrator(db) + // 创建迁移器(传入数据库类型,性能更好) + dbType := "mysql" // 默认值 + if f.cfg.Database != nil && f.cfg.Database.Type != "" { + dbType = f.cfg.Database.Type + } + migrator := migration.NewMigratorWithType(db, dbType) // 加载迁移文件 migrations, err := migration.LoadMigrationsFromFiles(migrationsDir, "*.sql") diff --git a/migration/helper.go b/migration/helper.go index ffb23f3..abe9406 100644 --- a/migration/helper.go +++ b/migration/helper.go @@ -34,14 +34,14 @@ func RunMigrationsFromConfig(configFile, migrationsDir string) error { // RunMigrationsFromConfigWithCommand 从配置文件运行迁移(支持命令,黑盒模式) // // 这是最简单的迁移方式,内部自动处理: -// - 配置加载(支持文件、环境变量、默认路径) +// - 配置加载(支持配置文件、默认路径) // - 数据库连接(自动识别数据库类型) // - 迁移文件加载和执行 // // 参数: // - configFile: 配置文件路径,支持: // - 空字符串:自动查找(config.json, ../config.json) -// - 环境变量 DATABASE_URL:直接使用数据库URL +// - 相对路径或绝对路径:指定配置文件路径 // - migrationsDir: 迁移文件目录,支持: // - 空字符串:使用默认目录 "migrations" // - 相对路径或绝对路径 @@ -57,9 +57,6 @@ func RunMigrationsFromConfig(configFile, migrationsDir string) error { // // // 指定配置和迁移目录 // migration.RunMigrationsFromConfigWithCommand("config.json", "scripts/sql", "up") -// -// // 使用环境变量 -// // DATABASE_URL="mysql://..." migration.RunMigrationsFromConfigWithCommand("", "migrations", "up") func RunMigrationsFromConfigWithCommand(configFile, migrationsDir, command string) error { // 加载配置 cfg, err := loadConfigFromFileOrEnv(configFile) @@ -78,8 +75,8 @@ func RunMigrationsFromConfigWithCommand(configFile, migrationsDir, command strin migrationsDir = "migrations" } - // 创建迁移器 - migrator := NewMigrator(db) + // 创建迁移器(传入数据库类型,性能更好) + migrator := NewMigratorWithType(db, cfg.Database.Type) // 加载迁移文件 migrations, err := LoadMigrationsFromFiles(migrationsDir, "*.sql") @@ -122,22 +119,16 @@ func RunMigrationsFromConfigWithCommand(configFile, migrationsDir, command strin return nil } -// loadConfigFromFileOrEnv 从文件或环境变量加载配置 +// loadConfigFromFileOrEnv 从配置文件加载配置 +// 支持指定配置文件路径,或自动查找默认路径 func loadConfigFromFileOrEnv(configFile string) (*config.Config, error) { - // 优先从环境变量加载 - if dbURL := os.Getenv("DATABASE_URL"); dbURL != "" { - return &config.Config{ - Database: &config.DatabaseConfig{ - DSN: dbURL, - }, - }, nil - } - - // 尝试从配置文件加载 + // 如果指定了配置文件路径,优先使用 if configFile != "" { if _, err := os.Stat(configFile); err == nil { return config.LoadFromFile(configFile) } + // 如果指定的文件不存在,返回错误 + return nil, fmt.Errorf("配置文件不存在: %s", configFile) } // 尝试默认路径 @@ -148,7 +139,7 @@ func loadConfigFromFileOrEnv(configFile string) (*config.Config, error) { } } - return nil, fmt.Errorf("未找到配置文件,也未设置环境变量 DATABASE_URL") + return nil, fmt.Errorf("未找到配置文件,请指定配置文件路径或确保存在以下文件之一: %v", defaultPaths) } // connectDB 连接数据库 diff --git a/migration/migration.go b/migration/migration.go index ca0db09..ea1dc73 100644 --- a/migration/migration.go +++ b/migration/migration.go @@ -26,6 +26,7 @@ type Migrator struct { db *gorm.DB migrations []Migration tableName string + dbType string // 数据库类型: mysql, postgres, sqlite } // NewMigrator 创建新的迁移器 @@ -41,6 +42,25 @@ func NewMigrator(db *gorm.DB, tableName ...string) *Migrator { db: db, migrations: make([]Migration, 0), tableName: table, + dbType: "", // 未指定时为空,会使用兼容模式 + } +} + +// NewMigratorWithType 创建新的迁移器(指定数据库类型,性能更好) +// db: GORM数据库连接 +// dbType: 数据库类型 ("mysql", "postgres", "sqlite") +// tableName: 存储迁移记录的表名,默认为 "schema_migrations" +func NewMigratorWithType(db *gorm.DB, dbType string, tableName ...string) *Migrator { + table := "schema_migrations" + if len(tableName) > 0 && tableName[0] != "" { + table = tableName[0] + } + + return &Migrator{ + db: db, + migrations: make([]Migration, 0), + tableName: table, + dbType: dbType, } } @@ -56,18 +76,78 @@ func (m *Migrator) AddMigrations(migrations ...Migration) { // initTable 初始化迁移记录表 func (m *Migrator) initTable() error { - // 检查表是否存在 + // 检查表是否存在(根据数据库类型使用对应的SQL,性能更好) var exists bool - err := m.db.Raw(fmt.Sprintf(` - SELECT EXISTS ( - SELECT FROM information_schema.tables - WHERE table_schema = CURRENT_SCHEMA() - AND table_name = '%s' - ) - `, m.tableName)).Scan(&exists).Error + var err error + switch m.dbType { + case "mysql": + // MySQL/MariaDB语法 + var count int64 + err = m.db.Raw(fmt.Sprintf(` + SELECT COUNT(*) FROM information_schema.tables + WHERE table_schema = DATABASE() + AND table_name = '%s' + `, m.tableName)).Scan(&count).Error + if err == nil { + exists = count > 0 + } + case "postgres": + // PostgreSQL语法 + err = m.db.Raw(fmt.Sprintf(` + SELECT EXISTS ( + SELECT 1 FROM information_schema.tables + WHERE table_schema = CURRENT_SCHEMA() + AND table_name = '%s' + ) + `, m.tableName)).Scan(&exists).Error + case "sqlite": + // SQLite语法 + var count int64 + err = m.db.Raw(fmt.Sprintf(` + SELECT COUNT(*) FROM sqlite_master + WHERE type='table' AND name='%s' + `, m.tableName)).Scan(&count).Error + if err == nil { + exists = count > 0 + } + default: + // 未指定数据库类型时,使用兼容模式(向后兼容) + // 按顺序尝试不同数据库的语法 + var count int64 + err = m.db.Raw(fmt.Sprintf(` + SELECT COUNT(*) FROM information_schema.tables + WHERE table_schema = DATABASE() + AND table_name = '%s' + `, m.tableName)).Scan(&count).Error + if err == nil && count > 0 { + exists = true + } else { + var pgExists bool + err = m.db.Raw(fmt.Sprintf(` + SELECT EXISTS ( + SELECT 1 FROM information_schema.tables + WHERE table_schema = CURRENT_SCHEMA() + AND table_name = '%s' + ) + `, m.tableName)).Scan(&pgExists).Error + if err == nil { + exists = pgExists + } else { + var sqliteCount int64 + err = m.db.Raw(fmt.Sprintf(` + SELECT COUNT(*) FROM sqlite_master + WHERE type='table' AND name='%s' + `, m.tableName)).Scan(&sqliteCount).Error + if err == nil && sqliteCount > 0 { + exists = true + } + } + } + } + + // 如果查询失败,假设表不存在,尝试创建 if err != nil { - // 如果查询失败,可能是SQLite或其他数据库,尝试直接创建 exists = false } @@ -89,19 +169,57 @@ func (m *Migrator) initTable() error { // 注意:这个检查可能在某些数据库中失败,但不影响功能 // 如果字段不存在,记录执行时间时会失败,但不影响迁移执行 var hasExecutionTime bool - checkSQL := fmt.Sprintf(` - SELECT COUNT(*) > 0 - FROM information_schema.columns - WHERE table_schema = CURRENT_SCHEMA() - AND table_name = '%s' - AND column_name = 'execution_time' - `, m.tableName) - err = m.db.Raw(checkSQL).Scan(&hasExecutionTime).Error - if err == nil && !hasExecutionTime { + var columnCount int64 + var checkErr error + + switch m.dbType { + case "mysql": + // MySQL/MariaDB语法 + checkErr = m.db.Raw(fmt.Sprintf(` + SELECT COUNT(*) + FROM information_schema.columns + WHERE table_schema = DATABASE() + AND table_name = '%s' + AND column_name = 'execution_time' + `, m.tableName)).Scan(&columnCount).Error + if checkErr == nil { + hasExecutionTime = columnCount > 0 + } + case "postgres": + // PostgreSQL语法 + checkErr = m.db.Raw(fmt.Sprintf(` + SELECT COUNT(*) + FROM information_schema.columns + WHERE table_schema = CURRENT_SCHEMA() + AND table_name = '%s' + AND column_name = 'execution_time' + `, m.tableName)).Scan(&columnCount).Error + if checkErr == nil { + hasExecutionTime = columnCount > 0 + } + case "sqlite": + // SQLite不支持information_schema,跳过检查 + hasExecutionTime = false + default: + // 兼容模式:尝试MySQL语法 + checkErr = m.db.Raw(fmt.Sprintf(` + SELECT COUNT(*) + FROM information_schema.columns + WHERE table_schema = DATABASE() + AND table_name = '%s' + AND column_name = 'execution_time' + `, m.tableName)).Scan(&columnCount).Error + if checkErr == nil { + hasExecutionTime = columnCount > 0 + } + } + + if !hasExecutionTime { // 尝试添加字段(如果失败不影响功能) + // 注意:SQLite的ALTER TABLE ADD COLUMN语法略有不同,但GORM会处理 _ = m.db.Exec(fmt.Sprintf(` ALTER TABLE %s - ADD COLUMN execution_time INT COMMENT '执行耗时(ms)' + ADD COLUMN execution_time INT `, m.tableName)) } } diff --git a/templates/migrate/main.go b/templates/migrate/main.go index 1ef6865..ad2ce80 100644 --- a/templates/migrate/main.go +++ b/templates/migrate/main.go @@ -24,11 +24,11 @@ import ( // ./migrate down # 回滚最后一个迁移 // // Docker 中使用: -// # 方式1:挂载配置文件 +// # 方式1:挂载配置文件(推荐) // docker run -v /host/config.json:/app/config.json myapp ./migrate up // -// # 方式2:使用环境变量 -// docker run -e DATABASE_URL="mysql://..." myapp ./migrate up +// # 方式2:使用环境变量指定配置文件路径 +// docker run -e CONFIG_FILE=/etc/app/config.json myapp ./migrate up // // # 方式3:指定容器内的配置文件路径 // docker run myapp ./migrate up -config /etc/app/config.json @@ -41,8 +41,7 @@ import ( // 配置优先级(从高到低): // 1. 命令行参数 -config 和 -dir // 2. 环境变量 CONFIG_FILE 和 MIGRATIONS_DIR -// 3. 环境变量 DATABASE_URL(直接连接,无需配置文件) -// 4. 默认值(config.json 和 migrations) +// 3. 默认值(config.json 和 migrations) var ( configFile string @@ -137,15 +136,14 @@ func printHelp() { fmt.Println(" # 指定配置和迁移目录") fmt.Println(" migrate up -c config.json -d db/migrations") fmt.Println() - fmt.Println(" # 使用环境变量") - fmt.Println(" DATABASE_URL='mysql://...' migrate up") + fmt.Println(" # 使用环境变量指定配置文件路径") + fmt.Println(" CONFIG_FILE=/etc/app/config.json migrate up") fmt.Println() - fmt.Println(" # Docker 中使用") + fmt.Println(" # Docker 中使用(挂载配置文件)") fmt.Println(" docker run -v /host/config.json:/app/config.json myapp migrate up") fmt.Println() fmt.Println("配置优先级(从高到低):") fmt.Println(" 1. 命令行参数 -config 和 -dir") fmt.Println(" 2. 环境变量 CONFIG_FILE 和 MIGRATIONS_DIR") - fmt.Println(" 3. 环境变量 DATABASE_URL") - fmt.Println(" 4. 默认值(config.json 和 migrations)") + fmt.Println(" 3. 默认值(config.json 和 migrations)") }