Instatic数据库迁移最佳实践:规划与执行
Instatic数据库迁移最佳实践:规划与执行
【免费下载链接】InstaticInstatic is a modern self-hosted visual CMS - get it running in 1 minute项目地址: https://gitcode.com/GitHub_Trending/in/Instatic
Instatic作为现代自托管可视化CMS,其数据库迁移系统是保障生产环境稳定性的核心组件。本文为您提供Instatic数据库迁移的完整指南,涵盖从规划到执行的每个关键步骤。
为什么数据库迁移如此重要?🚀
Instatic支持PostgreSQL和SQLite双数据库引擎,这一独特设计意味着每个数据库变更都需要在两个平台上保持完全一致。更重要的是,Instatic已有真实用户的生产环境部署,这意味着:
- 零停机迁移:迁移必须在运行时安全执行
- 数据完整性:用户内容不能丢失或损坏
- 向后兼容:新版本必须与现有数据无缝协作
- 跨平台一致性:PostgreSQL和SQLite必须保持相同语义
Instatic迁移架构揭秘
Instatic的迁移系统基于三个核心规则:
规则1:仓库层使用ANSI标准SQL
所有server/repositories/中的代码必须使用跨数据库兼容的SQL语句。这意味着禁止使用PostgreSQL特有的语法:
-- ❌ 禁止:PostgreSQL特有语法 SELECT DISTINCT ON (user_id) * FROM users ORDER BY created_at DESC; -- ✅ 推荐:跨平台兼容语法 SELECT user_id, created_at FROM ( SELECT *, ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY created_at DESC) as rn FROM users ) ranked WHERE rn = 1;规则2:JSON列以_json结尾
这是Instatic的关键约定,让数据库适配器自动处理JSON序列化:
// 自动处理JSON转换 await db`UPDATE site SET settings_json = ${settingsObject} WHERE id = ${id}`; // 读取时自动解析 const { rows } = await db<SiteRow>`SELECT id, settings_json FROM site`; // rows[0].settings_json 已经是JavaScript对象规则3:双数据库迁移文件
每个迁移必须在两个文件中定义相同语义:
server/db/migrations-pg.ts- PostgreSQL迁移server/db/migrations-sqlite.ts- SQLite迁移
两个文件必须包含相同的ID和顺序,确保迁移历史完全一致。
迁移生命周期管理
1. 规划阶段:风险评估
在编写任何迁移代码之前,请考虑:
- 影响范围:哪些表会被修改?
- 数据量:迁移需要处理多少行数据?
- 依赖关系:是否存在外键约束?
- 回滚策略:如果迁移失败,如何恢复?
2. 开发阶段:编写安全迁移
添加新表示例:
在PostgreSQL迁移文件中:
{ id: '0042-add-subscribers', label: '添加订阅者表', sql: ` CREATE TABLE subscribers ( id TEXT PRIMARY KEY, email TEXT NOT NULL UNIQUE, metadata_json JSONB NOT NULL DEFAULT '{}', created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP ); `, },在SQLite迁移文件中:
{ id: '0042-add-subscribers', label: '添加订阅者表', sql: ` CREATE TABLE subscribers ( id TEXT PRIMARY KEY, email TEXT NOT NULL UNIQUE, metadata_json TEXT NOT NULL DEFAULT '{}', created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP ); `, },3. 处理SQLite约束限制
SQLite不支持ALTER TABLE DROP CONSTRAINT,需要使用表重建模式:
-- PostgreSQL:直接删除约束 ALTER TABLE my_table DROP CONSTRAINT IF EXISTS my_constraint_name; -- SQLite:表重建模式 PRAGMA defer_foreign_keys = on; CREATE TABLE my_table__migr042 ( id TEXT PRIMARY KEY, -- 新表结构 ); INSERT INTO my_table__migr042 (id, col_a, col_b) SELECT id, col_a, col_b FROM my_table; DROP TABLE my_table; ALTER TABLE my_table__migr042 RENAME TO my_table; -- 重新创建索引 CREATE UNIQUE INDEX IF NOT EXISTS my_table_unique_idx ON my_table (col_a, col_b);4. 外键处理的特殊场景
当父表被ON DELETE RESTRICT引用时,需要特殊处理:
{ id: '017_layouts_system_table', label: '扩展data_tables的kind检查约束', disableForeignKeys: true, // SQLite专用标志 sql: `...表重建SQL...`, },disableForeignKeys标志告诉迁移运行器在执行此迁移时临时禁用外键约束,迁移完成后验证完整性。
迁移执行流程
Instatic的迁移运行器server/db/runMigrations.ts采用以下安全流程:
// 1. 检查迁移是否已应用 const { rows } = await db`SELECT id FROM schema_migrations WHERE id = ${migration.id}`; if (rows.length > 0) continue; // 跳过已应用的迁移 // 2. 在事务中执行迁移 await db.transaction(async (tx) => { await tx.unsafe(migration.sql); await tx`INSERT INTO schema_migrations (id) VALUES (${migration.id})`; }); // 3. 验证外键完整性(如果禁用过外键) const { rows: violations } = await db`PRAGMA foreign_key_check`; if (violations.length > 0) { throw new Error(`迁移 ${migration.id} 遗留外键违规`); }最佳实践清单 ✅
必须遵守的规则
- 始终添加迁移:不要直接修改现有表结构
- 双数据库同步:每个迁移必须在两个文件中定义
- 使用事务:确保迁移的原子性
- 保持向后兼容:新增列使用
DEFAULT值或允许NULL - 测试迁移脚本:在生产环境运行前充分测试
迁移命名规范
- 前缀编号:
001_、002_等,确保顺序执行 - 描述性名称:
add_user_avatar_column、create_audit_log_table - 小写字母:使用下划线分隔单词
- 动词开头:
add_、create_、drop_、rename_
数据迁移策略
软删除模式:
-- 添加deleted_at列而不是直接删除 ALTER TABLE users ADD COLUMN deleted_at TIMESTAMPTZ; -- 查询时过滤已删除记录 SELECT * FROM users WHERE deleted_at IS NULL;批量数据迁移:
// 分批次处理大数据量迁移 const BATCH_SIZE = 1000; let offset = 0; while (true) { const { rows } = await db<OldRow>` SELECT * FROM old_table LIMIT ${BATCH_SIZE} OFFSET ${offset} `; if (rows.length === 0) break; await db.transaction(async (tx) => { for (const row of rows) { await tx`INSERT INTO new_table (...) VALUES (...)`; } }); offset += BATCH_SIZE; }常见陷阱与解决方案 ⚠️
陷阱1:忽略SQLite的DDL限制
问题:SQLite不支持ALTER TABLE ADD COLUMN IF NOT EXISTS解决方案:依赖schema_migrations表确保迁移只运行一次
陷阱2:忘记JSON列后缀
问题:JSON列未以_json结尾,导致适配器无法自动解析解决方案:严格遵守命名约定,使用*_json后缀
陷阱3:迁移顺序依赖
问题:迁移B依赖迁移A创建的表,但执行顺序错误解决方案:使用数字前缀确保正确顺序,如001_、002_
陷阱4:生产环境数据量过大
问题:迁移耗时过长导致服务中断解决方案:
- 使用分批处理
- 在低峰期执行
- 监控迁移进度
- 准备回滚计划
测试与验证 🔧
本地测试流程
创建测试数据库:
DATABASE_URL=sqlite:./test.db bun run dev运行迁移测试:
bun test src/__tests__/architecture/migration-parity.test.ts bun test src/__tests__/architecture/db-json-column-naming.test.ts验证跨平台兼容性:
# 使用PostgreSQL测试 DATABASE_URL=postgres://localhost:5432/instatic_test bun test # 使用SQLite测试 DATABASE_URL=sqlite:./test.db bun test
生产前检查清单
- 迁移在两个数据库文件中都有定义
- 迁移ID在两个文件中相同
- JSON列使用
_json后缀 - 没有使用PostgreSQL特有语法
- 外键约束正确处理
- 迁移脚本在事务中执行
- 有适当的数据验证逻辑
紧急情况处理 🚨
迁移失败时
如果迁移在生产环境失败:
- 立即停止服务:防止数据不一致
- 检查错误日志:
server/db/runMigrations.ts会记录详细错误 - 手动回滚:如果迁移在事务中,会自动回滚
- 修复问题:根据错误信息修正迁移脚本
- 重新部署:修复后重新启动服务
数据损坏恢复
Instatic的迁移系统设计确保了数据安全:
- 事务保护:每个迁移在事务中执行
- 外键验证:迁移后验证数据完整性
- 增量迁移:每次只执行一个迁移,便于排查
高级迁移模式
模式演化模式
当需要重大架构变更时:
// 阶段1:添加新结构 { id: '020_add_new_schema', sql: `CREATE TABLE new_table (...);`, }, // 阶段2:数据迁移 { id: '021_migrate_data', sql: ` INSERT INTO new_table (...) SELECT ... FROM old_table WHERE condition; `, }, // 阶段3:切换读写(应用层) // 阶段4:清理旧数据(可选) { id: '022_drop_old_table', sql: `DROP TABLE old_table;`, },零停机迁移技巧
- 双写策略:同时写入新旧表
- 数据同步:使用触发器或应用层同步
- 逐步切换:按用户或功能逐步迁移
- 验证阶段:并行运行验证脚本
总结与建议
Instatic的数据库迁移系统经过精心设计,支持生产环境的零停机升级。关键要点:
- 始终使用事务:确保迁移的原子性
- 保持双数据库同步:PostgreSQL和SQLite迁移必须一致
- 遵守命名约定:JSON列使用
_json后缀 - 测试充分:在生产前充分测试迁移脚本
- 监控执行:记录迁移执行时间和结果
通过遵循这些最佳实践,您可以确保Instatic数据库迁移的平滑、安全执行,为用户提供无缝的升级体验。
记住:每个迁移都是对生产数据的直接操作,谨慎规划、充分测试、逐步执行是成功的关键。Instatic的架构已经为您提供了强大的工具,正确使用它们将确保您的CMS系统始终稳定可靠。
【免费下载链接】InstaticInstatic is a modern self-hosted visual CMS - get it running in 1 minute项目地址: https://gitcode.com/GitHub_Trending/in/Instatic
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考