226 lines
10 KiB
Markdown
226 lines
10 KiB
Markdown
# 数据库跨库迁移操作手册(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 目标仍用自研泵。
|
||
两种方式可任选其一,不要混跑同一目标库。
|