# 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.antdvue --src-username=root --src-password=root \ --dst-type=pgsql --dst-host=127.0.0.1 --dst-hostport=5432 \ --dst-database=rxthinkcmf.thinkphp8.antdvue --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.antdvue --src-username=root --src-password=root \ --dst-type=sqlsrv --dst-host=127.0.0.1 --dst-hostport=1433 \ --dst-database=rxthinkcmf.thinkphp8.antdvue --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.antdvue --src-username=root --src-password=root \ --dst-type=sqlite --dst-database=runtime/rxthinkcmf.thinkphp8.antdvue.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.antdvue --src-username=postgres --src-password=123456 \ --dst-type=mysql --dst-host=127.0.0.1 --dst-hostport=3306 \ --dst-database=rxthinkcmf.thinkphp8.antdvue --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.antdvue --src-username=sa --src-password=123456 \ --dst-type=mysql --dst-host=127.0.0.1 --dst-hostport=3306 \ --dst-database=rxthinkcmf.thinkphp8.antdvue --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.antdvue --src-username=root --src-password=root \ --dst-type=pgsql --dst-host=127.0.0.1 --dst-hostport=5432 \ --dst-database=rxthinkcmf.thinkphp8.antdvue --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.antdvue --src-username=root --src-password=root \ --dst-type=pgsql --dst-host=127.0.0.1 --dst-hostport=5432 \ --dst-database=rxthinkcmf.thinkphp8.antdvue --dst-username=postgres --dst-password=123456 \ --drop-target-first ============================================================== [迁移开始] 源: mysql:rxthinkcmf.thinkphp8.antdvue → 目标: pgsql:rxthinkcmf.thinkphp8.antdvue [迁移开始] 待迁移 25 张表 ============================================================== [·] [1/5] 建目标库... · 目标库 "rxthinkcmf.thinkphp8.antdvue" 已就绪 [✓] [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.antdvue --src-username=root --src-password=root \ --dst-type=sqlsrv --dst-host=127.0.0.1 --dst-hostport=1433 \ --dst-database=rxthinkcmf.thinkphp8.antdvue --dst-username=sa --dst-password=123456 \ --drop-target-first ============================================================== [迁移开始] 源: mysql:rxthinkcmf.thinkphp8.antdvue → 目标: sqlsrv:rxthinkcmf.thinkphp8.antdvue [迁移开始] 待迁移 25 张表 ============================================================== [·] [1/5] 建目标库... · 目标库 "rxthinkcmf.thinkphp8.antdvue" 已就绪 [✓] [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.antdvue --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.antdvue → 目标: oracle:rxthinkcmf.thinkphp8.antdvue [迁移开始] 待迁移 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.antdvue --src-username=root --src-password=root \ --dst-type=sqlite --dst-database=runtime/rxthinkcmf.thinkphp8.antdvue.db ============================================================== [迁移开始] 源: mysql:rxthinkcmf.thinkphp8.antdvue → 目标: sqlite:runtime/rxthinkcmf.thinkphp8.antdvue.db [迁移开始] 待迁移 25 张表 ============================================================== [·] [1/5] 建目标库... · 目标库 "runtime/rxthinkcmf.thinkphp8.antdvue.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.antdvue --src-username=root --src-password=root \ --dst-type=pgsql --dst-host=127.0.0.1 --dst-hostport=5432 \ --dst-database=rxthinkcmf.thinkphp8.antdvue --dst-username=postgres --dst-password=123456 \ --tables=think_user,think_role,think_menu \ --drop-target-first ============================================================== [迁移开始] 源: mysql:rxthinkcmf.thinkphp8.antdvue → 目标: pgsql:rxthinkcmf.thinkphp8.antdvue [迁移开始] 待迁移 3 张表 ============================================================== [·] [1/5] 建目标库... · 目标库 "rxthinkcmf.thinkphp8.antdvue" 已就绪 [✓] [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() 验证:创建 `` 放入 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.antdvue`)各驱动均已适配 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.antdvue` 这类含点号的库名,各驱动均已适配。 ### Q: SQLite 作为目标有什么限制? A: SQLite 不支持字段注释、表注释和 collation。迁移完成后数据完整,索引和外键正常迁移。