10 KiB
数据库跨库迁移操作手册(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 做什么:
- 建目标库(不存在才建,按方言连维护库:MySQL 无库连接 / PG 连
postgres/ MSSQL 连master)。 - 建结构:
Base.metadata.create_all()按目标方言生成 DDL(模型即 head,含city.parent_code nullable等已落地模型定义,无需再跑 alembic)。 - 泵数据:按拓扑序(基础表先、依赖表后),每表
SELECT * ... ORDER BY id, 分块 executemany 显式带 id 插入目标 → 表间裸列引用(user.dept_id、role_menu.role_id/menu_id、menu.parent_id等)完整保留。 - 重置自增序列:PG
setval/ MSSQLIDENTITY_INSERT+DBCC CHECKIDENT/ Oracle 为每表重建sequence+BEFORE INSERT触发器(Oracle 无原生自增); MySQL / SQLite 显式插 id 后自动跟随,无需处理。 - 校验:逐表
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):
- 行数:上一步已逐表 PASS。
- 密码/盐原样(bcrypt 双重加盐,原样搬):
SELECT id, username, password, salt FROM fastapi_user ORDER BY id LIMIT 5; - 自增序列已推进(新插入不带 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 -- 验证后删掉该测试行 - 0002 生效(
city.parent_code可空):\d fastapi_city -- parent_code 应为 可空(nullable) - 树形引用完整(父子都在):
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(无悬空父引用) - 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. 切换应用指向目标库
- 改项目根
.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_' # 必须与迁移时一致 - 重启应用:
python src/main.py(或 docker-compose)。 - 回归验证:
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 目标仍用自研泵。 两种方式可任选其一,不要混跑同一目标库。