Skip to content

数据库迁移

概述

使用 Alembic 管理数据库结构迁移,同时提供跨库数据迁移脚本,支持从 MySQL 迁移到 PostgreSQL / SQL Server / SQLite / Oracle 等多种数据库。

迁移工具链

  • 结构迁移:Alembic(自动生成 DDL 差异)
  • 数据迁移scripts/migrate_db.py(跨库数据泵)
  • 初始化scripts/init_db.py(全新安装一键建库)

全新安装初始化

首次部署时,使用初始化脚本一键完成建库、建表、Alembic stamp:

bash
# 方式一:直接运行脚本
python scripts/init_db.py

# 方式二:Make 命令
make db-init

初始化流程

  1. 检测并创建数据库(如不存在)
  2. 执行 Base.metadata.create_all() 建表
  3. 导入基础 SQL 数据(如有)
  4. Alembic stamp head(标记当前为最新版本)

Alembic 结构迁移

生成迁移脚本

修改模型后,执行以下命令自动生成迁移脚本:

bash
# 生成增量迁移脚本
alembic revision --autogenerate -m "描述变更内容"

# 或使用 Make 命令
make migrate-revision

注意事项

  • 执行前确保数据库连接正常(.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_idrole_menu.role_id/menu_idmenu.parent_id 等)
  • 自增序列重置:迁移后按方言自动处理(详见下文)
  • 进度提示:全部耗时步骤带缓冲提示([·] 请勿中断 → [✓] 完成),不会显得卡死
  • 连接独立:源/目标连接全部由 CLI 显式参数给出,不依赖 .env,也无需先切 DB_DRIVER

前置准备

  1. 源库(MySQL)可正常访问
  2. 目标库所需驱动已包含在 requirements.txt
目标库驱动包说明
PostgreSQLpsycopg[binary]psycopg3
SQL Serverpymssql-
SQLite内置无需额外驱动
Oracleoracledbthin 模式,无需安装 Oracle Client
bash
pip install -r requirements.txt

驱动与默认端口

目标库--dst-driver默认端口--dst-db 填什么
MySQLmysql3306数据库名
PostgreSQLpostgresql5432数据库名(含点号会自动双引号)
SQL Servermssql1433数据库名
SQLitesqlite无端口SQLite 文件路径(相对/绝对皆可)
Oracleoracle1521SERVICE_NAME(如 ORCLPDB1、XE,非库名)

驱动别名:postgres / pgpostgresqlsqlserver / sql_servermssqlsqlite3sqlite

迁移流程

迁移分 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

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

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

Oracle 特殊说明

  • Oracle 无"数据库名",--dst-dbSERVICE_NAME(如 ORCLPDB1XE
  • schema 即 --dst-user 连接的用户(无需也无法"建库",连上即视为就绪)
  • 该用户需有 CONNECTRESOURCE 权限
  • 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

警告

--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, ...

无外键设计

数据库不使用物理外键,依赖关系通过业务代码维护。迁移时显式带 id 插入,保住引用完整性。

各驱动自增序列处理

驱动处理方式说明
PostgreSQLsetval(pg_get_serial_sequence(...), MAX(id))SERIAL 序列重置到最大值
SQL ServerIDENTITY_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 数据泵。建议每次迁移前先干跑确认,生产环境务必做好备份。

小蚂蚁云团队 · 提供技术支持