migration_system 模块技术深度解析
1. 模块概述与问题解决
migration_system 模块是 Dolt 存储后端的重要组成部分,负责安全、可靠地管理数据库 schema 演化。在长期运行的软件系统中,随着功能迭代往往伴随着数据结构变化,如何在不丢失数据的情况下升级数据库 schema 是一个经典难题。migration_system 专门解决这个问题,它提供了一套机制,确保 schema 变更可以安全、有序地应用,支持幂等操作,并与 Dolt 的版本控制特性紧密结合。
核心设计理念:
- **向前兼容性:确保 schema 变更不会破坏现有数据库
- **幂等性:迁移可以多次安全执行而不会造成问题
- **与 Dolt 集成:利用 Dolt 的版本控制特性来记录 schema 变更
2. 架构与核心抽象
核心抽象是 Migration 结构体,它代表单个 schema 迁移的封装。每个 Migration 包含两个字段:
Name:迁移的唯一标识符,用于诊断和日志Func:实际执行迁移的函数,它接收*sql.DB作为参数并返回错误
这个模块的设计采用了**线性迁移链的架构:
- 所有迁移按照添加到
migrationsList中,严格按照顺序执行 - 每个迁移都是幂等的,这意味着可以多次运行不会产生副作用
- 迁移过程与 Dolt 版本控制系统集成,确保每次迁移都有记录
数据流程图
flowchart TD
A[启动 DoltStore] --> B[RunMigrations]
B --> C{遍历 migrationsList}
C --> D[执行单个迁移]
D --> E{检查是否已应用}
E -- 未应用 --> F[应用变更]
E -- 已应用 --> G[跳过]
F --> H[提交 Dolt 提交]
G --> H
H --> I[完成]
3. 核心组件详解
Migration 结构体
type Migration struct {
Name string
Func func(*sql.DB) error
}
这个简单但强大的抽象,将迁移的名称和逻辑封装在一起。名称用于日志和调试,而函数则包含实际的迁移逻辑。关键是,每个迁移函数都必须是幂等的。
migrationsList 变量
这是一个有序的迁移列表,所有的迁移都必须按顺序添加到这里。新的迁移应该总是追加到列表的末尾,以确保正确的执行顺序。
RunMigrations 函数
func RunMigrations(db *sql.DB) error
这是模块的核心函数,它遍历 migrationsList 中的每个迁移,按顺序执行它们。执行完所有迁移后,它使用 Dolt 的 `DOLT_COMMIT 来提交 schema 变更。这个函数确保了:
- 严格的执行顺序
- 幂等执行
- 与 Dolt 版本控制集成
CreateIgnoredTables 函数
func CreateIgnoredTables(db *sql.DB) error
这个函数用于重新创建被 dolt_ignore 忽略的表(例如 wisps 和 wisp_*)。这些表只存在于工作集中,不会在分支时继承。这个函数是幂等的,可安全重复调用。
ListMigrations 函数
func ListMigrations() []string
返回所有注册迁移的名称列表,主要用于调试和诊断。
4. 迁移实现模式与 helper 函数
幂等性实现是迁移设计的关键。每个迁移函数都遵循一个标准模式:
- 首先检查迁移是否已经应用
- 如果没有应用,则执行相应的 schema 变更
- 如果已经应用,则直接返回
辅助函数(在 migrations/helpers.go 中)提供了关键的检查功能:
columnExists: 检查表中是否存在特定列tableExists: 检查数据库中是否存在特定表isTableNotFoundError: 检查错误是否为表不存在错误
这些辅助函数确保迁移可以安全地多次运行,不会造成重复操作。
5. 与其他模块的关系
migration_system 是 Dolt 存储后端的核心组成部分,被 DoltStore 在初始化时调用。它与 Dolt 的事务管理、版本控制等功能紧密配合。
依赖关系:
- 被 Dolt Storage Backend 模块的
DoltStore在初始化时调用 - 使用了 Dolt 的事务系统
- 与 Dolt 的版本控制集成
- 与 Dolt 的
dolt_ignore机制配合
6. 设计决策与权衡
设计决策:
- **线性迁移链:所有迁移按固定顺序执行,简化了依赖关系管理
- **幂等迁移:每个迁移都必须可安全多次运行,避免了状态管理的复杂性
- **与 Dolt 版本控制集成:利用 Dolt 的特性来记录和管理 schema 变更
- **没有版本表:不使用专门的版本表来追踪已应用的迁移,而是让每个迁移自己检查是否已应用
权衡与张力:
- 线性迁移链 vs 灵活的迁移依赖:线性链简化了实现,但限制了迁移的灵活性
- 无版本表 vs 有版本表:无版本表简化了架构,但每个迁移都需要自己实现检查逻辑
- Dolt 集成 vs 数据库独立性:与 Dolt 紧密集成使迁移更强大,但也使迁移系统与 Dolt 耦合
7. 使用场景与常见模式
新增迁移的步骤:
- 在
internal/storage/dolt/migrations目录下创建新的迁移文件 - 实现迁移函数,确保它是幂等的
- 在
migrationsList中添加新的Migration结构体 - 测试迁移,确保它可以多次安全运行
常见模式:
- 检查列是否存在后再添加
- 检查表是否存在后再创建
- 使用 Dolt 的事务来确保变更的原子性
- 提交变更到 Dolt 版本控制系统
8. 注意事项与常见陷阱
- 迁移顺序很重要: 新的迁移必须总是追加到
migrationsList的末尾 - 迁移必须是幂等的: 确保迁移可以多次安全运行,不会造成重复操作
- 与 Dolt 的集成: 迁移提交到 Dolt 版本控制系统,但要处理 "nothing to commit" 的情况
- 被忽略的表:
wisps和wisp_*表不会在分支时继承,需要使用CreateIgnoredTables重新创建