fix: sqlite migrate v3
This commit is contained in:
@@ -4,7 +4,6 @@
|
||||
本次更新包含底层数据结构的重大突破,将所有数据的 ID 类型从数字编号平滑迁移为20位的字符式全局唯一标识符(`xid`)。系统在启动时会**自动进行数据的清洗、映射、拷贝与外键修补**,以确保旧数据被妥善对接。
|
||||
- **备份位置**:执行迁移前,即使有程序自动转换逻辑(data\migration_v3_backup_backup_xxx.zip),为了数据安全,仍然建议您**提前手动进行备份**。系统的默认 SQLite 数据库文件通常位于配置的 `data/` 目录中。
|
||||
- **如果遇到失败**:由于不同用户原本的数据和环境复杂度存在差异,如果遇到未预期的迁移失败或数据显示丢失,**请使用原本备份的数据库** 并 **降级至 `v1.0.10` 及以下旧版本** 进行恢复与使用。
|
||||
- **全新启用**:对部分希望拥抱新数据结构的用户而言,也可以根据情况选择在新版本中直接重新建立配置。
|
||||
- **致谢与展望**:这次数据结构级的大幅重构,主要是为了后续项目功能扩展(含分布式管理、多租户隔离、数据同步等)的底层根基准备,再不改以后改不动了。给大家带来的使用不便敬请见谅,感谢支持!
|
||||
|
||||
## 快速部署
|
||||
|
||||
@@ -122,12 +122,23 @@ func RunMigrationV3() error {
|
||||
}
|
||||
|
||||
mappings := make(map[string]map[uint]string)
|
||||
err := db.Transaction(func(tx *gorm.DB) error {
|
||||
return performHardMigration(tx, mappings)
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
// 根据数据库类型决定是否使用事务:
|
||||
// - PostgreSQL: DDL 完全支持事务,使用事务保证原子性
|
||||
// - MySQL: DDL 会隐式提交事务,包裹事务无意义
|
||||
// - SQLite: DDL+DML 混合在 GORM 事务中会导致数据丢失
|
||||
dbType := db.Dialector.Name()
|
||||
if dbType == "postgres" {
|
||||
err := db.Transaction(func(tx *gorm.DB) error {
|
||||
return performHardMigration(tx, mappings)
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
// SQLite / MySQL: 不使用事务包裹
|
||||
if err := performHardMigration(db, mappings); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 标记成功
|
||||
@@ -153,33 +164,36 @@ func markMigrationSuccess(db *gorm.DB) error {
|
||||
return db.Model(&flag).Update("value", "true").Error
|
||||
}
|
||||
|
||||
func performHardMigration(tx *gorm.DB, mappings map[string]map[uint]string) error {
|
||||
func performHardMigration(db *gorm.DB, mappings map[string]map[uint]string) error {
|
||||
allTables := getMigrationTables()
|
||||
|
||||
// ---------------------------------------------------------
|
||||
// 第一阶段:全量构建 ID 映射映射表 (Pass 1)
|
||||
// ---------------------------------------------------------
|
||||
for _, t := range allTables {
|
||||
actualName := getTableName(tx, t.Model)
|
||||
if actualName == "" || !tx.Migrator().HasTable(actualName) {
|
||||
actualName := getTableName(db, t.Model)
|
||||
if actualName == "" || !db.Migrator().HasTable(actualName) {
|
||||
continue
|
||||
}
|
||||
mappings[t.EntityName] = make(map[uint]string)
|
||||
oldTableName := actualName + "_v2_bak"
|
||||
|
||||
// 如果还没有备份表,说明这是第一次处理该表,先重命名
|
||||
if !tx.Migrator().HasTable(oldTableName) {
|
||||
if isTableStringID(tx, t.Model) {
|
||||
if !db.Migrator().HasTable(oldTableName) {
|
||||
if isTableStringID(db, t.Model) {
|
||||
continue // 已经是字符串 ID 且无备份,跳过
|
||||
}
|
||||
if err := tx.Migrator().RenameTable(actualName, oldTableName); err != nil {
|
||||
if err := db.Migrator().RenameTable(actualName, oldTableName); err != nil {
|
||||
return fmt.Errorf("重命名表 %s 失败: %v", actualName, err)
|
||||
}
|
||||
// SQLite 重命名表后,索引名称不变,会导致 AutoMigrate 创建新表时索引冲突
|
||||
// 需要先删除备份表上的旧索引
|
||||
dropOldIndexes(db, oldTableName)
|
||||
}
|
||||
|
||||
// 预先为该表所有记录生成新的 xid
|
||||
var rows []map[string]interface{}
|
||||
tx.Table(oldTableName).Select("id").Find(&rows)
|
||||
db.Table(oldTableName).Select("id").Find(&rows)
|
||||
for _, row := range rows {
|
||||
if val, ok := getValFromMap(row, "id"); ok {
|
||||
uid := parseUint(val)
|
||||
@@ -195,27 +209,27 @@ func performHardMigration(tx *gorm.DB, mappings map[string]map[uint]string) erro
|
||||
// 第二、三阶段:正式转换数据并处理关联字段 (Pass 2 & 3)
|
||||
// ---------------------------------------------------------
|
||||
for _, t := range allTables {
|
||||
actualName := getTableName(tx, t.Model)
|
||||
actualName := getTableName(db, t.Model)
|
||||
oldTableName := actualName + "_v2_bak"
|
||||
|
||||
if !tx.Migrator().HasTable(oldTableName) {
|
||||
if !db.Migrator().HasTable(oldTableName) {
|
||||
// 虽然可能已经改过格式,但为了安全还是 AutoMigrate 一下
|
||||
tx.AutoMigrate(t.Model)
|
||||
db.AutoMigrate(t.Model)
|
||||
continue
|
||||
}
|
||||
|
||||
logger.Infof("[MigrationV3] Pass 2&3: 正在转换数据并修复关联: %s", actualName)
|
||||
tx.AutoMigrate(t.Model)
|
||||
db.AutoMigrate(t.Model)
|
||||
|
||||
// 获取新表的有效列名(小写)
|
||||
columnTypes, _ := tx.Migrator().ColumnTypes(t.Model)
|
||||
columnTypes, _ := db.Migrator().ColumnTypes(t.Model)
|
||||
validColumns := make(map[string]bool)
|
||||
for _, ct := range columnTypes {
|
||||
validColumns[strings.ToLower(ct.Name())] = true
|
||||
}
|
||||
|
||||
var oldData []map[string]interface{}
|
||||
if err := tx.Table(oldTableName).Find(&oldData).Error; err != nil {
|
||||
if err := db.Table(oldTableName).Find(&oldData).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -264,13 +278,13 @@ func performHardMigration(tx *gorm.DB, mappings map[string]map[uint]string) erro
|
||||
filteredRow[k] = v
|
||||
}
|
||||
}
|
||||
if err := tx.Table(actualName).Create(filteredRow).Error; err != nil {
|
||||
if err := db.Table(actualName).Create(filteredRow).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// 迁移完成,清理备份表
|
||||
tx.Migrator().DropTable(oldTableName)
|
||||
db.Migrator().DropTable(oldTableName)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -324,3 +338,52 @@ func transformMultiIDs(oldStr string, parentEntity string, mappings map[string]m
|
||||
}
|
||||
return strings.Join(result, ",")
|
||||
}
|
||||
|
||||
// dropOldIndexes 删除备份表上的旧索引,防止 AutoMigrate 创建新表时索引名冲突
|
||||
// SQLite 重命名表后索引名不变,MySQL/PostgreSQL 也可能存在类似问题
|
||||
func dropOldIndexes(db *gorm.DB, tableName string) {
|
||||
dbType := db.Dialector.Name()
|
||||
var indexNames []string
|
||||
|
||||
switch dbType {
|
||||
case "sqlite":
|
||||
var indexes []struct{ Name string }
|
||||
db.Raw("SELECT name FROM sqlite_master WHERE type='index' AND tbl_name=? AND name NOT LIKE 'sqlite_%'", tableName).Scan(&indexes)
|
||||
for _, idx := range indexes {
|
||||
indexNames = append(indexNames, idx.Name)
|
||||
}
|
||||
case "mysql":
|
||||
var indexes []struct{ KeyName string `gorm:"column:Key_name"` }
|
||||
db.Raw("SHOW INDEX FROM `" + tableName + "`").Scan(&indexes)
|
||||
seen := make(map[string]bool)
|
||||
for _, idx := range indexes {
|
||||
if idx.KeyName != "PRIMARY" && !seen[idx.KeyName] {
|
||||
indexNames = append(indexNames, idx.KeyName)
|
||||
seen[idx.KeyName] = true
|
||||
}
|
||||
}
|
||||
case "postgres":
|
||||
var indexes []struct{ IndexName string `gorm:"column:indexname"` }
|
||||
db.Raw("SELECT indexname FROM pg_indexes WHERE tablename=?", tableName).Scan(&indexes)
|
||||
for _, idx := range indexes {
|
||||
if !strings.HasSuffix(idx.IndexName, "_pkey") {
|
||||
indexNames = append(indexNames, idx.IndexName)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for _, name := range indexNames {
|
||||
var dropSQL string
|
||||
switch dbType {
|
||||
case "mysql":
|
||||
dropSQL = fmt.Sprintf("DROP INDEX `%s` ON `%s`", name, tableName)
|
||||
default:
|
||||
dropSQL = fmt.Sprintf("DROP INDEX IF EXISTS \"%s\"", name)
|
||||
}
|
||||
if err := db.Exec(dropSQL).Error; err != nil {
|
||||
logger.Warnf("[MigrationV3] 删除旧索引 %s 失败 (可忽略): %v", name, err)
|
||||
} else {
|
||||
logger.Infof("[MigrationV3] 已删除备份表旧索引: %s", name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user