# 数据库迁移 ## 概述 项目使用 Flask-Migrate(基于 Alembic)管理数据库结构迁移,同时提供跨库数据迁移脚本,支持从 MySQL 迁移到 PostgreSQL / SQL Server / SQLite / Oracle 等多种数据库。 :::tip 迁移工具链 - **结构迁移**:Flask-Migrate / Alembic(自动生成 DDL 差异) - **数据迁移**:`scripts/migrate_db.py`(跨库数据泵) - **初始化**:`scripts/setup_db.py`(全新安装一键建库) ::: ## 全新安装初始化 首次部署时,使用初始化脚本一键完成建库、建表、Flask-Migrate stamp: ```bash # 方式一:直接运行脚本 python scripts/setup_db.py # 方式二:Make 命令 make db-init ``` :::tip 初始化流程 1. 检测并创建数据库(如不存在) 2. 执行 `Base.metadata.create_all()` 建表 3. 导入基础 SQL 数据(如有) 4. Flask-Migrate stamp head(标记当前为最新版本) ::: ## Flask-Migrate 结构迁移 ### 生成迁移脚本 修改模型后,执行以下命令自动生成迁移脚本: ```bash # 生成增量迁移脚本 flask db migrate -m "描述变更内容" # 或使用 Make 命令 make migrate-revision ``` :::tip 注意事项 - 执行前确保数据库连接正常(`.env` 中的数据库配置正确) - 自动生成的脚本需人工审查,确认无误后再执行 - 迁移脚本位于 `migrations/versions/` 目录 ::: ### 应用迁移 ```bash # 应用所有待执行的迁移 flask db upgrade # 或使用 Make 命令 make migrate-upgrade ``` ### 回滚迁移 ```bash # 回滚最近一次迁移 flask db downgrade -1 # 回滚到指定版本 flask db downgrade ``` ### 查看迁移状态 ```bash # 查看当前版本 flask db current # 查看迁移历史 flask db history ``` ### 手动标记版本 对于已有数据库,手动标记为最新版本: ```bash flask db 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.flask.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.flask.elevue" ``` ### MySQL → PostgreSQL ```bash python scripts/migrate_db.py \ --src-driver=mysql --src-host=127.0.0.1 --src-port=3306 \ --src-db=djangoadmin.flask.elevue --src-user=root --src-pass=root \ --dst-driver=postgresql --dst-host=localhost --dst-port=5432 \ --dst-db=djangoadmin.flask.elevue --dst-user=postgres --dst-pass=你的密码 \ --drop-target-first ``` 输出示例: ``` ============================================================== [迁移开始] 源: mysql:djangoadmin.flask.elevue → 目标: postgresql:djangoadmin.flask.elevue [迁移开始] 待迁移 23 张表 ============================================================== [·] [1/5] 建目标库... · 正在连接 postgres 维护库... · 正在创建目标库 "djangoadmin.flask.elevue"(UTF8)... [✓] [1/5] 建目标库 完成(耗时 947ms) [·] [2/5] 建表结构(23 张表 create_all)... [✓] [2/5] 建表结构 完成(耗时 1.1s),23 张表 [·] [3/5] 逐表迁移数据... · [1/23] 正在迁移 flask_dept... [✓ flask_dept] 共 23 行(耗时 12ms) · [3/23] 正在迁移 flask_city... [flask_city] +1000(累计 1000) [flask_city] +1000(累计 2000) [flask_city] +1000(累计 3000) [flask_city] +1000(累计 4000) [flask_city] +150(累计 4150) [✓ flask_city] 共 4150 行(耗时 850ms) ... [✓] [3/5] 逐表迁移数据 完成(耗时 3.5s),共 4843 行 [·] [4/5] 重置自增序列... [✓] [4/5] 重置自增序列 完成(耗时 412ms) ← PG setval 到 MAX(id) [·] [5/5] 逐表行数校验... [flask_dept] source=23 target=23 PASS [flask_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.flask.elevue --src-user=root --src-pass=root \ --dst-driver=mssql --dst-host=localhost --dst-port=1433 \ --dst-db=djangoadmin.flask.elevue --dst-user=sa --dst-pass=你的密码 \ --drop-target-first ``` :::tip SQL Server 特殊处理 迁移时自动 `SET IDENTITY_INSERT ON` 保留原 id,迁移后 `DBCC CHECKIDENT RESEED` 重置自增。 ::: ### MySQL → SQLite ```bash python scripts/migrate_db.py \ --src-driver=mysql --src-host=127.0.0.1 --src-port=3306 \ --src-db=djangoadmin.flask.elevue --src-user=root --src-pass=root \ --dst-driver=sqlite --dst-db=D:/data/djangoadmin.flask.elevue.db \ --drop-target-first ``` :::tip SQLite 说明 - `--dst-db` 即数据库文件完整路径(相对/绝对皆可) - SQLite 无建库概念,`--drop-target-first` 时删除旧文件后重建 - 自增 id 由 rowid 自动跟随,无需重置序列 ::: ### MySQL → Oracle ```bash python scripts/migrate_db.py \ --src-driver=mysql --src-host=127.0.0.1 --src-port=3306 \ --src-db=djangoadmin.flask.elevue --src-user=root --src-pass=root \ --dst-driver=oracle --dst-host=localhost --dst-port=1521 \ --dst-db=ORCLPDB1 --dst-user=flask --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` 清理旧表 ::: ### 幂等重跑 迁移失败或需重跑时,加 `--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('flask_user', 'id'), (SELECT max(id) FROM flask_user)); -- SQL Server DBCC CHECKIDENT ('flask_user', RESEED, ) -- Oracle DROP SEQUENCE flask_user_id_seq; CREATE SEQUENCE flask_user_id_seq START WITH ; ``` ## 常见问题与排错 | 问题 | 原因 | 解决方案 | |:----|:----|:----| | 目标库数据叠加/主键冲突 | 未加 `--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` | | 回滚一次迁移 | `flask db downgrade -1` | | 标记已有库为最新 | `make migrate-stamp` | | 跨库迁移干跑 | `make db-migrate-dry-run ARGS="..."` | | 跨库迁移执行 | `make db-migrate ARGS="..."` | ## 总结 数据库迁移分为结构迁移(Flask-Migrate / Alembic)和数据迁移(`migrate_db.py`)两部分。全新安装用 `make db-init`,模型变更用 Flask-Migrate 自动生成迁移脚本,跨库切换用 `migrate_db.py` 数据泵。建议每次迁移前先干跑确认,生产环境务必做好备份。