1 Commits

7 changed files with 194 additions and 77 deletions

View File

@@ -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
```

View File

@@ -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"

View File

@@ -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`
## 更多信息

View File

@@ -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")

View File

@@ -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 连接数据库

View File

@@ -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))
}
}

View File

@@ -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")
}