Files
DjangoAdmin_FastAPI_EleVue/document/数据库迁移_runbook.md
T
2026-09-12 17:39:13 +08:00

10 KiB
Raw Blame History

数据库跨库迁移操作手册(MySQL → PostgreSQL / SQL Server / SQLite / Oracle)

适用范围:把本项目一个正在运行的 MySQL 库 的「结构 + 数据」整体迁到 PostgreSQL / SQL Server / SQLite / Oracle 等目标库,后续应用切换 DB_DRIVER 即可。

结构迁移由 Base.metadata.create_all()(方言可移植 DDL)完成; 数据迁移由 scripts/migrate_db.py 完成(拓扑序逐表显式带 id 批量插入, 保住无外键裸列引用关系,再按方言重置自增序列)。


0. 原理速览

migrate_db.py 做什么:

  1. 建目标库(不存在才建,按方言连维护库:MySQL 无库连接 / PG 连 postgres / MSSQL 连 master)。
  2. 建结构:Base.metadata.create_all() 按目标方言生成 DDL(模型即 head,含 city.parent_code nullable 等已落地模型定义,无需再跑 alembic)。
  3. 泵数据:按拓扑序(基础表先、依赖表后),每表 SELECT * ... ORDER BY id, 分块 executemany 显式带 id 插入目标 → 表间裸列引用(user.dept_id、 role_menu.role_id/menu_id、menu.parent_id 等)完整保留。
  4. 重置自增序列:PG setval / MSSQL IDENTITY_INSERT + DBCC CHECKIDENT / Oracle 为每表重建 sequence + BEFORE INSERT 触发器(Oracle 无原生自增); MySQL / SQLite 显式插 id 后自动跟随,无需处理。
  5. 校验:逐表 COUNT(*) 对比,不一致则退出码 1。

1. 前置条件

  • 源 MySQL 可连接、账号有读权限;目标库已安装且维护库(postgres/master)账号有建库权限; Oracle 目标需服务可达、schema 用户有 CONNECT, RESOURCE 权限。
  • 依赖齐全:pip install -r requirements.txt(PyMySQL / psycopg / pymssql / oracledb 均已内置)。
  • 项目 .env 存在且合法(脚本内部 create_app() 会做启动期安全配置校验,与 init_db.py 同约束)。
  • 建议先用 document/djangoadmin.fastapi.elevue.sql 同源数据的库做一次演练。

2. 干跑(DRY-RUN,先核对计划)

只连源、逐表数行数、打印迁移计划,不碰目标库:

make db-migrate-dry-run ARGS=" \
  --src-driver=mysql --src-host=127.0.0.1 --src-port=3306 \
  --src-db=djangoadmin.fastapi.elevue --src-user=root --src-pass=root \
  --dst-driver=postgresql --dst-host=127.0.0.1 --dst-port=5432 \
  --dst-db=djangoadmin.fastapi.elevue --dst-user=postgres --dst-pass=xxx"

输出应为每张 fastapi_* 表的行数(如 fastapi_city: 5xxx 行),核对源端行数与预期一致。

口令可改用环境变量避免进 shell 历史:SRC_DB_PASS / DST_DB_PASS。


3. 正式迁移

在干跑计划之上加 --drop-target-first(先删目标库同名库再重建,幂等可重跑):

make db-migrate ARGS=" \
  --src-driver=mysql --src-host=127.0.0.1 --src-port=3306 \
  --src-db=djangoadmin.fastapi.elevue --src-user=root --src-pass=root \
  --dst-driver=postgresql --dst-host=127.0.0.1 --dst-port=5432 \
  --dst-db=djangoadmin.fastapi.elevue --dst-user=postgres --dst-pass=xxx \
  --drop-target-first"

预期日志(阶段横幅 + 子步骤 + 逐表进度,每阶段带耗时,末尾汇总):

==============================================================
[迁移开始] 源: mysql:djangoadmin.fastapi.elevue → 目标: mssql:djangoadmin.fastapi.elevue
[迁移开始] 待迁移 23 张表
==============================================================
[·] [1/5] 建目标库...(可能耗时较长,请勿中断)
  · 正在连接 SQL Server(master 维护库)...
  · 正在删除旧目标库 [djangoadmin.fastapi.elevue](SINGLE_USER + DROP)...
  · 正在创建目标库 [djangoadmin.fastapi.elevue]...
[✓] [1/5] 建目标库 完成(耗时 947ms)
[·] [2/5] 建表结构(23 张表 create_all)...(可能耗时较长,请勿中断)
[✓] [2/5] 建表结构(23 张表 create_all) 完成(耗时 827ms),23 张表
[·] [3/5] 逐表迁移数据...(可能耗时较长,请勿中断)
  · [1/23] 正在迁移 fastapi_dept...
  [✓ fastapi_dept] 共 5 行(耗时 12ms)
  · [2/23] 正在迁移 fastapi_menu...
  [✓ fastapi_menu] 共 54 行(耗时 110ms)
  · [3/23] 正在迁移 fastapi_city...
  [fastapi_city] +1000(累计 1000)
  ...
  [✓ fastapi_city] 共 4150 行(耗时 4.2s)
  ...(其余表各一行 ✓ 汇总)
[✓] [3/5] 逐表迁移数据 完成,共 4843 行(耗时 5.7s)
[·] [4/5] 重置自增序列...(可能耗时较长,请勿中断)
[✓] [4/5] 重置自增序列 完成(耗时 113ms)  ← PostgreSQL/MSSQL/Oracle 显示实际耗时;
                                             MySQL/SQLite 显示「自动跟随,无需处理」
  [fastapi_dept] source=5 target=5 PASS
  ...
