35 KiB
7.6 数据库迁移工具
:::tip 概述
db:migrate 是一个 CLI 命令行工具,支持在 MySQL / PostgreSQL / SQL Server / Oracle / SQLite 五种数据库之间任意组合迁移。完整迁移流程包含自动建库、建表结构(含索引、外键、注释、自增、collation)、分批迁移数据、重置自增序列、逐表行数校验 5 个步骤。数据迁移基于主键稳定排序,支持事务保护与自动回滚。目标库不存在时自动创建,支持含点号的数据库名。
:::
迁移流程
┌──────────────────────────────────────────────────────────────┐
│ 数据库迁移 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
命令格式
# =============================================================================
# 数据库迁移命令
# =============================================================================
# php artisan db:migrate → 调用 Laravel Artisan 的数据库迁移工具
# [选项] → 通过 --key=value 格式传入源/目标数据库连接参数
# =============================================================================
php artisan db:migrate [选项]
必填选项
| 选项 | 说明 |
|---|---|
--src-type |
源数据库驱动:mysql / pgsql / sqlsrv / oracle / sqlite |
--src-database |
源数据库名 |
--dst-type |
目标数据库驱动:mysql / pgsql / sqlsrv / oracle / sqlite |
--dst-database |
目标数据库名(SQLite 时为文件路径) |
源连接选项
| 选项 | 默认值 | 说明 |
|---|---|---|
--src-host |
127.0.0.1 |
源主机地址 |
--src-port |
自动推断 | 源端口(mysql=3306, pgsql=5432, sqlsrv=1433, oracle=1521) |
--src-username |
- | 源用户名(SQLite 除外) |
--src-password |
- | 源密码(SQLite 除外) |
目标连接选项
| 选项 | 默认值 | 说明 |
|---|---|---|
--dst-host |
127.0.0.1 |
目标主机地址 |
--dst-port |
自动推断 | 目标端口 |
--dst-username |
- | 目标用户名(SQLite 除外) |
--dst-password |
- | 目标密码(SQLite 除外) |
迁移选项
| 选项 | 默认值 | 说明 |
|---|---|---|
--drop-target-first |
否 | 迁移前先删除目标库中的同名表 |
--batch-size |
1000 |
每批读取的行数(影响内存占用与迁移速度) |
--tables |
全部 | 只迁移指定表(逗号分隔,不含前缀) |
:::warning 参数名为 --src-port / --dst-port
选项是 --src-port / --dst-port(不是 hostport),与签名 DbMigrateCommand::$signature 保持一致。端口缺省时按驱动自动推断。
:::
迁移命令签名
MySQL → PostgreSQL
# -----------------------------------------------------------------------------
# MySQL 迁移到 PostgreSQL
# -----------------------------------------------------------------------------
# --src-* → 源数据库连接参数(MySQL)
# --dst-* → 目标数据库连接参数(PostgreSQL)
# --drop-target-first → 迁移前先删除目标库中的同名表(避免表已存在报错)
php artisan db:migrate \
--src-type=mysql --src-host=127.0.0.1 --src-port=3306 \
--src-database=rxthinkcmf.laravel13.elevue --src-username=root --src-password=root \
--dst-type=pgsql --dst-host=127.0.0.1 --dst-port=5432 \
--dst-database=rxthinkcmf.laravel13.elevue --dst-username=postgres --dst-password=123456 \
--drop-target-first
MySQL → SQL Server
# -----------------------------------------------------------------------------
# MySQL 迁移到 SQL Server
# -----------------------------------------------------------------------------
# --dst-type=sqlsrv → 目标驱动为 SQL Server
# --dst-port=1433 → SQL Server 默认端口
php artisan db:migrate \
--src-type=mysql --src-host=127.0.0.1 --src-port=3306 \
--src-database=rxthinkcmf.laravel13.elevue --src-username=root --src-password=root \
--dst-type=sqlsrv --dst-host=127.0.0.1 --dst-port=1433 \
--dst-database=rxthinkcmf.laravel13.elevue --dst-username=sa --dst-password=123456 \
--drop-target-first
MySQL → SQLite
# -----------------------------------------------------------------------------
# MySQL 迁移到 SQLite
# -----------------------------------------------------------------------------
# --dst-type=sqlite → 目标驱动为 SQLite(无需用户名密码)
# --dst-database → SQLite 文件路径(文件不存在时自动创建)
php artisan db:migrate \
--src-type=mysql --src-host=127.0.0.1 --src-port=3306 \
--src-database=rxthinkcmf.laravel13.elevue --src-username=root --src-password=root \
--dst-type=sqlite --dst-database=database/rxthinkcmf.laravel13.elevue.db
MySQL → Oracle
# -----------------------------------------------------------------------------
# MySQL 迁移到 Oracle
# -----------------------------------------------------------------------------
# --dst-type=oracle → 目标驱动为 Oracle(自动建用户 + 序列 + 触发器)
php artisan db:migrate \
--src-type=mysql --src-host=127.0.0.1 --src-port=3306 \
--src-database=rxthinkcmf.laravel13.elevue --src-username=root --src-password=root \
--dst-type=oracle --dst-host=127.0.0.1 --dst-port=1521 \
--dst-database=rxthinkcmf.laravel13.elevue --dst-username=system --dst-password=123456 \
--drop-target-first
MySQL → MySQL(同实例跨库复制)
# -----------------------------------------------------------------------------
# MySQL 同库复制(源和目标在同一 MySQL 实例,数据库名不同)
# -----------------------------------------------------------------------------
# 不带 --drop-target-first → 目标表已存在时跳过(不删除)
php artisan db:migrate \
--src-type=mysql --src-host=127.0.0.1 --src-port=3306 \
--src-database=source_db --src-username=root --src-password=root \
--dst-type=mysql --dst-host=127.0.0.1 --dst-port=3306 \
--dst-database=target_db --dst-username=root --dst-password=root
PostgreSQL → MySQL
# -----------------------------------------------------------------------------
# PostgreSQL 迁移到 MySQL
# -----------------------------------------------------------------------------
php artisan db:migrate \
--src-type=pgsql --src-host=127.0.0.1 --src-port=5432 \
--src-database=rxthinkcmf.laravel13.elevue --src-username=postgres --src-password=123456 \
--dst-type=mysql --dst-host=127.0.0.1 --dst-port=3306 \
--dst-database=rxthinkcmf.laravel13.elevue --dst-username=root --dst-password=root \
--drop-target-first
SQL Server → MySQL
# -----------------------------------------------------------------------------
# SQL Server 迁移到 MySQL
# -----------------------------------------------------------------------------
php artisan db:migrate \
--src-type=sqlsrv --src-host=127.0.0.1 --src-port=1433 \
--src-database=rxthinkcmf.laravel13.elevue --src-username=sa --src-password=123456 \
--dst-type=mysql --dst-host=127.0.0.1 --dst-port=3306 \
--dst-database=rxthinkcmf.laravel13.elevue --dst-username=root --dst-password=root \
--drop-target-first
只迁移指定表
# -----------------------------------------------------------------------------
# 只迁移指定表(逗号分隔表名,不含前缀)
# -----------------------------------------------------------------------------
# --tables=laravel_user,laravel_role,laravel_menu → 只迁移这 3 张表
php artisan db:migrate \
--src-type=mysql --src-host=127.0.0.1 --src-port=3306 \
--src-database=rxthinkcmf.laravel13.elevue --src-username=root --src-password=root \
--dst-type=pgsql --dst-host=127.0.0.1 --dst-port=5432 \
--dst-database=rxthinkcmf.laravel13.elevue --dst-username=postgres --dst-password=123456 \
--tables=laravel_user,laravel_role,laravel_menu \
--drop-target-first
迁移输出日志
MySQL → PostgreSQL 完整日志
$ php artisan db:migrate \
--src-type=mysql --src-host=127.0.0.1 --src-port=3306 \
--src-database=rxthinkcmf.laravel13.elevue --src-username=root --src-password=root \
--dst-type=pgsql --dst-host=127.0.0.1 --dst-port=5432 \
--dst-database=rxthinkcmf.laravel13.elevue --dst-username=postgres --dst-password=123456 \
--drop-target-first
==============================================================
[迁移开始] 源: mysql:rxthinkcmf.laravel13.elevue → 目标: pgsql:rxthinkcmf.laravel13.elevue
[迁移开始] 待迁移 25 张表
==============================================================
[·] [1/5] 建目标库...
· 目标库 "rxthinkcmf.laravel13.elevue" 已就绪
[✓] [1/5] 建目标库 完成(耗时 0ms)
[·] [2/5] 建表结构(25 张表)...
[✓] [2/5] 建表结构 完成(耗时 440ms),25 张表,118 个索引,0 个外键
[·] [3/5] 逐表迁移数据...
· [1/25] 正在迁移 laravel_article...
[laravel_article] +13(累计 13)
[✓ laravel_article] 共 13 行(耗时 10ms)
· [2/25] 正在迁移 laravel_city...
[laravel_city] +1000(累计 1000)
...(大表分批输出进度,每批 1000 行)...
[laravel_city] +562(累计 783562)
[✓ laravel_city] 共 783562 行(耗时 10m10s)
...(其余表类似)...
[✓] [3/5] 逐表迁移数据 完成(耗时 10m10s),共 785215 行
[·] [4/5] 重置自增序列...
[✓] [4/5] 重置自增序列 完成(耗时 23ms)
[·] [5/5] 逐表行数校验...
[laravel_article] source=13 target=13 PASS
[laravel_city] source=783562 target=783562 PASS
...(其余表类似)...
[✓] [5/5] 逐表行数校验 完成(耗时 452ms),25 张表全部 PASS
==============================================================
[迁移完成] 成功:785215 行 / 25 张表 / 总耗时 10m11s
==============================================================
MySQL → Oracle 日志摘要
$ php artisan db:migrate \
--src-type=mysql --src-host=127.0.0.1 --src-port=3306 \
--src-database=rxthinkcmf.laravel13.elevue --src-username=root --src-password=root \
--dst-type=oracle --dst-host=127.0.0.1 --dst-port=1521 \
--dst-database=rxthinkcmf --dst-username=system --dst-password=123456 \
--drop-target-first
==============================================================
[迁移开始] 源: mysql:rxthinkcmf.laravel13.elevue → 目标: oracle:rxthinkcmf.laravel13.elevue
==============================================================
[·] [1/5] 建目标库...
· 目标库 "rxthinkcmf" 已就绪(CREATE USER + GRANT)
[✓] [1/5] 建目标库 完成(耗时 0ms)
[·] [2/5] 建表结构(25 张表)...
· 为 laravel_article 创建序列+触发器(自增列 id)
· 为 laravel_city 创建序列+触发器(自增列 id)
...(其余含自增列的表类似)...
[✓] [2/5] 建表结构 完成(耗时 2.1s)
[·] [3/5] 逐表迁移数据...
...(Oracle INSERT 耗时较 MySQL/SQLite 更长)...
[✓] [3/5] 逐表迁移数据 完成(耗时 15m30s),共 785215 行
[·] [4/5] 重置自增序列...
· 重置序列: laravel_article_id_seq → 14
· 重置序列: laravel_city_id_seq → 783563
[✓] [4/5] 重置自增序列 完成(耗时 180ms)
[·] [5/5] 逐表行数校验...
...(25 张表全部 PASS)...
==============================================================
[迁移完成] 成功:785215 行 / 25 张表 / 总耗时 15m33s
==============================================================
:::tip Oracle 迁移要点
- 建库:Oracle 通过
CREATE USER创建用户,库名即用户名,需要 DBA 权限 - 自增列:自动创建序列(
_seq)和触发器(_trg),插入时自动填充——与应用侧OracleProcessor的主键回填约定一致 - 序列重置:迁移完成后重置到 MAX+1
- 分页:使用
ROW_NUMBER() OVER (ORDER BY 主键)保证稳定排序 - 字段名:Oracle 默认大写,工具自动处理大小写转换 :::
只迁移指定表日志
$ php artisan db:migrate \
--src-type=mysql --src-host=127.0.0.1 --src-port=3306 \
--src-database=rxthinkcmf.laravel13.elevue --src-username=root --src-password=root \
--dst-type=pgsql --dst-host=127.0.0.1 --dst-port=5432 \
--dst-database=rxthinkcmf.laravel13.elevue --dst-username=postgres --dst-password=123456 \
--tables=laravel_user,laravel_role,laravel_menu \
--drop-target-first
==============================================================
[迁移开始] 待迁移 3 张表
==============================================================
[·] [2/5] 建表结构(3 张表)...
· 已删除目标表: laravel_user
· 已删除目标表: laravel_role
· 已删除目标表: laravel_menu
[✓] [2/5] 建表结构 完成(耗时 85ms),3 张表,5 个索引,0 个外键
[·] [3/5] 逐表迁移数据...
· [1/3] 正在迁移 laravel_user...
[laravel_user] +22(累计 22)
[✓ laravel_user] 共 22 行(耗时 9ms)
...(其余表类似)...
[✓] [3/5] 逐表迁移数据 完成(耗时 30ms),共 274 行
[·] [4/5] 重置自增序列...
[✓] [4/5] 重置自增序列 完成(耗时 25ms)
[·] [5/5] 逐表行数校验...
[laravel_user] source=22 target=22 PASS
[laravel_role] source=24 target=24 PASS
[laravel_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)→ PostgreSQLnumeric(10,2)(精度不丢失)datetime(3)→ PostgreSQLtimestamp(3)(毫秒精度保留)enum('a','b','c')→ PostgreSQLvarchar+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 维护库执行:
-- 创建目标数据库(UTF-8 编码)
-- 若数据库已存在则跳过
-- LC_COLLATE / LC_CTYPE:仅当源与目标驱动相同时,从源库读取 collation 并应用
CREATE DATABASE "数据库名" WITH ENCODING 'UTF8'
:::warning PgSQL 数据库不存在的判定
PostgreSQL 数据库不存在时返回的 SQLSTATE 可能是 3D000(明确不存在)或 08006(连接异常,中文环境下错误信息可能乱码)。工具会主动连接 postgres 维护库查询 pg_database 确认目标库是否真的不存在,避免误判。
:::
序列重置
迁移完成后,对每张有自增主键的表分两步重置序列:
// 第一步:通过 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 特殊处理
建库
-- 连接 master 库,创建目标数据库
-- COLLATE:仅当源与目标驱动相同时,从源库读取 collation 并应用
CREATE DATABASE [数据库名]
:::warning SQL Server "登录失败"的判定
SQL Server 数据库不存在时返回 SQLSTATE 28000 并提示"登录失败",容易与密码错误混淆。工具会尝试连接 master 库验证凭据:若 master 连接成功说明凭据正确、是数据库不存在;若 master 也失败则为密码错误。
:::
自增列迁移
数据迁移时自动处理 IDENTITY_INSERT:
-- 迁移前开启(允许显式写入自增列的值)
SET IDENTITY_INSERT [表名] ON
-- 批量 INSERT ...
-- 迁移后关闭
SET IDENTITY_INSERT [表名] OFF
序列重置
-- 重置标识种子到指定值,下一次插入自动加一
DBCC CHECKIDENT ('[表名]', RESEED, {$maxId})
Oracle 特殊处理
建库
Oracle 的"建库"实际上是创建用户并授权。工具自动连接 Oracle 实例(默认服务名 XE),执行:
-- 创建用户(库名即用户名,密码与连接密码一致)
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 没有原生自增语法,工具自动创建序列+触发器实现等效功能:
-- 创建序列(从 1 开始,每次递增 1,不缓存)
-- 序列名规则:表名_列名_SEQ(大写)
CREATE SEQUENCE "TABLE_ID_SEQ" START WITH 1 INCREMENT BY 1 NOCACHE;
-- 创建触发器(插入前自动取序列值填充自增列)
-- 触发器名规则:表名_列名_TRG(大写)
-- IF :NEW.COL IS NULL 判断:允许显式传值,仅当未指定时才自动取序列
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;
:::tip 与应用侧的衔接
序列命名 {表}_{列}_seq(大写)与应用侧 App\Support\Database\OracleProcessor 的 insertGetId 回填约定完全一致(见 5.16 统一响应与异常抛出 相关说明与 4.9 数据库设计)。
:::
序列重置
-- 重置序列到 MAX(id)+1
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 扩展。Laragon 默认自带 MySQL、PostgreSQL 和 SQLite 驱动,SQL Server 和 Oracle 需手动安装。
运行以下命令检查已安装的驱动:
# 检查已安装的 PDO 驱动(-m 列出扩展,findstr /i 不区分大小写过滤)
$PHP_HOME\php.exe -m | findstr /i pdo
# 列出所有可用的 PDO 驱动
$PHP_HOME\php.exe -r "print_r(PDO::getAvailableDrivers());"
MySQL(Laragon 默认自带)
无需额外操作。验证:
$PHP_HOME\php.exe -m | findstr mysql
PostgreSQL(DLL 随包自带,php.ini 默认注释)
ext\php_pdo_pgsql.dll 与 libpq.dll 已随 PHP 包附带,但 $PHP_HOME\php.ini 扩展区的
extension=pdo_pgsql 默认是注释状态,需去掉分号启用:
;extension=pdo_odbc
extension=pdo_pgsql ← 去掉此行分号
extension=pdo_sqlite
验证(pdo_pgsql 出现即可,PDO::getAvailableDrivers() 应包含 pgsql):
$PHP_HOME\php.exe -m | findstr pgsql
$PHP_HOME\php.exe -r "print_r(PDO::getAvailableDrivers());"
CLI 每次进程都会重读 php.ini,无需重启;跑在 Apache(mod_php)下的需重启 Apache 才生效。
未启用时执行 --dst-type=pgsql 会报 could not find driver。
SQL Server(需手动安装)
Laragon 默认不带 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.13.3RTW.zip(本项目基于 PHP 8.3 起步,该包覆盖 PHP 8.3 / 8.4 / 8.5)
解压后目录结构(平铺,无子目录),文件命名规则:php_{驱动}_{PHP版本}_{线程安全}_{架构}.dll
- 驱动:
sqlsrv或pdo_sqlsrv - PHP 版本:
83(PHP 8.3)/84(PHP 8.4)/85(PHP 8.5) - 线程安全:
ts(Thread Safe)/nts(Non-Thread Safe) - 架构:
x64/x86
Windows_5.13.3RTW.zip 解压后共 24 个 DLL:
php_sqlsrv_83_nts_x64.dll php_sqlsrv_83_nts_x86.dll
php_sqlsrv_83_ts_x64.dll php_sqlsrv_83_ts_x86.dll
php_sqlsrv_84_nts_x64.dll php_sqlsrv_84_nts_x86.dll
php_sqlsrv_84_ts_x64.dll php_sqlsrv_84_ts_x86.dll
php_sqlsrv_85_nts_x64.dll php_sqlsrv_85_nts_x86.dll
php_sqlsrv_85_ts_x64.dll php_sqlsrv_85_ts_x86.dll
php_pdo_sqlsrv_83_nts_x64.dll php_pdo_sqlsrv_83_nts_x86.dll
php_pdo_sqlsrv_83_ts_x64.dll php_pdo_sqlsrv_83_ts_x86.dll
php_pdo_sqlsrv_84_nts_x64.dll php_pdo_sqlsrv_84_nts_x86.dll
php_pdo_sqlsrv_84_ts_x64.dll php_pdo_sqlsrv_84_ts_x86.dll
php_pdo_sqlsrv_85_nts_x64.dll php_pdo_sqlsrv_85_nts_x86.dll
php_pdo_sqlsrv_85_ts_x64.dll php_pdo_sqlsrv_85_ts_x86.dll
将以下两个文件复制到 $PHP_HOME\ext\(本项目用 PHP 8.3 TS x64):
php_sqlsrv_83_ts_x64.dllphp_pdo_sqlsrv_83_ts_x64.dll
:::tip 版本匹配(PHP 8.3 + TS + x64)
- PHP 版本:
83= PHP 8.3 - 线程安全:Laragon 的 Apache 用 TS(运行
$PHP_HOME\php.exe -i | findstr "Thread Safety"确认 →enabled) - 架构:
x64(64 位系统) :::
步骤 3:在 php.ini 中启用扩展
编辑 $PHP_HOME\php.ini:
- 启用前置扩展:找到
;extension=mbstring去掉分号(sqlsrv 依赖 mbstring) - 在 mbstring 之后添加以下两行(文件名必须与 ext 目录中的实际文件一致):
; SQL Server PHP 扩展(必须在 mbstring 之后加载)
extension=php_sqlsrv_83_ts_x64.dll ; SQL Server 核心驱动
extension=php_pdo_sqlsrv_83_ts_x64.dll ; SQL Server PDO 驱动(迁移工具使用)
- 保存后重启 Apache(命令行验证无需重启 Apache,只需重新运行 php 命令)
步骤 4:验证安装
# 检查 SQL Server 驱动是否已加载(应看到 sqlsrv 和 pdo_sqlsrv 两行)
$PHP_HOME\php.exe -m | findstr sqlsrv
Oracle(需手动配置)
迁移工具基于 PDO 连接数据库,需要 pdo_oci 扩展,需安装 Oracle Instant Client 并在 php.ini 中启用 pdo_oci(完整图文步骤见 2.1 开发环境准备「数据库安装 → Oracle」)。
步骤摘要:
- 安装 Oracle Instant Client(Basic Package,x64,推荐 19c),解压到固定路径
C:\oracle\instantclient_19_20(⚠️ 避免空格与中文) - 将该路径添加到系统
PATH - 安装 VC++ 2015-2022 Redistributable(x64):https://aka.ms/vc14/vc_redist.x64.exe
- 编辑
$PHP_HOME\php.ini,启用extension=pdo_oci - 完全重启 Laragon(不是只重启 Apache)
:::warning 安装 VC++ 运行库 Oracle Instant Client 19c 依赖 VC++ 2015-2022 Redistributable,缺少会导致 DLL 加载失败。 :::
验证安装:
# 检查 pdo_oci 扩展是否已加载
$PHP_HOME\php.exe -m | findstr -i "pdo_oci"
# 列出所有可用的 PDO 驱动(应包含 oci)
$PHP_HOME\php.exe -r "print_r(PDO::getAvailableDrivers());"
应看到 pdo_oci 输出,且 PDO::getAvailableDrivers() 返回的数组中包含 oci。
SQLite(默认自带)
无需额外操作。验证:
$PHP_HOME\php.exe -m | findstr sqlite
驱动安装速查表
| 驱动 | Laragon 默认 | 安装方式 | 验证命令 |
|---|---|---|---|
| pdo_mysql | ✅ 自带 | 无需安装 | php -m | findstr mysql |
| pdo_pgsql | ✅ 自带 | php.ini 启用 | php -m | findstr pgsql |
| pdo_sqlsrv | ❌ 需安装 | ODBC 驱动 + 扩展 DLL(83_ts_x64) | php -m | findstr sqlsrv |
| pdo_oci | ❌ 需配置 | InstantClient + VC++ 运行库 + php.ini 启用 | php -m | findstr pdo_oci |
| pdo_sqlite | ✅ 自带 | 无需安装 | php -m | findstr sqlite |
相关代码
| 文件 | 说明 |
|---|---|
app/Console/Commands/DbMigrateCommand.php |
CLI 命令入口(参数解析、退出码,Laravel 自动发现注册) |
app/Services/DbMigrateService.php |
迁移引擎服务(连接、建库、建表、数据迁移、序列重置、行数校验) |
app/Services/DbSchemaBuilder.php |
DDL 生成器(类型映射、CREATE TABLE / INDEX / FK / COMMENT 语句生成) |
注意事项
:::warning 迁移前检查
- 确保目标数据库用户有 CREATE TABLE / INSERT / CREATE INDEX / ALTER TABLE(外键)权限
- PostgreSQL 迁移还需要 CREATE DATABASE 权限
- SQL Server 迁移需要 sa 或有 CREATE DATABASE 权限的用户
- Oracle 迁移需要 DBA 权限或具有 CREATE USER / CREATE SEQUENCE / CREATE TRIGGER 权限的账号
--drop-target-first会删除目标表的全部数据(含索引、外键和注释),请谨慎使用- 大表迁移建议调大
--batch-size(如 5000)以提高效率 - 迁移过程中如遇中断,已提交的
--batch-size批次数据不会回滚,建议使用--drop-target-first重试 - 含点号的数据库名(如
rxthinkcmf.laravel13.elevue)各驱动均已适配 - 索引、外键和注释在建表阶段同步迁移,使用
--drop-target-first重建时会一并重建 - 外键约束在所有表创建后统一添加,避免引用顺序问题
- 命令退出码:成功返回 0,失败或行数校验未通过返回 1(可用于 CI/CD 判断) :::
常见问题
Q: 迁移失败后如何重试?
A: 使用 --drop-target-first 选项重新执行,会先删除目标表再重建。
Q: 如何只迁移部分表?
A: 使用 --tables 选项指定表名,逗号分隔(不含前缀):
php artisan db:migrate ... --tables=laravel_user,laravel_role
Q: 大表迁移很慢怎么办?
A: 调大 --batch-size,如 --batch-size=5000。默认 1000 行一批,增大批量可减少数据库交互次数。
Q: 目标库不存在会自动创建吗?
A: 会。所有驱动(MySQL / PostgreSQL / SQL Server / Oracle / SQLite)在目标库不存在时都会自动创建,无需手动建库。建库时会自动从源库读取 collation 并应用(仅当源与目标驱动相同时)。
PostgreSQL 走维护库 postgres 执行 CREATE DATABASE,含点号的库名会自动加双引号(如 "rxthinkcmf.laravel13.elevue");建库失败时抛出 创建目标数据库失败: … 并把原始异常保留在 previous 中。
Q: 报 could not find driver 怎么办?
A: PHP 缺少目标驱动的 PDO 扩展。迁到 PostgreSQL 需要 pdo_pgsql:在 $PHP_HOME\php.ini 扩展区去掉 extension=pdo_pgsql 的分号(DLL 随包自带,无需下载),CLI 重读 php.ini 即生效;Linux 用 apt install php8.3-pgsql。可用 $PHP_HOME\php.exe -r "print_r(PDO::getAvailableDrivers());" 确认驱动已加载。
Q: PostgreSQL 报 SQLSTATE 08006 错误怎么办?
A: 08006 是连接异常的通用错误码,服务端消息可能是英文(database "x" does not exist)或中文(数据库 "x" 不存在),中文环境下控制台还可能出现乱码(先 chcp 65001 或换 Windows Terminal 查看)。工具对两种语言的消息都会识别,并主动连接 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、函数调用等 - 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),实现等效的自增功能;序列命名与应用侧 OracleProcessor 主键回填约定一致。
Q: 含点号的数据库名支持吗?
A: 支持。如 rxthinkcmf.laravel13.elevue 这类含点号的库名,各驱动均已适配。
Q: SQLite 作为目标有什么限制?
A: SQLite 不支持字段注释、表注释和 collation。迁移完成后数据完整,索引和外键正常迁移。
Q: 与 document/ 下的 SQL 脚本有何区别?
A: document/ 下的五套 SQL 脚本是静态初始化脚本(建库 + 初始数据,见 2.5 数据库初始化);db:migrate 是活的迁移工具,用于把一个已有数据库完整搬到另一个数据库(结构 + 数据 + 元数据),两者互补。