460 lines
16 KiB
Markdown
460 lines
16 KiB
Markdown
# 数据库迁移
|
||
|
||
## 概述
|
||
|
||
使用 Alembic 管理数据库结构迁移,同时提供跨库数据迁移脚本,支持从 MySQL 迁移到 PostgreSQL / SQL Server / SQLite / Oracle 等多种数据库。
|
||
|
||
:::tip 迁移工具链
|
||
- **结构迁移**:Alembic(自动生成 DDL 差异)
|
||
- **数据迁移**:`scripts/migrate_db.py`(跨库数据泵)
|
||
- **初始化**:`scripts/init_db.py`(全新安装一键建库)
|
||
:::
|
||
|
||
## 全新安装初始化
|
||
|
||
首次部署时,使用初始化脚本一键完成建库、建表、Alembic stamp:
|
||
|
||
```bash
|
||
# 方式一:直接运行脚本
|
||
python scripts/init_db.py
|
||
|
||
# 方式二:Make 命令
|
||
make db-init
|
||
```
|
||
|
||
:::tip 初始化流程
|
||
1. 检测并创建数据库(如不存在)
|
||
2. 执行 `Base.metadata.create_all()` 建表
|
||
3. 导入基础 SQL 数据(如有)
|
||
4. Alembic stamp head(标记当前为最新版本)
|
||
:::
|
||
|
||
## Alembic 结构迁移
|
||
|
||
### 生成迁移脚本
|
||
|
||
修改模型后,执行以下命令自动生成迁移脚本:
|
||
|
||
```bash
|
||
# 生成增量迁移脚本
|
||
alembic revision --autogenerate -m "描述变更内容"
|
||
|
||
# 或使用 Make 命令
|
||
make migrate-revision
|
||
```
|
||
|
||
:::tip 注意事项
|
||
- 执行前确保数据库连接正常(`.env` 中的数据库配置正确)
|
||
- 自动生成的脚本需人工审查,确认无误后再执行
|
||
- 迁移脚本位于 `migrations/versions/` 目录
|
||
:::
|
||
|
||
### 应用迁移
|
||
|
||
```bash
|
||
# 应用所有待执行的迁移
|
||
alembic upgrade head
|
||
|
||
# 或使用 Make 命令
|
||
make migrate-upgrade
|
||
```
|
||
|
||
### 回滚迁移
|
||
|
||
```bash
|
||
# 回滚最近一次迁移
|
||
alembic downgrade -1
|
||
|
||
# 回滚到指定版本
|
||
alembic downgrade <revision_id>
|
||
```
|
||
|
||
### 查看迁移状态
|
||
|
||
```bash
|
||
# 查看当前版本
|
||
alembic current
|
||
|
||
# 查看迁移历史
|
||
alembic history
|
||
|
||
# 查看待执行的迁移
|
||
alembic history --indicate-current
|
||
```
|
||
|
||
### 手动标记版本
|
||
|
||
对于已有数据库,手动标记为最新版本:
|
||
|
||
```bash
|
||
alembic stamp head
|
||
|
||
# 或使用 Make 命令
|
||
make migrate-stamp
|
||
```
|
||
|
||
## 跨库数据迁移
|
||
|
||
将源库(通常 MySQL)的结构和数据整体迁移到目标库(PostgreSQL / SQL Server / SQLite / Oracle)。
|
||
|
||
### 脚本说明
|
||
|
||
`scripts/migrate_db.py` 是项目内置的跨库迁移脚本,核心特性:
|
||
|
||
- **目标库结构**:用模型 metadata 生成(`Base.metadata.create_all`),方言可移植,模型即 head
|
||
- **数据泵**:按拓扑序逐表 `SELECT ... ORDER BY id`,显式带 id 批量插入,保住无外键裸列引用(`user.dept_id`、`role_menu.role_id/menu_id`、`menu.parent_id` 等)
|
||
- **自增序列重置**:迁移后按方言自动处理(详见下文)
|
||
- **进度提示**:全部耗时步骤带缓冲提示(`[·]` 请勿中断 → `[✓]` 完成),不会显得卡死
|
||
- **连接独立**:源/目标连接全部由 CLI 显式参数给出,不依赖 `.env`,也无需先切 `DB_DRIVER`
|
||
|
||
### 前置准备
|
||
|
||
1. 源库(MySQL)可正常访问
|
||
2. 目标库所需驱动已包含在 `requirements.txt`:
|
||
|
||
| 目标库 | 驱动包 | 说明 |
|
||
|:----|:----|:----|
|
||
| PostgreSQL | psycopg[binary] | psycopg3 |
|
||
| SQL Server | pymssql | - |
|
||
| SQLite | 内置 | 无需额外驱动 |
|
||
| Oracle | oracledb | thin 模式,无需安装 Oracle Client |
|
||
|
||
```bash
|
||
pip install -r requirements.txt
|
||
```
|
||
|
||
### 驱动与默认端口
|
||
|
||
| 目标库 | `--dst-driver` | 默认端口 | `--dst-db` 填什么 |
|
||
|:----|:----|:----|:----|
|
||
| MySQL | `mysql` | 3306 | 数据库名 |
|
||
| PostgreSQL | `postgresql` | 5432 | 数据库名(含点号会自动双引号) |
|
||
| SQL Server | `mssql` | 1433 | 数据库名 |
|
||
| SQLite | `sqlite` | 无端口 | SQLite 文件路径(相对/绝对皆可) |
|
||
| Oracle | `oracle` | 1521 | SERVICE_NAME(如 ORCLPDB1、XE,非库名) |
|
||
|
||
驱动别名:`postgres` / `pg` → `postgresql`;`sqlserver` / `sql_server` → `mssql`;`sqlite3` → `sqlite`
|
||
|
||
### 迁移流程
|
||
|
||
迁移分 5 个阶段自动执行:
|
||
|
||
```
|
||
1. 建目标库(create_database_if_missing)
|
||
├─ MySQL: CREATE DATABASE IF NOT EXISTS
|
||
├─ PG: 连接 postgres 维护库 → CREATE DATABASE
|
||
├─ MSSQL: 连接 master 库 → CREATE DATABASE
|
||
├─ Oracle: 不建库,只清理旧表(schema 即连接用户)
|
||
└─ SQLite: 创建文件目录 + 可选删旧文件
|
||
|
||
2. 建表结构(Base.metadata.create_all)
|
||
└─ 方言适配,自动生成对应数据库的 DDL
|
||
|
||
3. 逐表泵数据(拓扑序,分块 executemany)
|
||
└─ 显式带 id 插入,保住引用完整性
|
||
|
||
4. 重置自增序列(按方言处理)
|
||
├─ PostgreSQL: setval(seq, MAX(id))
|
||
├─ SQL Server: IDENTITY_INSERT ON + DBCC CHECKIDENT RESEED
|
||
├─ Oracle: 补 sequence + BEFORE INSERT 触发器
|
||
└─ MySQL/SQLite: 自动跟随,无需处理
|
||
|
||
5. 逐表 COUNT(*) 校验
|
||
└─ source vs target 行数一致 → PASS
|
||
```
|
||
|
||
### 常用参数
|
||
|
||
| 参数 | 说明 |
|
||
|:----|:----|
|
||
| `--src-driver` / `--src-host` / `--src-port` / `--src-db` / `--src-user` / `--src-pass` | 源库连接参数 |
|
||
| `--dst-driver` / `--dst-host` / `--dst-port` / `--dst-db` / `--dst-user` / `--dst-pass` | 目标库连接参数 |
|
||
| `--dry-run` | 干跑模式,只连源库逐表数行数,不碰目标库 |
|
||
| `--drop-target-first` | 迁移前删除目标库所有表(幂等重跑) |
|
||
| `--only-tables a,b` | 仅迁移指定表(续传用) |
|
||
| `--skip-existing` | 目标表行数已等于源表则跳过 |
|
||
| `--skip-verify` | 跳过迁移后的逐表行数校验 |
|
||
| `--chunk-size N` | 每批插入行数,默认 1000 |
|
||
| `--single-transaction` | 全部表包进一个大事务(默认每表一个事务) |
|
||
|
||
口令脱敏:`--src-pass` / `--dst-pass` 留空时回落环境变量 `SRC_DB_PASS` / `DST_DB_PASS`,避免口令进 shell 历史。
|
||
|
||
### 干跑示例
|
||
|
||
迁移前建议先干跑核对计划:
|
||
|
||
```bash
|
||
# 干跑:核对源库表和行数
|
||
python scripts/migrate_db.py --dry-run \
|
||
--src-driver=mysql --src-host=127.0.0.1 --src-port=3306 \
|
||
--src-user=root --src-pass=root --src-db=djangoadmin.fastapi.elevue
|
||
|
||
# 或使用 Make 命令
|
||
make db-migrate-dry-run ARGS="\
|
||
--src-driver=mysql --src-host=127.0.0.1 --src-port=3306 \
|
||
--src-user=root --src-pass=root --src-db=djangoadmin.fastapi.elevue"
|
||
```
|
||
|
||
### MySQL → PostgreSQL
|
||
|
||
```bash
|
||
python scripts/migrate_db.py \
|
||
--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=localhost --dst-port=5432 \
|
||
--dst-db=djangoadmin.fastapi.elevue --dst-user=postgres --dst-pass=你的密码 \
|
||
--drop-target-first
|
||
```
|
||
|
||
输出示例:
|
||
|
||
```
|
||
==============================================================
|
||
[迁移开始] 源: mysql:djangoadmin.fastapi.elevue → 目标: postgresql:djangoadmin.fastapi.elevue
|
||
[迁移开始] 待迁移 23 张表
|
||
==============================================================
|
||
[·] [1/5] 建目标库...
|
||
· 正在连接 postgres 维护库...
|
||
· 正在创建目标库 "djangoadmin.fastapi.elevue"(UTF8)...
|
||
[✓] [1/5] 建目标库 完成(耗时 947ms)
|
||
[·] [2/5] 建表结构(23 张表 create_all)...
|
||
[✓] [2/5] 建表结构 完成(耗时 1.1s),23 张表
|
||
[·] [3/5] 逐表迁移数据...
|
||
· [1/23] 正在迁移 fastapi_dept...
|
||
[✓ fastapi_dept] 共 23 行(耗时 12ms)
|
||
· [3/23] 正在迁移 fastapi_city...
|
||
[fastapi_city] +1000(累计 1000)
|
||
[fastapi_city] +1000(累计 2000)
|
||
[fastapi_city] +1000(累计 3000)
|
||
[fastapi_city] +1000(累计 4000)
|
||
[fastapi_city] +150(累计 4150)
|
||
[✓ fastapi_city] 共 4150 行(耗时 850ms)
|
||
...
|
||
[✓] [3/5] 逐表迁移数据 完成(耗时 3.5s),共 4843 行
|
||
[·] [4/5] 重置自增序列...
|
||
[✓] [4/5] 重置自增序列 完成(耗时 412ms) ← PG setval 到 MAX(id)
|
||
[·] [5/5] 逐表行数校验...
|
||
[fastapi_dept] source=23 target=23 PASS
|
||
[fastapi_city] source=4150 target=4150 PASS
|
||
...
|
||
[✓] [5/5] 逐表行数校验 完成(耗时 103ms),23 张表全部 PASS
|
||
==============================================================
|
||
[迁移完成] 成功:4843 行 / 23 张表 / 总耗时 6.0s
|
||
==============================================================
|
||
```
|
||
|
||
### MySQL → SQL Server
|
||
|
||
```bash
|
||
python scripts/migrate_db.py \
|
||
--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=mssql --dst-host=localhost --dst-port=1433 \
|
||
--dst-db=djangoadmin.fastapi.elevue --dst-user=sa --dst-pass=你的密码 \
|
||
--drop-target-first
|
||
```
|
||
|
||
:::tip SQL Server 特殊处理
|
||
迁移时自动 `SET IDENTITY_INSERT ON` 保留原 id,迁移后 `DBCC CHECKIDENT RESEED` 重置自增。
|
||
:::
|
||
|
||
输出示例:
|
||
|
||
```
|
||
==============================================================
|
||
[迁移开始] 源: mysql → 目标: mssql:djangoadmin.fastapi.elevue
|
||
[迁移开始] 待迁移 23 张表
|
||
==============================================================
|
||
[·] [1/5] 建目标库...
|
||
· 正在连接 SQL Server(master 维护库)...
|
||
· 正在删除旧目标库 [djangoadmin.fastapi.elevue](SINGLE_USER + DROP)...
|
||
· 正在创建目标库 [djangoadmin.fastapi.elevue]...
|
||
[✓] [1/5] 建目标库 完成(耗时 304ms)
|
||
[·] [2/5] 建表结构(23 张表 create_all)...
|
||
[✓] [2/5] 建表结构 完成(耗时 743ms),23 张表
|
||
[·] [3/5] 逐表迁移数据...
|
||
...
|
||
[✓ fastapi_city] 共 4150 行(耗时 3.9s)
|
||
...
|
||
[✓] [3/5] 逐表迁移数据 完成(耗时 5.3s),共 4843 行
|
||
[·] [4/5] 重置自增序列...
|
||
[✓] [4/5] 重置自增序列 完成(耗时 90ms)
|
||
[·] [5/5] 逐表行数校验...
|
||
[✓] [5/5] 逐表行数校验 完成(耗时 100ms),23 张表全部 PASS
|
||
==============================================================
|
||
[迁移完成] 成功:4843 行 / 23 张表 / 总耗时 6.6s
|
||
==============================================================
|
||
```
|
||
|
||
### MySQL → SQLite
|
||
|
||
```bash
|
||
python scripts/migrate_db.py \
|
||
--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=sqlite --dst-db=D:/data/djangoadmin.fastapi.elevue.db \
|
||
--drop-target-first
|
||
```
|
||
|
||
:::tip SQLite 说明
|
||
- `--dst-db` 即数据库文件完整路径(相对/绝对皆可)
|
||
- SQLite 无建库概念,`--drop-target-first` 时删除旧文件后重建
|
||
- 自增 id 由 rowid 自动跟随,无需重置序列
|
||
:::
|
||
|
||
输出示例:
|
||
|
||
```
|
||
==============================================================
|
||
[迁移开始] 源: mysql → 目标: sqlite:D:/data/djangoadmin.fastapi.elevue.db
|
||
[迁移开始] 待迁移 23 张表
|
||
==============================================================
|
||
[·] [1/5] 建目标库...
|
||
· 检查/准备 sqlite 文件...
|
||
· 删除旧 sqlite 文件...
|
||
[✓] [1/5] 建目标库 完成(耗时 8ms)
|
||
[·] [2/5] 建表结构(23 张表 create_all)...
|
||
[✓] [2/5] 建表结构 完成(耗时 96ms),23 张表
|
||
[·] [3/5] 逐表迁移数据...
|
||
[✓ fastapi_city] 共 4150 行(耗时 850ms)
|
||
...
|
||
[✓] [3/5] 逐表迁移数据 完成(耗时 1.2s),共 4843 行
|
||
[·] [4/5] 重置自增序列...
|
||
· MySQL/SQLite 自动跟随,无需处理
|
||
[✓] [4/5] 重置自增序列 完成(耗时 0ms)
|
||
[·] [5/5] 逐表行数校验...
|
||
[✓] [5/5] 逐表行数校验 完成(耗时 100ms),23 张表全部 PASS
|
||
==============================================================
|
||
[迁移完成] 成功:4843 行 / 23 张表 / 总耗时 1.4s
|
||
==============================================================
|
||
```
|
||
|
||
### MySQL → Oracle
|
||
|
||
```bash
|
||
python scripts/migrate_db.py \
|
||
--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=oracle --dst-host=localhost --dst-port=1521 \
|
||
--dst-db=ORCLPDB1 --dst-user=fastapi --dst-pass=你的密码 \
|
||
--drop-target-first
|
||
```
|
||
|
||
:::warning Oracle 特殊说明
|
||
- Oracle 无"数据库名",`--dst-db` 填 **SERVICE_NAME**(如 `ORCLPDB1`、`XE`)
|
||
- schema 即 `--dst-user` 连接的用户(无需也无法"建库",连上即视为就绪)
|
||
- 该用户需有 `CONNECT`、`RESOURCE` 权限
|
||
- `create_all` 对 Integer 主键只生成 `INTEGER NOT NULL`(无 identity),迁移后脚本自动补 sequence(`START WITH MAX(id)+1`)+ `BEFORE INSERT` 触发器
|
||
- `--drop-target-first` 时尽力 `DROP TABLE ... PURGE` 清理旧表
|
||
:::
|
||
|
||
输出示例:
|
||
|
||
```
|
||
==============================================================
|
||
[迁移开始] 源: mysql → 目标: oracle:ORCLPDB1
|
||
[迁移开始] 待迁移 23 张表
|
||
==============================================================
|
||
[·] [1/5] 建目标库...
|
||
· 正在连接 Oracle 服务 ORCLPDB1(schema=fastapi)...
|
||
· drop-first:清理 23 张旧项目表(DROP TABLE ... PURGE)...
|
||
[✓] [1/5] 建目标库 完成(耗时 4.6s)
|
||
[·] [2/5] 建表结构(23 张表 create_all)...
|
||
[✓] [2/5] 建表结构 完成(耗时 2.4s),23 张表
|
||
[·] [3/5] 逐表迁移数据...
|
||
[✓ fastapi_city] 共 4150 行(耗时 2m12s)
|
||
...
|
||
[✓] [3/5] 逐表迁移数据 完成(耗时 2m41s),共 4843 行
|
||
[·] [4/5] 重置自增序列... ← 补 sequence + BEFORE INSERT 触发器
|
||
[✓] [4/5] 重置自增序列 完成(耗时 6.2s)
|
||
[·] [5/5] 逐表行数校验...
|
||
[✓] [5/5] 逐表行数校验 完成(耗时 2.6s),23 张表全部 PASS
|
||
==============================================================
|
||
[迁移完成] 成功:4843 行 / 23 张表 / 总耗时 2m54s
|
||
==============================================================
|
||
```
|
||
|
||
### 幂等重跑
|
||
|
||
迁移失败或需重跑时,加 `--drop-target-first` 可安全重跑:
|
||
|
||
```bash
|
||
python scripts/migrate_db.py ... --drop-target-first
|
||
```
|
||
|
||
:::warning 警告
|
||
`--drop-target-first` 会删除目标库所有表(Oracle 清旧表、SQLite 删旧文件),仅在确认目标库无生产数据时使用。
|
||
:::
|
||
|
||
### 断点续传
|
||
|
||
迁移过程被中断时,已迁移的表按每表一个事务提交,未迁完的表可配合 `--only-tables` 和 `--skip-existing` 断点续传:
|
||
|
||
```bash
|
||
# 只迁移未完成的表
|
||
python scripts/migrate_db.py ... --only-tables user,role,article --skip-existing
|
||
```
|
||
|
||
### 迁移拓扑顺序
|
||
|
||
数据泵按拓扑序迁移,确保引用完整性:
|
||
|
||
```
|
||
基础表(先迁移):dept, menu, role, level, position, city, category, dict, config, ...
|
||
依赖表(后迁移):role_menu, dict_item, config_item, user, user_role, article, notice, ...
|
||
```
|
||
|
||
:::tip 无外键设计
|
||
数据库不使用物理外键,依赖关系通过业务代码维护。迁移时显式带 id 插入,保住引用完整性。
|
||
:::
|
||
|
||
### 各驱动自增序列处理
|
||
|
||
| 驱动 | 处理方式 | 说明 |
|
||
|:----|:----|:----|
|
||
| PostgreSQL | `setval(pg_get_serial_sequence(...), MAX(id))` | SERIAL 序列重置到最大值 |
|
||
| SQL Server | `IDENTITY_INSERT ON` + `DBCC CHECKIDENT RESEED` | 显式插 id + 重置种子 |
|
||
| Oracle | 补 sequence + BEFORE INSERT 触发器 | `create_all` 无原生自增,脚本自动补建 |
|
||
| MySQL / SQLite | 自动跟随 | 显式插 id 后 auto_increment / rowid 自动跟随 |
|
||
|
||
如需手动修复自增:
|
||
|
||
```sql
|
||
-- PostgreSQL
|
||
SELECT setval(pg_get_serial_sequence('fastapi_user', 'id'), (SELECT max(id) FROM fastapi_user));
|
||
|
||
-- SQL Server
|
||
DBCC CHECKIDENT ('fastapi_user', RESEED, <max_id>)
|
||
|
||
-- Oracle
|
||
DROP SEQUENCE fastapi_user_id_seq;
|
||
CREATE SEQUENCE fastapi_user_id_seq START WITH <max_id+1>;
|
||
```
|
||
|
||
## 常见问题与排错
|
||
|
||
| 问题 | 原因 | 解决方案 |
|
||
|:----|:----|:----|
|
||
| 目标库数据叠加/主键冲突 | 未加 `--drop-target-first`,已有数据与迁移数据冲突 | 加 `--drop-target-first` 重跑 |
|
||
| 迁移后报 "no such table" | `--dst-db` 指向错误 | Oracle 填 SERVICE_NAME,SQLite 填文件路径 |
|
||
| 中文乱码 | 目标库编码非 UTF8 | 删除重建(`--drop-target-first`),PG 建库强制 UTF8 |
|
||
| Oracle 新增记录 id 异常 | sequence 未正确创建 | 脚本自动补建;若手动建过同名 sequence 会先删后建 |
|
||
| 自增 id 未延续原值 | 序列未重置 | 参考上表手动修复 |
|
||
| 迁移中断 | 网络或超时 | 配合 `--only-tables` + `--skip-existing` 断点续传 |
|
||
|
||
## 常见操作速查
|
||
|
||
| 场景 | 命令 |
|
||
|------|------|
|
||
| 全新安装 | `make db-init` |
|
||
| 模型变更后生成迁移 | `make migrate-revision` |
|
||
| 应用迁移 | `make migrate-upgrade` |
|
||
| 回滚一次迁移 | `alembic downgrade -1` |
|
||
| 标记已有库为最新 | `make migrate-stamp` |
|
||
| 跨库迁移干跑 | `make db-migrate-dry-run ARGS="..."` |
|
||
| 跨库迁移执行 | `make db-migrate ARGS="..."` |
|
||
|
||
## 总结
|
||
|
||
数据库迁移分为结构迁移(Alembic)和数据迁移(`migrate_db.py`)两部分。全新安装用 `make db-init`,模型变更用 Alembic 自动生成迁移脚本,跨库切换用 `migrate_db.py` 数据泵。建议每次迁移前先干跑确认,生产环境务必做好备份。
|