Files
rxthinkcmf-wiki/RXThinkCMF_ThinkPhp8_EleVue/zh/tools/db-migrate.md
T
2026-10-10 12:20:37 +08:00

858 lines
37 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.
# 7.6 数据库迁移工具
:::tip 概述
`db:migrate` 是一个 CLI 命令行工具,支持在 MySQL / PostgreSQL / SQL Server / Oracle / SQLite 五种数据库之间任意组合迁移。完整迁移流程包含自动建库、建表结构(含索引、外键、注释、自增、collation)、分批迁移数据、重置自增序列、逐表行数校验 5 个步骤。数据迁移基于主键稳定排序,支持事务保护与自动回滚。目标库不存在时自动创建,支持含点号的数据库名。
:::
## 迁移流程
```text
┌──────────────────────────────────────────────────────────────┐
│ 数据库迁移 5 步流程 │
└──────────────────────────────────────────────────────────────┘
[1/5] 建目标库
│ 目标库不存在时自动创建(所有驱动均支持)
│ 自动从源库读取 collation 并应用(仅源与目标驱动相同时)
│ MySQL: CREATE DATABASE ... COLLATE | PgSQL: LC_COLLATE
│ SqlServer: CREATE DATABASE ... COLLATE | Oracle: CREATE USER
│ SQLite: 自动创建文件
▼
[2/5] 建表结构 + 索引 + 注释 + 外键
│ 源库 getTableFields() → DbSchemaBuilder 生成 DDL → 目标库执行
│ 自动迁移字段注释(MySQL: COMMENT 子句 | PgSQL/Oracle: COMMENT ON COLUMN | SqlServer: sp_addextendedproperty)
│ 自动迁移表注释(MySQL: ALTER TABLE COMMENT | PgSQL/Oracle: COMMENT ON TABLE)
│ 自动迁移非主键索引(UNIQUE INDEX / 普通 INDEX / 全文索引)
│ 自动迁移外键约束(所有表创建后统一添加,避免引用顺序问题)
│ 字段默认值完整保留(含空字符串 DEFAULT '',函数型默认值自动识别)
│ enum/set 类型自动迁移候选值(生成 CHECK 约束)
│ decimal/datetime 精度保留(如 decimal(10,2)、datetime(3))
│ 排序规则(collation)自动迁移:表级 + 列级(MySQL/PostgreSQL)
│ Oracle 自增:自动创建序列+触发器
│ 支持 --drop-target-first 先删后建
▼
[3/5] 逐表迁移数据(事务保护)
│ 按 batch-size(默认 1000)分页读取,每页再拆为 100 行的 INSERT 语句
│ 每个 batch-size 为一个独立事务,提交后即生效
│ 当前批次失败时回滚,已提交的历史批次不受影响
│ 基于主键 ORDER BY 稳定排序,避免分页漏行/重复
│ blob 字段使用 PDO::PARAM_LOB 绑定
│ SQL Server: 自动开关 IDENTITY_INSERT
▼
[4/5] 重置自增序列
│ PostgreSQL: SELECT setval 重置序列到 MAX(id)+1
│ SQL Server: DBCC CHECKIDENT 重置标识种子
│ Oracle: ALTER SEQUENCE 重置序列
│ MySQL / SQLite: 跳过(无需重置)
▼
[5/5] 逐表行数校验
SELECT COUNT(*) 对比源和目标,输出 PASS/FAIL
```
## 命令格式
```bash
# =============================================================================
# 数据库迁移命令
# =============================================================================
# php think db:migrate → 调用 ThinkPHP 命令行的数据库迁移工具
# [选项] → 通过 --key=value 格式传入源/目标数据库连接参数
# =============================================================================
php think db:migrate [选项]
```
### 必填选项
| 选项 | 说明 |
|------|------|
| `--src-type` | 源数据库驱动:`mysql` / `pgsql` / `sqlsrv` / `oracle` / `sqlite` |
| `--src-database` | 源数据库名 |
| `--dst-type` | 目标数据库驱动:`mysql` / `pgsql` / `sqlsrv` / `oracle` / `sqlite` |
| `--dst-database` | 目标数据库名 |
### 源连接选项
| 选项 | 默认值 | 说明 |
|------|--------|------|
| `--src-host` | `127.0.0.1` | 源主机地址 |
| `--src-hostport` | 自动推断 | 源端口(mysql=3306, pgsql=5432, sqlsrv=1433, oracle=1521) |
| `--src-username` | - | 源用户名(SQLite 除外) |
| `--src-password` | - | 源密码(SQLite 除外) |
### 目标连接选项
| 选项 | 默认值 | 说明 |
|------|--------|------|
| `--dst-host` | `127.0.0.1` | 目标主机地址 |
| `--dst-hostport` | 自动推断 | 目标端口 |
| `--dst-username` | - | 目标用户名(SQLite 除外) |
| `--dst-password` | - | 目标密码(SQLite 除外) |
### 迁移选项
| 选项 | 默认值 | 说明 |
|------|--------|------|
| `--drop-target-first` | 否 | 迁移前先删除目标库中的同名表 |
| `--batch-size` | `1000` | 每批读取的行数(影响内存占用与迁移速度) |
| `--tables` | 全部 | 只迁移指定表(逗号分隔) |
## 迁移命令签名
### MySQL → PostgreSQL
```bash
# -----------------------------------------------------------------------------
# MySQL 迁移到 PostgreSQL
# -----------------------------------------------------------------------------
# --src-* → 源数据库连接参数(MySQL)
# --dst-* → 目标数据库连接参数(PostgreSQL)
# --drop-target-first → 迁移前先删除目标库中的同名表(避免表已存在报错)
php think db:migrate \
--src-type=mysql --src-host=127.0.0.1 --src-hostport=3306 \
--src-database=rxthinkcmf.thinkphp8.elevue --src-username=root --src-password=root \
--dst-type=pgsql --dst-host=127.0.0.1 --dst-hostport=5432 \
--dst-database=rxthinkcmf.thinkphp8.elevue --dst-username=postgres --dst-password=123456 \
--drop-target-first
```
### MySQL → SQL Server
```bash
# -----------------------------------------------------------------------------
# MySQL 迁移到 SQL Server
# -----------------------------------------------------------------------------
# --dst-type=sqlsrv → 目标驱动为 SQL Server
# --dst-hostport=1433 → SQL Server 默认端口
php think db:migrate \
--src-type=mysql --src-host=127.0.0.1 --src-hostport=3306 \
--src-database=rxthinkcmf.thinkphp8.elevue --src-username=root --src-password=root \
--dst-type=sqlsrv --dst-host=127.0.0.1 --dst-hostport=1433 \
--dst-database=rxthinkcmf.thinkphp8.elevue --dst-username=sa --dst-password=123456 \
--drop-target-first
```
### MySQL → SQLite
```bash
# -----------------------------------------------------------------------------
# MySQL 迁移到 SQLite
# -----------------------------------------------------------------------------
# --dst-type=sqlite → 目标驱动为 SQLite(无需用户名密码)
# --dst-database → SQLite 文件路径(文件不存在时自动创建)
php think db:migrate \
--src-type=mysql --src-host=127.0.0.1 --src-hostport=3306 \
--src-database=rxthinkcmf.thinkphp8.elevue --src-username=root --src-password=root \
--dst-type=sqlite --dst-database=runtime/rxthinkcmf.thinkphp8.elevue.db
```
### MySQL → MySQL(同库复制)
```bash
# -----------------------------------------------------------------------------
# MySQL 同库复制(源和目标在同一 MySQL 实例,数据库名不同)
# -----------------------------------------------------------------------------
# 不带 --drop-target-first → 目标表已存在时跳过(不删除)
php think db:migrate \
--src-type=mysql --src-host=127.0.0.1 --src-hostport=3306 \
--src-database=source_db --src-username=root --src-password=root \
--dst-type=mysql --dst-host=127.0.0.1 --dst-hostport=3306 \
--dst-database=target_db --dst-username=root --dst-password=root
```
### PostgreSQL → MySQL
```bash
# -----------------------------------------------------------------------------
# PostgreSQL 迁移到 MySQL
# -----------------------------------------------------------------------------
php think db:migrate \
--src-type=pgsql --src-host=127.0.0.1 --src-hostport=5432 \
--src-database=rxthinkcmf.thinkphp8.elevue --src-username=postgres --src-password=123456 \
--dst-type=mysql --dst-host=127.0.0.1 --dst-hostport=3306 \
--dst-database=rxthinkcmf.thinkphp8.elevue --dst-username=root --dst-password=root \
--drop-target-first
```
### SQL Server → MySQL
```bash
# -----------------------------------------------------------------------------
# SQL Server 迁移到 MySQL
# -----------------------------------------------------------------------------
php think db:migrate \
--src-type=sqlsrv --src-host=127.0.0.1 --src-hostport=1433 \
--src-database=rxthinkcmf.thinkphp8.elevue --src-username=sa --src-password=123456 \
--dst-type=mysql --dst-host=127.0.0.1 --dst-hostport=3306 \
--dst-database=rxthinkcmf.thinkphp8.elevue --dst-username=root --dst-password=root \
--drop-target-first
```
### 只迁移指定表
```bash
# -----------------------------------------------------------------------------
# 只迁移指定表(逗号分隔表名,不含前缀)
# -----------------------------------------------------------------------------
# --tables=think_user,think_role,think_menu → 只迁移这 3 张表
php think db:migrate \
--src-type=mysql --src-host=127.0.0.1 --src-hostport=3306 \
--src-database=rxthinkcmf.thinkphp8.elevue --src-username=root --src-password=root \
--dst-type=pgsql --dst-host=127.0.0.1 --dst-hostport=5432 \
--dst-database=rxthinkcmf.thinkphp8.elevue --dst-username=postgres --dst-password=123456 \
--tables=think_user,think_role,think_menu \
--drop-target-first
```
## 迁移输出日志
### MySQL → PostgreSQL 完整日志
```bash
$ php think db:migrate \
--src-type=mysql --src-host=127.0.0.1 --src-hostport=3306 \
--src-database=rxthinkcmf.thinkphp8.elevue --src-username=root --src-password=root \
--dst-type=pgsql --dst-host=127.0.0.1 --dst-hostport=5432 \
--dst-database=rxthinkcmf.thinkphp8.elevue --dst-username=postgres --dst-password=123456 \
--drop-target-first
==============================================================
[迁移开始] 源: mysql:rxthinkcmf.thinkphp8.elevue → 目标: pgsql:rxthinkcmf.thinkphp8.elevue
[迁移开始] 待迁移 25 张表
==============================================================
[·] [1/5] 建目标库...
· 目标库 "rxthinkcmf.thinkphp8.elevue" 已就绪
[✓] [1/5] 建目标库 完成(耗时 0ms)
[·] [2/5] 建表结构(25 张表)...
[✓] [2/5] 建表结构 完成(耗时 440ms),25 张表,118 个索引,0 个外键
[·] [3/5] 逐表迁移数据...
· [1/25] 正在迁移 think_article...
[think_article] +13(累计 13)
[✓ think_article] 共 13 行(耗时 10ms)
· [2/25] 正在迁移 think_category...
[think_category] +150(累计 150)
[✓ think_category] 共 150 行(耗时 12ms)
· [3/25] 正在迁移 think_city...
[think_city] +1000(累计 1000)
[think_city] +1000(累计 2000)
[think_city] +1000(累计 3000)
...(大表分批输出进度,每批 1000 行)...
[think_city] +562(累计 783562)
[✓ think_city] 共 783562 行(耗时 10m10s)
· [4/25] 正在迁移 think_config...
[think_config] +1(累计 1)
[✓ think_config] 共 1 行(耗时 11ms)
...(其余表类似)...
· [25/25] 正在迁移 think_user_role...
[think_user_role] +25(累计 25)
[✓ think_user_role] 共 25 行(耗时 6ms)
[✓] [3/5] 逐表迁移数据 完成(耗时 10m10s),共 785215 行
[·] [4/5] 重置自增序列...
[✓] [4/5] 重置自增序列 完成(耗时 23ms)
[·] [5/5] 逐表行数校验...
[think_article] source=13 target=13 PASS
[think_category] source=150 target=150 PASS
[think_city] source=783562 target=783562 PASS
[think_config] source=1 target=1 PASS
...(其余表类似)...
[think_user_role] source=25 target=25 PASS
[✓] [5/5] 逐表行数校验 完成(耗时 452ms),25 张表全部 PASS
==============================================================
[迁移完成] 成功:785215 行 / 25 张表 / 总耗时 10m11s
==============================================================
```
### MySQL → SQL Server 日志摘要
```bash
$ php think db:migrate \
--src-type=mysql --src-host=127.0.0.1 --src-hostport=3306 \
--src-database=rxthinkcmf.thinkphp8.elevue --src-username=root --src-password=root \
--dst-type=sqlsrv --dst-host=127.0.0.1 --dst-hostport=1433 \
--dst-database=rxthinkcmf.thinkphp8.elevue --dst-username=sa --dst-password=123456 \
--drop-target-first
==============================================================
[迁移开始] 源: mysql:rxthinkcmf.thinkphp8.elevue → 目标: sqlsrv:rxthinkcmf.thinkphp8.elevue
[迁移开始] 待迁移 25 张表
==============================================================
[·] [1/5] 建目标库...
· 目标库 "rxthinkcmf.thinkphp8.elevue" 已就绪
[✓] [1/5] 建目标库 完成(耗时 0ms)
[·] [2/5] 建表结构(25 张表)...
[✓] [2/5] 建表结构 完成(耗时 1.5s),25 张表,118 个索引,0 个外键
[·] [3/5] 逐表迁移数据...
...(逐表迁移,SQL Server 每行耗时较 MySQL/SQLite 更长)...
[✓] [3/5] 逐表迁移数据 完成(耗时 11m48s),共 785231 行
[·] [4/5] 重置自增序列...
[✓] [4/5] 重置自增序列 完成(耗时 52ms)
[·] [5/5] 逐表行数校验...
...(25 张表全部 PASS)...
[✓] [5/5] 逐表行数校验 完成(耗时 681ms),25 张表全部 PASS
==============================================================
[迁移完成] 成功:785231 行 / 25 张表 / 总耗时 11m50s
==============================================================
```
### MySQL → Oracle 日志摘要
```bash
$ php think db:migrate \
--src-type=mysql --src-host=127.0.0.1 --src-hostport=3306 \
--src-database=rxthinkcmf.thinkphp8.elevue --src-username=root --src-password=root \
--dst-type=oracle --dst-host=127.0.0.1 --dst-hostport=1521 \
--dst-database=rxthinkcmf --dst-username=system --dst-password=123456 \
--drop-target-first
==============================================================
[迁移开始] 源: mysql:rxthinkcmf.thinkphp8.elevue → 目标: oracle:rxthinkcmf.thinkphp8.elevue
[迁移开始] 待迁移 25 张表
==============================================================
[·] [1/5] 建目标库...
· 目标库 "rxthinkcmf" 已就绪
[✓] [1/5] 建目标库 完成(耗时 0ms)
[·] [2/5] 建表结构(25 张表)...
· 为 think_article 创建序列+触发器(自增列 id)
· 为 think_city 创建序列+触发器(自增列 id)
...(其余含自增列的表类似)...
[✓] [2/5] 建表结构 完成(耗时 2.1s),25 张表,118 个索引,0 个外键
[·] [3/5] 逐表迁移数据...
· [1/25] 正在迁移 think_article...
[think_article] +13(累计 13)
[✓ think_article] 共 13 行(耗时 45ms)
...(Oracle INSERT 耗时较 MySQL/SQLite 更长)...
[✓] [3/5] 逐表迁移数据 完成(耗时 15m30s),共 785215 行
[·] [4/5] 重置自增序列...
· 重置序列: think_article_id_seq → 14
· 重置序列: think_city_id_seq → 783563
...(其余序列类似)...
[✓] [4/5] 重置自增序列 完成(耗时 180ms)
[·] [5/5] 逐表行数校验...
...(25 张表全部 PASS)...
[✓] [5/5] 逐表行数校验 完成(耗时 820ms),25 张表全部 PASS
==============================================================
[迁移完成] 成功:785215 行 / 25 张表 / 总耗时 15m33s
==============================================================
```
:::tip Oracle 迁移要点
- **建库**:Oracle 通过 `CREATE USER` 创建用户,库名即用户名,需要 DBA 权限
- **自增列**:自动创建序列(`_seq`)和触发器(`_trg`),插入时自动填充
- **序列重置**:迁移完成后通过 `ALTER SEQUENCE ... RESTART` 重置到 MAX+1
- **分页**:使用 `ROW_NUMBER() OVER (ORDER BY 主键)` 保证稳定排序
- **字段名**:Oracle 默认大写,工具自动处理大小写转换
:::
### MySQL → SQLite 日志摘要
```bash
$ php think db:migrate \
--src-type=mysql --src-host=127.0.0.1 --src-hostport=3306 \
--src-database=rxthinkcmf.thinkphp8.elevue --src-username=root --src-password=root \
--dst-type=sqlite --dst-database=runtime/rxthinkcmf.thinkphp8.elevue.db
==============================================================
[迁移开始] 源: mysql:rxthinkcmf.thinkphp8.elevue → 目标: sqlite:runtime/rxthinkcmf.thinkphp8.elevue.db
[迁移开始] 待迁移 25 张表
==============================================================
[·] [1/5] 建目标库...
· 目标库 "runtime/rxthinkcmf.thinkphp8.elevue.db" 已就绪
[✓] [1/5] 建目标库 完成(耗时 0ms)
[·] [2/5] 建表结构(25 张表)...
[✓] [2/5] 建表结构 完成(耗时 1s),25 张表,118 个索引,0 个外键
[·] [3/5] 逐表迁移数据...
...(SQLite 写入速度最快)...
[✓] [3/5] 逐表迁移数据 完成(耗时 9m44s),共 785235 行
[·] [4/5] 重置自增序列...
· 当前驱动 (sqlite) 无需重置序列,跳过
[✓] [4/5] 重置自增序列 完成(耗时 0ms)
[·] [5/5] 逐表行数校验...
...(25 张表全部 PASS)...
[✓] [5/5] 逐表行数校验 完成(耗时 427ms),25 张表全部 PASS
==============================================================
[迁移完成] 成功:785235 行 / 25 张表 / 总耗时 9m46s
==============================================================
```
### 只迁移指定表日志
```bash
$ php think db:migrate \
--src-type=mysql --src-host=127.0.0.1 --src-hostport=3306 \
--src-database=rxthinkcmf.thinkphp8.elevue --src-username=root --src-password=root \
--dst-type=pgsql --dst-host=127.0.0.1 --dst-hostport=5432 \
--dst-database=rxthinkcmf.thinkphp8.elevue --dst-username=postgres --dst-password=123456 \
--tables=think_user,think_role,think_menu \
--drop-target-first
==============================================================
[迁移开始] 源: mysql:rxthinkcmf.thinkphp8.elevue → 目标: pgsql:rxthinkcmf.thinkphp8.elevue
[迁移开始] 待迁移 3 张表
==============================================================
[·] [1/5] 建目标库...
· 目标库 "rxthinkcmf.thinkphp8.elevue" 已就绪
[✓] [1/5] 建目标库 完成(耗时 0ms)
[·] [2/5] 建表结构(3 张表)...
· 已删除目标表: think_user
· 已删除目标表: think_role
· 已删除目标表: think_menu
[✓] [2/5] 建表结构 完成(耗时 85ms),3 张表,5 个索引,0 个外键
[·] [3/5] 逐表迁移数据...
· [1/3] 正在迁移 think_user...
[think_user] +22(累计 22)
[✓ think_user] 共 22 行(耗时 9ms)
· [2/3] 正在迁移 think_role...
[think_role] +24(累计 24)
[✓ think_role] 共 24 行(耗时 5ms)
· [3/3] 正在迁移 think_menu...
[think_menu] +228(累计 228)
[✓ think_menu] 共 228 行(耗时 12ms)
[✓] [3/5] 逐表迁移数据 完成(耗时 30ms),共 274 行
[·] [4/5] 重置自增序列...
[✓] [4/5] 重置自增序列 完成(耗时 25ms)
[·] [5/5] 逐表行数校验...
[think_user] source=22 target=22 PASS
[think_role] source=24 target=24 PASS
[think_menu] source=228 target=228 PASS
[✓] [5/5] 逐表行数校验 完成(耗时 60ms),3 张表全部 PASS
==============================================================
[迁移完成] 成功:274 行 / 3 张表 / 总耗时 250ms
==============================================================
```
## 类型映射
| 源类型 | MySQL | PostgreSQL | SqlServer | Oracle | SQLite |
|--------|-------|------------|-----------|--------|--------|
| bigint | bigint | bigint | bigint | NUMBER(19) | INTEGER |
| int | int | integer | int | NUMBER(10) | INTEGER |
| smallint | smallint | smallint | smallint | NUMBER(5) | INTEGER |
| tinyint | tinyint | smallint | tinyint | NUMBER(3) | INTEGER |
| decimal | decimal | numeric | decimal | NUMBER | REAL |
| float | float | real | float | BINARY_FLOAT | REAL |
| double | double | double precision | float | BINARY_DOUBLE | REAL |
| varchar | varchar | varchar | nvarchar | VARCHAR2 | TEXT |
| char | char | char | nchar | CHAR | TEXT |
| text | text | text | ntext | CLOB | TEXT |
| blob | blob | bytea | varbinary | BLOB | BLOB |
| datetime | datetime | timestamp | datetime | TIMESTAMP | TEXT |
| timestamp | timestamp | timestamp | datetime | TIMESTAMP | TEXT |
| date | date | date | date | DATE | TEXT |
| boolean | tinyint(1) | boolean | bit | NUMBER(1) | INTEGER |
| json | json | jsonb | ntext | CLOB | TEXT |
:::tip 精度保留
- `decimal(10,2)` → PostgreSQL `numeric(10,2)`(精度不丢失)
- `datetime(3)` → PostgreSQL `timestamp(3)`(毫秒精度保留)
- `enum('a','b','c')` → PostgreSQL `varchar` + `CHECK (col IN ('a','b','c'))`(候选值保留)
:::
## 各驱动特殊处理
| 驱动 | 建库方式 | 分页排序 | 自增语法 | 索引 | 外键 | 字段注释 | 表注释 | collation | 序列重置 |
|------|----------|----------|----------|------|------|----------|--------|-----------|----------|
| MySQL | `CREATE DATABASE ... COLLATE` | 主键 `ORDER BY` | `AUTO_INCREMENT` | ✅ | ✅ | ✅ `COMMENT` | ✅ | ✅ 库/表/列 | 跳过 |
| PostgreSQL | `LC_COLLATE` | 主键 `ORDER BY` | `GENERATED BY DEFAULT AS IDENTITY` | ✅ | ✅ | ✅ `COMMENT ON COLUMN` | ✅ | ✅ 库/列 | ✅ `setval` |
| SQL Server | `COLLATE` | 主键 `ORDER BY` | `IDENTITY(1,1)` | ✅ | ✅ | ✅ `sp_addextendedproperty` | ✅ | ✅ 库级 | ✅ `DBCC CHECKIDENT` |
| Oracle | `CREATE USER` + `GRANT` | `ROW_NUMBER()` | 序列+触发器 | ✅ | ✅ | ✅ `COMMENT ON COLUMN` | ✅ | ❌ | ✅ `ALTER SEQUENCE` |
| SQLite | 自动创建文件 | 主键 `ORDER BY` | `INTEGER PRIMARY KEY AUTOINCREMENT` | ✅ | ✅ | ❌ | ❌ | ❌ | 跳过 |
## PostgreSQL 特殊处理
### 建库
迁移到 PostgreSQL 时,工具会自动连接 `postgres` 维护库执行:
```sql
-- 创建目标数据库(UTF-8 编码)
-- 若数据库已存在则跳过
-- LC_COLLATE / LC_CTYPE:仅当源与目标驱动相同时,从源库读取 collation 并应用
CREATE DATABASE "数据库名" WITH ENCODING 'UTF8'
```
:::warning PgSQL 数据库不存在的判定
PostgreSQL 数据库不存在时返回的 SQLSTATE 可能是 `3D000`(明确不存在)或 `08006`(连接异常,中文环境下错误信息可能乱码)。工具会主动连接 `postgres` 维护库查询 `pg_database` 确认目标库是否真的不存在,避免误判。
:::
### 序列重置
迁移完成后,对每张有自增主键的表分两步重置序列:
```php
// 第一步:通过 pg_get_serial_sequence() 获取列关联的实际序列名
// 兼容 serial(序列名如 table_id_seq)和 IDENTITY(序列名由系统自动生成)两种风格
$seqName = $dstPdo->query(
"SELECT pg_get_serial_sequence('表名', '字段名') AS s"
)->fetch()['s'];
// 第二步:查询当前最大值,将序列重置到 max+1
// setval 的第三个参数 false 表示下一个 nextval 返回给定值
$maxId = $dstPdo->query(
"SELECT COALESCE(MAX(\"字段名\"), 0) AS m FROM \"表名\""
)->fetch()['m'];
$dstPdo->exec("SELECT setval({$quotedSeq}, {$maxId} + 1, false)");
```
## SQL Server 特殊处理
### 建库
```sql
-- 连接 master 库,创建目标数据库
-- COLLATE:仅当源与目标驱动相同时,从源库读取 collation 并应用
CREATE DATABASE [数据库名]
```
:::warning SQL Server "登录失败"的判定
SQL Server 数据库不存在时返回 SQLSTATE `28000` 并提示"登录失败",容易与密码错误混淆。工具会尝试连接 `master` 库验证凭据:若 master 连接成功说明凭据正确、是数据库不存在;若 master 也失败则为密码错误。
:::
### 自增列迁移
数据迁移时自动处理 `IDENTITY_INSERT`:
```sql
-- 迁移前开启(允许显式写入自增列的值)
SET IDENTITY_INSERT [表名] ON
-- 批量 INSERT ...
-- 迁移后关闭
SET IDENTITY_INSERT [表名] OFF
```
### 序列重置
```sql
-- 重置标识种子到指定值,下一次插入自动加一
-- 第一个参数:表名(带方括号)
-- 第三个参数:RESEED 值(当前最大 ID)
DBCC CHECKIDENT ('[表名]', RESEED, {$maxId})
```
## Oracle 特殊处理
### 建库
Oracle 的"建库"实际上是创建用户并授权。工具自动连接 Oracle 实例(默认服务名 `XE`),执行:
```sql
-- 创建用户(库名即用户名,密码与连接密码一致)
CREATE USER "用户名" IDENTIFIED BY "密码" DEFAULT TABLESPACE USERS;
-- 授权:CONNECT(连接)、RESOURCE(建表等基础对象操作)
GRANT CONNECT, RESOURCE TO "用户名";
```
:::warning Oracle 连接串
Oracle 的 DSN 格式为 `oci:dbname=//host:port/service_name`,其中 `service_name` 默认为 `XE`(Express Edition)。生产环境需根据实际的 Oracle 服务名修改,可通过 `lsnrctl status` 查看。
:::
### 自增列
Oracle 没有原生自增语法,工具自动创建序列+触发器实现等效功能:
```sql
-- 创建序列(从 1 开始,每次递增 1,不缓存)
-- 序列名规则:表名_列名_SEQ(大写)
CREATE SEQUENCE "TABLE_ID_SEQ" START WITH 1 INCREMENT BY 1 NOCACHE;
-- 创建触发器(插入前自动取序列值填充自增列)
-- 触发器名规则:表名_列名_TRG(大写)
-- IF :NEW.COL IS NULL 判断:允许显式传值,仅当未指定时才自动取序列
-- :NEW 引用的列名必须大写且不带引号
CREATE OR REPLACE TRIGGER "TABLE_ID_TRG"
BEFORE INSERT ON "TABLE" FOR EACH ROW
BEGIN
IF :NEW.ID IS NULL THEN
SELECT "TABLE_ID_SEQ".NEXTVAL INTO :NEW.ID FROM dual;
END IF;
END;
```
### 序列重置
```sql
-- 重置序列到 MAX(id)+1
-- ALTER SEQUENCE ... RESTART:将序列起点设置为指定值,下一次 nextval 返回该值
ALTER SEQUENCE "TABLE_ID_SEQ" RESTART START WITH 1234;
```
:::warning Oracle 建库权限
Oracle 建库实际上是通过 `CREATE USER` + `GRANT CONNECT, RESOURCE` 创建用户。需要 DBA 权限或具有 CREATE USER 权限的账号。连接串中的服务名默认为 `XE`(Express Edition),正式环境需根据实际配置修改。
:::
## 驱动安装与环境配置
迁移工具基于 PDO 直连数据库,各驱动需要对应的 PDO 扩展。XAMPP 默认只自带 MySQL、PostgreSQL 和 SQLite 驱动,SQL Server 和 Oracle 需手动安装。
运行以下命令检查已安装的驱动:
```bash
# 检查已安装的 PDO 驱动(-m 列出扩展,findstr /i 不区分大小写过滤)
D:\xampp8.2\php\php.exe -m | findstr /i pdo
```
### MySQL(XAMPP 默认自带)
无需额外操作。验证:
```bash
# 检查 MySQL 驱动是否已加载
D:\xampp8.2\php\php.exe -m | findstr mysql
```
### PostgreSQL(XAMPP 默认自带)
无需额外操作。验证:
```bash
# 检查 PostgreSQL 驱动是否已加载
D:\xampp8.2\php\php.exe -m | findstr pgsql
```
若未安装,编辑 `D:\xampp8.2\php\php.ini`,找到 `;extension=pdo_pgsql` 去掉分号启用。
### SQL Server(需手动安装)
XAMPP 默认不带 SQL Server 驱动,需要安装两个组件。
:::warning 先决条件
安装 ODBC 驱动前,系统需要 Microsoft Visual C++ Redistributable。大多数 Windows 已预装,如果安装失败,先安装它再重试。
:::
**步骤 1:安装 Microsoft ODBC Driver for SQL Server**
这是底层通信组件,sqlsrv 扩展依赖它,必须安装。
- 下载地址:https://learn.microsoft.com/zh-cn/sql/connect/odbc/download-odbc-driver-for-sql-server
- 版本选择:推荐 ODBC Driver 18(最新),两者可与 17 并存
- 安装:下载 x64 版本的 `msodbcsql.msi`,双击按向导完成
**步骤 2:下载 PHP 扩展 DLL**
从微软官方 GitHub 下载:
- 下载地址(Releases 页面):https://github.com/Microsoft/msphpsql/releases
- 下载文件:`Windows_5.12.0RTW.zip`
解压后目录结构(平铺,无子目录):
```text
Windows_5.12.0RTW/
├── php_pdo_sqlsrv_81_nts_x64.dll
├── php_pdo_sqlsrv_81_ts_x64.dll
├── php_pdo_sqlsrv_82_nts_x64.dll
├── php_pdo_sqlsrv_82_ts_x64.dll ← PHP 8.2 TS x64
├── php_pdo_sqlsrv_83_nts_x64.dll
├── php_pdo_sqlsrv_83_ts_x64.dll
├── php_sqlsrv_81_nts_x64.dll
├── php_sqlsrv_81_ts_x64.dll
├── php_sqlsrv_82_nts_x64.dll
├── php_sqlsrv_82_ts_x64.dll ← PHP 8.2 TS x64
├── php_sqlsrv_83_nts_x64.dll
├── php_sqlsrv_83_ts_x64.dll
└── ...(另有 x86 版本)
```
**文件命名规则**:`php_{驱动}_{PHP版本}_{线程安全}_{架构}.dll`
- 驱动:`sqlsrv` 或 `pdo_sqlsrv`
- PHP 版本:`81` / `82` / `83`
- 线程安全:`ts`(Thread Safe)/ `nts`(Non-Thread Safe)
- 架构:`x64` / `x86`
将以下两个文件复制到 `D:\xampp8.2\php\ext\`:
- `php_sqlsrv_82_ts_x64.dll`
- `php_pdo_sqlsrv_82_ts_x64.dll`
:::tip 版本匹配(PHP 8.2 + TS + x64)
- PHP 版本:`82` = PHP 8.2
- 线程安全:XAMPP 默认 TS(运行 `php -v` 确认,输出含 `Thread Safety => enabled`)
- 架构:`x64`(64 位系统)
:::
**步骤 3:在 php.ini 中启用扩展**
编辑 `D:\xampp8.2\php\php.ini`(注意不是 `apache\bin\php.ini`):
1. 启用前置扩展:找到 `;extension=mbstring` 去掉分号(sqlsrv 依赖 mbstring)
2. 在 mbstring 之后添加以下两行(文件名必须与 ext 目录中的实际文件一致):
```ini
; SQL Server PHP 扩展(必须在 mbstring 之后加载)
extension=php_sqlsrv_82_ts_x64.dll ; SQL Server 核心驱动
extension=php_pdo_sqlsrv_82_ts_x64.dll ; SQL Server PDO 驱动(迁移工具使用)
```
3. 保存后重启 XAMPP 中的 Apache 服务(命令行验证无需重启 Apache,只需重新运行 php 命令)
**步骤 4:验证安装**
```bash
# 检查 SQL Server 驱动是否已加载(应看到 sqlsrv 和 pdo_sqlsrv 两行)
D:\xampp8.2\php\php.exe -m | findstr sqlsrv
```
应看到 `sqlsrv` 和 `pdo_sqlsrv` 两行输出。
### Oracle(需手动配置)
XAMPP 8.2 已自带 `php_pdo_oci.dll`(位于 `D:\xampp8.2\php\ext\`)。迁移工具基于 PDO 连接数据库,需要 `pdo_oci` 扩展,只需安装 Oracle Instant Client 并在 php.ini 中启用 `pdo_oci` 即可。
**步骤 1:安装 Oracle Instant Client**
pdo_oci 扩展依赖 Oracle 客户端库,必须先安装。
- 下载地址:https://www.oracle.com/database/technologies/instant-client/downloads.html
- 版本选择:下载 Basic Package,选择 x64 版本,推荐 19c(与 XAMPP 自带的 DLL 对应)
- 安装:
1. 下载 `instantclient-basic-windows.x64-19.20.0.0.0dbru.zip`
2. 解压到固定路径,如 `C:\oracle\instantclient_19_20`(⚠️ 避免空格与中文)
3. 配置系统环境变量:
- 将 `C:\oracle\instantclient_19_20` 添加到系统 `PATH`
- 新建系统变量 `TNS_ADMIN` = `C:\oracle\instantclient_19_20`(可选,用于 tnsnames.ora)
4. 重启终端或重启电脑使环境变量生效
:::warning 安装 VC++ 运行库
Oracle Instant Client 19c 依赖 VC++ 2015-2022 Redistributable,缺少会导致 DLL 加载失败。
下载地址:https://aka.ms/vc14/vc_redist.x64.exe
:::
**步骤 2:在 php.ini 中启用扩展**
编辑 `D:\xampp8.2\php\php.ini`(注意不是 `apache\bin\php.ini`):
找到以下行:
```ini
;extension=pdo_oci ; Oracle PDO 驱动(默认被注释)
```
去掉前面的分号,改为:
```ini
extension=pdo_oci ; 启用 Oracle PDO 驱动(迁移工具依赖此扩展)
```
保存后重启 XAMPP(完全关闭再启动,不是只重启 Apache)。
**步骤 3:验证安装**
命令行验证:
```bash
# 检查 pdo_oci 扩展是否已加载
D:\xampp8.2\php\php.exe -m | findstr -i "pdo_oci"
# 列出所有可用的 PDO 驱动(应包含 oci)
D:\xampp8.2\php\php.exe -r "print_r(PDO::getAvailableDrivers());"
```
应看到 `pdo_oci` 输出,且 `PDO::getAvailableDrivers()` 返回的数组中包含 `oci`。
phpinfo() 验证:创建 `<?php phpinfo(); ?>` 放入 htdocs,浏览器访问后搜索 `pdo_oci`,能看到 PDO drivers 中有 `oci` 且 Oracle Instant Client 版本号显示即成功。
### SQLite(XAMPP 默认自带)
无需额外操作。验证:
```bash
# 检查 SQLite 驱动是否已加载
D:\xampp8.2\php\php.exe -m | findstr sqlite
```
### 驱动安装速查表
| 驱动 | XAMPP 默认 | 安装方式 | 验证命令 |
|------|-----------|----------|----------|
| pdo_mysql | ✅ 自带 | 无需安装 | `php -m \| findstr mysql` |
| pdo_pgsql | ✅ 自带 | php.ini 启用 | `php -m \| findstr pgsql` |
| pdo_sqlsrv | ❌ 需安装 | ODBC 驱动 + PECL 扩展 | `php -m \| findstr sqlsrv` |
| pdo_oci | ⚠️ DLL 自带,需启用 | InstantClient + VC++ 运行库 + php.ini 启用 | `php -m \| findstr pdo_oci` |
| pdo_sqlite | ✅ 自带 | 无需安装 | `php -m \| findstr sqlite` |
## 相关代码
| 文件 | 说明 |
|------|------|
| `app/command/DbMigrateCommand.php` | CLI 命令入口(参数解析、退出码) |
| `app/service/DbMigrateService.php` | 迁移引擎服务(连接、建库、建表、数据迁移、序列重置、行数校验) |
| `app/service/DbSchemaBuilder.php` | DDL 生成器(类型映射、CREATE TABLE / INDEX / FK / COMMENT 语句生成) |
| `config/console.php` | 命令注册 |
## 注意事项
:::warning 迁移前检查
1. 确保目标数据库用户有 CREATE TABLE / INSERT / CREATE INDEX / ALTER TABLE(外键)权限
2. PostgreSQL 迁移还需要 CREATE DATABASE 权限
3. SQL Server 迁移需要 sa 或有 CREATE DATABASE 权限的用户
4. Oracle 迁移需要 DBA 权限或具有 CREATE USER / CREATE SEQUENCE / CREATE TRIGGER 权限的账号
5. `--drop-target-first` 会删除目标表的全部数据(含索引、外键和注释),请谨慎使用
6. 大表迁移建议调大 `--batch-size`(如 5000)以提高效率
7. 迁移过程中如遇中断,已提交的 `--batch-size` 批次数据不会回滚,建议使用 `--drop-target-first` 重试
8. 含点号的数据库名(如 `rxthinkcmf.thinkphp8.elevue`)各驱动均已适配
9. 索引、外键和注释在建表阶段同步迁移,使用 `--drop-target-first` 重建时会一并重建
10. 外键约束在所有表创建后统一添加,避免引用顺序问题
11. 命令退出码:成功返回 0,失败或行数校验未通过返回 1(可用于 CI/CD 判断)
:::
## 常见问题
### Q: 迁移失败后如何重试?
A: 使用 `--drop-target-first` 选项重新执行,会先删除目标表再重建。
### Q: 如何只迁移部分表?
A: 使用 `--tables` 选项指定表名,逗号分隔:
```bash
php think db:migrate ... --tables=think_user,think_role
```
### Q: 大表迁移很慢怎么办?
A: 调大 `--batch-size`,如 `--batch-size=5000`。默认 1000 行一批,增大批量可减少数据库交互次数。
### Q: 目标库不存在会自动创建吗?
A: 会。所有驱动(MySQL / PostgreSQL / SQL Server / Oracle / SQLite)在目标库不存在时都会自动创建,无需手动建库。建库时会自动从源库读取 collation 并应用(仅当源与目标驱动相同时)。
### Q: PostgreSQL 报 SQLSTATE 08006 错误怎么办?
A: `08006` 是连接异常的通用错误码,在中文环境下错误信息可能乱码。工具会主动连接 `postgres` 维护库查询 `pg_database` 确认目标库是否不存在。如果确认不存在会自动建库;如果维护库也连不上,请检查 PostgreSQL 服务是否启动、端口是否正确、密码是否正确。
### Q: SQL Server 报"登录失败"怎么办?
A: SQL Server 数据库不存在时会返回"登录失败"(SQLSTATE 28000),工具会自动连接 `master` 库验证凭据并区分。如果确实是密码错误,请检查 `--dst-password`。
### Q: 索引、外键、注释、默认值会迁移吗?
A: 会。迁移工具在建表阶段会同步迁移以下元数据:
- **索引**:自动读取源表的非主键索引(UNIQUE / 普通 / 全文),在目标表创建后执行 `CREATE INDEX`
- **外键**:自动读取源表的外键约束,在所有表创建后统一添加 `ALTER TABLE ... ADD CONSTRAINT ... FOREIGN KEY`,避免引用顺序问题
- **字段注释**:MySQL 目标直接写入 `COMMENT` 子句;PostgreSQL / Oracle 使用 `COMMENT ON COLUMN`;SQL Server 使用 `sp_addextendedproperty`
- **表注释**:MySQL 使用 `ALTER TABLE COMMENT`;PostgreSQL / Oracle 使用 `COMMENT ON TABLE`
- **默认值**:完整保留,包括空字符串 `DEFAULT ''`、数值默认值、`CURRENT_TIMESTAMP`、`CURRENT_DATE`、函数调用(如 `uuid_generate_v4()`)等
- **enum/set**:自动迁移候选值,生成 `CHECK` 约束
- **decimal/datetime 精度**:保留精度参数(如 `decimal(10,2)` → `numeric(10,2)`,`datetime(3)` → `timestamp(3)`)
- **collation**:建库时自动应用源库 collation(仅同驱动时);建表时迁移表级和列级 collation(MySQL / PostgreSQL)
### Q: 数据迁移的可靠性如何保证?
A: 迁移工具有以下保障机制:
- **事务保护**:每个 `--batch-size`(默认 1000 行)为一个独立事务,提交后即生效;当前批次失败时回滚,已提交的历史批次不受影响
- **稳定排序**:分页查询基于主键列 `ORDER BY`,确保大表分批迁移不会漏行或重复
- **退出码**:命令成功返回 0,失败或行数校验未通过返回 1,可用于 CI/CD 自动化判断
- **行数校验**:迁移完成后逐表对比源和目标的行数,输出 PASS/FAIL
### Q: Oracle 自增列如何处理?
A: Oracle 没有原生自增语法。迁移工具会自动为自增列创建序列(`CREATE SEQUENCE`)和触发器(`CREATE TRIGGER`),实现等效的自增功能。
### Q: 含点号的数据库名支持吗?
A: 支持。如 `rxthinkcmf.thinkphp8.elevue` 这类含点号的库名,各驱动均已适配。
### Q: SQLite 作为目标有什么限制?
A: SQLite 不支持字段注释、表注释和 collation。迁移完成后数据完整,索引和外键正常迁移。