Files
djangoadmin-wiki/DjangoAdmin_Tornado_EleVue/zh/volume6/db-migrate.md
T
2026-09-16 16:36:49 +08:00

460 lines
16 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 数据库迁移
## 概述
使用 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.tornado.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.tornado.elevue"
```
### MySQL → PostgreSQL
```bash
python scripts/migrate_db.py \
--src-driver=mysql --src-host=127.0.0.1 --src-port=3306 \
--src-db=djangoadmin.tornado.elevue --src-user=root --src-pass=root \
--dst-driver=postgresql --dst-host=localhost --dst-port=5432 \
--dst-db=djangoadmin.tornado.elevue --dst-user=postgres --dst-pass=你的密码 \
--drop-target-first
```
输出示例:
```
==============================================================
[迁移开始] 源: mysql:djangoadmin.tornado.elevue → 目标: postgresql:djangoadmin.tornado.elevue
[迁移开始] 待迁移 23 张表
==============================================================
[·] [1/5] 建目标库...
· 正在连接 postgres 维护库...
· 正在创建目标库 "djangoadmin.tornado.elevue"(UTF8)...
[✓] [1/5] 建目标库 完成(耗时 947ms)
[·] [2/5] 建表结构(23 张表 create_all)...
[✓] [2/5] 建表结构 完成(耗时 1.1s),23 张表
[·] [3/5] 逐表迁移数据...
· [1/23] 正在迁移 tornado_dept...
[✓ tornado_dept] 共 23 行(耗时 12ms)
· [3/23] 正在迁移 tornado_city...
[tornado_city] +1000(累计 1000)
[tornado_city] +1000(累计 2000)
[tornado_city] +1000(累计 3000)
[tornado_city] +1000(累计 4000)
[tornado_city] +150(累计 4150)
[✓ tornado_city] 共 4150 行(耗时 850ms)
...
[✓] [3/5] 逐表迁移数据 完成(耗时 3.5s),共 4843 行
[·] [4/5] 重置自增序列...
[✓] [4/5] 重置自增序列 完成(耗时 412ms) ← PG setval 到 MAX(id)
[·] [5/5] 逐表行数校验...
[tornado_dept] source=23 target=23 PASS
[tornado_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.tornado.elevue --src-user=root --src-pass=root \
--dst-driver=mssql --dst-host=localhost --dst-port=1433 \
--dst-db=djangoadmin.tornado.elevue --dst-user=sa --dst-pass=你的密码 \
--drop-target-first
```
:::tip SQL Server 特殊处理
迁移时自动 `SET IDENTITY_INSERT ON` 保留原 id,迁移后 `DBCC CHECKIDENT RESEED` 重置自增。
:::
输出示例:
```
==============================================================
[迁移开始] 源: mysql → 目标: mssql:djangoadmin.tornado.elevue
[迁移开始] 待迁移 23 张表
==============================================================
[·] [1/5] 建目标库...
· 正在连接 SQL Server(master 维护库)...
· 正在删除旧目标库 [djangoadmin.tornado.elevue](SINGLE_USER + DROP)...
· 正在创建目标库 [djangoadmin.tornado.elevue]...
[✓] [1/5] 建目标库 完成(耗时 304ms)
[·] [2/5] 建表结构(23 张表 create_all)...
[✓] [2/5] 建表结构 完成(耗时 743ms),23 张表
[·] [3/5] 逐表迁移数据...
...
[✓ tornado_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.tornado.elevue --src-user=root --src-pass=root \
--dst-driver=sqlite --dst-db=D:/data/djangoadmin.tornado.elevue.db \
--drop-target-first
```
:::tip SQLite 说明
- `--dst-db` 即数据库文件完整路径(相对/绝对皆可)
- SQLite 无建库概念,`--drop-target-first` 时删除旧文件后重建
- 自增 id 由 rowid 自动跟随,无需重置序列
:::
输出示例:
```
==============================================================
[迁移开始] 源: mysql → 目标: sqlite:D:/data/djangoadmin.tornado.elevue.db
[迁移开始] 待迁移 23 张表
==============================================================
[·] [1/5] 建目标库...
· 检查/准备 sqlite 文件...
· 删除旧 sqlite 文件...
[✓] [1/5] 建目标库 完成(耗时 8ms)
[·] [2/5] 建表结构(23 张表 create_all)...
[✓] [2/5] 建表结构 完成(耗时 96ms),23 张表
[·] [3/5] 逐表迁移数据...
[✓ tornado_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.tornado.elevue --src-user=root --src-pass=root \
--dst-driver=oracle --dst-host=localhost --dst-port=1521 \
--dst-db=ORCLPDB1 --dst-user=tornado --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=tornado)...
· drop-first:清理 23 张旧项目表(DROP TABLE ... PURGE)...
[✓] [1/5] 建目标库 完成(耗时 4.6s)
[·] [2/5] 建表结构(23 张表 create_all)...
[✓] [2/5] 建表结构 完成(耗时 2.4s),23 张表
[·] [3/5] 逐表迁移数据...
[✓ tornado_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('tornado_user', 'id'), (SELECT max(id) FROM tornado_user));
-- SQL Server
DBCC CHECKIDENT ('tornado_user', RESEED, <max_id>)
-- Oracle
DROP SEQUENCE tornado_user_id_seq;
CREATE SEQUENCE tornado_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` 数据泵。建议每次迁移前先干跑确认,生产环境务必做好备份。