[✓] [5/5] 逐表行数校验 完成,23 张表全部 PASS(耗时 103ms)
==============================================================
[迁移完成] 成功:4843 行 / 23 张表 / 总耗时 7.1s
==============================================================

[·] = 阶段进行中(含「请勿中断」提示),[✓] = 阶段完成。失败时相应节点会标 FAIL, 例如 [✓] [5/5] 逐表行数校验 完成,FAIL N 张 并在下面列出明细、[✓] [4/5] 重置自增序列 完成,N 张失败。

退出码 0 = 成功;1 = 至少一张表 count 不一致([FAIL] 列出明细)。


4. 目标端校验清单

迁移后按需抽查(以 PostgreSQL 为例,MSSQL 用 SSMS 等价的 SQL):

  1. 行数:上一步已逐表 PASS。
  2. 密码/盐原样(bcrypt 双重加盐,原样搬):
    SELECT id, username, password, salt FROM fastapi_user ORDER BY id LIMIT 5;
    
  3. 自增序列已推进(新插入不带 id 不应主键冲突):
    SELECT last_value, is_called FROM fastapi_city_id_seq;  -- 确认 last_value >= MAX(id)
    SELECT MAX(id) FROM fastapi_city;
    INSERT INTO fastapi_city (city_code, area_code, zip_code, name, short_name, parent_id)
    VALUES ('010', '110100', '100000', '测试市', '测试', 0) RETURNING id;  -- id 应为 MAX+1
    -- 验证后删掉该测试行
    
  4. 0002 生效(city.parent_code 可空):
    \d fastapi_city    -- parent_code 应为 可空(nullable)
    
  5. 树形引用完整(父子都在):
    SELECT COUNT(*) FROM fastapi_menu m LEFT JOIN fastapi_menu p ON m.parent_id = p.id
    WHERE m.parent_id <> 0 AND p.id IS NULL;   -- 应为 0(无悬空父引用)
    
  6. CJK / Text 保真(utf8mb4 → PG UTF8):
    SELECT name FROM fastapi_city WHERE id = 1;              -- 中文正常
    SELECT LENGTH(content) FROM fastapi_article ORDER BY id DESC LIMIT 3;  -- 富文本长度合理
    

5. 切换应用指向目标库

  1. 改项目根 .env:
    DB_DRIVER='postgresql'
    DB_HOST='127.0.0.1'
    DB_PORT=5432
    DB_DATABASE='djangoadmin.fastapi.elevue'
    DB_USERNAME='postgres'
    DB_PASSWORD='xxx'
    DB_PREFIX='fastapi_'        # 必须与迁移时一致
    
  2. 重启应用:python src/main.py(或 docker-compose)。
  3. 回归验证:pytest(contract 快照守护表名/权限串/路由,integration 需真实库)。

6. 目标库补 alembic 基线(可选)

应用若走 alembic 增量迁移,在目标端把库标记到 head(幂等,create_all no-op):

DB_DRIVER=postgresql python scripts/init_db.py

init_db.py 会按当前 DB_DRIVER 连维护库判存在、create_all 补表(已是全量,无缺失)、stamp head。


7. 参数参考

参数 说明 默认
--src-driver / --dst-driver mysql/postgresql/mssql/oracle/sqlite mysql
--src-host / --dst-host 地址 127.0.0.1
--src-port / --dst-port 端口,缺省按驱动(3306/5432/1433) 驱动默认
--src-db / --dst-db 库名;oracle 时即服务名 service_name,sqlite 时即数据库文件完整路径 必填
--src-user / --dst-user 账号 root / postgres
--src-pass / --dst-pass 口令,留空回落 SRC_DB_PASS/DST_DB_PASS 空
--drop-target-first 先删目标同名库再重建(幂等重跑) off
--dry-run 只连源、逐表数行数、打印计划 off
--chunk-size 每批插入行数 1000
--only-tables 逗号分隔子集(全名或短名),续传用 全部
--skip-existing 目标已等于源行数的表跳过 off
--single-transaction 全库一个大事务(默认每表一个事务) off
--skip-verify 跳过迁移后逐表行数校验 off

8. 常见问题

  • 迁移中断:默认每表一个事务,失败只回滚当前表;修好源数据后重跑 --drop-target-first(全量)或 --only-tables=<未完成表>(续传)。
  • 重跑非幂等:不 --drop-target-first 会重复行导致 count 校验 FAIL。规范做法是 每次迁到全新目标库或加 --drop-target-first。
  • MySQL 零日期 0000-00-00:PyMySQL 默认转 None,脚本另有 sanitize 兜底, 统一落目标为 NULL;本项目时间列全部可空。
  • PG 主键冲突(新插入 id 撞已有值):序列没重置,按第 4 节第 3 步核对并手动 setval,或重跑一次迁移。
  • MSSQL:脚本对每张表自动 SET IDENTITY_INSERT ON → 插入 → OFF, 并 DBCC CHECKIDENT 重置;日志无报错即正常。
  • 目标库建库权限不足:让 DBA 预建空库后去掉 --drop-target-first 重跑 (建库分支检测已存在会跳过)。

9. 大数据量场景(可选:pgloader)

自研泵面向本项目量级(city 5k 行 + 其余小表,秒级完成)。若源库达到百万级行, MySQL → PostgreSQL 建议改用官方推荐的 pgloader(类型/索引/外键自动处理,并行加载):

# Ubuntu/Debian
sudo apt install pgloader
pgloader mysql://root:pass@127.0.0.1/djangoadmin.fastapi.elevue \
         postgresql://postgres:pass@127.0.0.1/djangoadmin.fastapi.elevue

注意:pgloader 只覆盖 MySQL → PostgreSQL 一条路径;MSSQL / SQLite 目标仍用自研泵。 两种方式可任选其一,不要混跑同一目标库。