# 数据库跨库迁移操作手册(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,先核对计划) 只连源、逐表数行数、打印迁移计划,**不碰目标库**: ```bash 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`(先删目标库同名库再重建,**幂等可重跑**): ```bash 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 双重加盐,原样搬): ```sql SELECT id, username, password, salt FROM fastapi_user ORDER BY id LIMIT 5; ``` 3. **自增序列已推进**(新插入不带 id 不应主键冲突): ```sql 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` 可空): ```sql \d fastapi_city -- parent_code 应为 可空(nullable) ``` 5. **树形引用完整**(父子都在): ```sql 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): ```sql SELECT name FROM fastapi_city WHERE id = 1; -- 中文正常 SELECT LENGTH(content) FROM fastapi_article ORDER BY id DESC LIMIT 3; -- 富文本长度合理 ``` --- ## 5. 切换应用指向目标库 1. 改项目根 `.env`: ```ini 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): ```bash 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**(类型/索引/外键自动处理,并行加载): ```bash # 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 目标仍用自研泵。 两种方式可任选其一,不要混跑同一目标库。