Skip to content

7.6 数据库迁移工具 ​

概述

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-host127.0.0.1源主机地址
--src-hostport自动推断源端口(mysql=3306, pgsql=5432, sqlsrv=1433, oracle=1521)
--src-username-源用户名(SQLite 除外)
--src-password-源密码(SQLite 除外)

目标连接选项 ​

选项默认值说明
--dst-host127.0.0.1目标主机地址
--dst-hostport自动推断目标端口
--dst-username-目标用户名(SQLite 除外)
--dst-password-目标密码(SQLite 除外)

迁移选项 ​

选项默认值说明
--drop-target-first否迁移前先删除目标库中的同名表
--batch-size1000每批读取的行数(影响内存占用与迁移速度)
--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.thinkphp6.elevue --src-username=root --src-password=root \
  --dst-type=pgsql --dst-host=127.0.0.1 --dst-hostport=5432 \
  --dst-database=rxthinkcmf.thinkphp6.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.thinkphp6.elevue --src-username=root --src-password=root \
  --dst-type=sqlsrv --dst-host=127.0.0.1 --dst-hostport=1433 \
  --dst-database=rxthinkcmf.thinkphp6.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.thinkphp6.elevue --src-username=root --src-password=root \
  --dst-type=sqlite --dst-database=runtime/rxthinkcmf.thinkphp6.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.thinkphp6.elevue --src-username=postgres --src-password=123456 \
  --dst-type=mysql --dst-host=127.0.0.1 --dst-hostport=3306 \
  --dst-database=rxthinkcmf.thinkphp6.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.thinkphp6.elevue --src-username=sa --src-password=123456 \
  --dst-type=mysql --dst-host=127.0.0.1 --dst-hostport=3306 \
  --dst-database=rxthinkcmf.thinkphp6.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.thinkphp6.elevue --src-username=root --src-password=root \
  --dst-type=pgsql --dst-host=127.0.0.1 --dst-hostport=5432 \
  --dst-database=rxthinkcmf.thinkphp6.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.thinkphp6.elevue --src-username=root --src-password=root \
    --dst-type=pgsql --dst-host=127.0.0.1 --dst-hostport=5432 \
    --dst-database=rxthinkcmf.thinkphp6.elevue --dst-username=postgres --dst-password=123456 \
    --drop-target-first

==============================================================
[迁移开始] 源: mysql:rxthinkcmf.thinkphp6.elevue → 目标: pgsql:rxthinkcmf.thinkphp6.elevue
[迁移开始] 待迁移 25 张表
==============================================================
[·] [1/5] 建目标库...
  · 目标库 "rxthinkcmf.thinkphp6.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.thinkphp6.elevue --src-username=root --src-password=root \
    --dst-type=sqlsrv --dst-host=127.0.0.1 --dst-hostport=1433 \
    --dst-database=rxthinkcmf.thinkphp6.elevue --dst-username=sa --dst-password=123456 \
    --drop-target-first

==============================================================
[迁移开始] 源: mysql:rxthinkcmf.thinkphp6.elevue → 目标: sqlsrv:rxthinkcmf.thinkphp6.elevue
[迁移开始] 待迁移 25 张表
==============================================================
[·] [1/5] 建目标库...
  · 目标库 "rxthinkcmf.thinkphp6.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.thinkphp6.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.thinkphp6.elevue → 目标: oracle:rxthinkcmf.thinkphp6.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
==============================================================

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.thinkphp6.elevue --src-username=root --src-password=root \
  --dst-type=sqlite --dst-database=runtime/rxthinkcmf.thinkphp6.elevue.db

==============================================================
[迁移开始] 源: mysql:rxthinkcmf.thinkphp6.elevue → 目标: sqlite:runtime/rxthinkcmf.thinkphp6.elevue.db
[迁移开始] 待迁移 25 张表
==============================================================
[·] [1/5] 建目标库...
  · 目标库 "runtime/rxthinkcmf.thinkphp6.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.thinkphp6.elevue --src-username=root --src-password=root \
    --dst-type=pgsql --dst-host=127.0.0.1 --dst-hostport=5432 \
    --dst-database=rxthinkcmf.thinkphp6.elevue --dst-username=postgres --dst-password=123456 \
    --tables=think_user,think_role,think_menu \
    --drop-target-first

==============================================================
[迁移开始] 源: mysql:rxthinkcmf.thinkphp6.elevue → 目标: pgsql:rxthinkcmf.thinkphp6.elevue
[迁移开始] 待迁移 3 张表
==============================================================
[·] [1/5] 建目标库...
  · 目标库 "rxthinkcmf.thinkphp6.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
==============================================================

类型映射 ​

源类型MySQLPostgreSQLSqlServerOracleSQLite
bigintbigintbigintbigintNUMBER(19)INTEGER
intintintegerintNUMBER(10)INTEGER
smallintsmallintsmallintsmallintNUMBER(5)INTEGER
tinyinttinyintsmallinttinyintNUMBER(3)INTEGER
decimaldecimalnumericdecimalNUMBERREAL
floatfloatrealfloatBINARY_FLOATREAL
doubledoubledouble precisionfloatBINARY_DOUBLEREAL
varcharvarcharvarcharnvarcharVARCHAR2TEXT
charcharcharncharCHARTEXT
texttexttextntextCLOBTEXT
blobblobbyteavarbinaryBLOBBLOB
datetimedatetimetimestampdatetimeTIMESTAMPTEXT
timestamptimestamptimestampdatetimeTIMESTAMPTEXT
datedatedatedateDATETEXT
booleantinyint(1)booleanbitNUMBER(1)INTEGER
jsonjsonjsonbntextCLOBTEXT

精度保留

  • 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序列重置
MySQLCREATE DATABASE ... COLLATE主键 ORDER BYAUTO_INCREMENT✅✅✅ COMMENT✅✅ 库/表/列跳过
PostgreSQLLC_COLLATE主键 ORDER BYGENERATED BY DEFAULT AS IDENTITY✅✅✅ COMMENT ON COLUMN✅✅ 库/列✅ setval
SQL ServerCOLLATE主键 ORDER BYIDENTITY(1,1)✅✅✅ sp_addextendedproperty✅✅ 库级✅ DBCC CHECKIDENT
OracleCREATE USER + GRANTROW_NUMBER()序列+触发器✅✅✅ COMMENT ON COLUMN✅❌✅ ALTER SEQUENCE
SQLite自动创建文件主键 ORDER BYINTEGER PRIMARY KEY AUTOINCREMENT✅✅❌❌❌跳过

PostgreSQL 特殊处理 ​

建库 ​

迁移到 PostgreSQL 时,工具会自动连接 postgres 维护库执行:

sql
-- 创建目标数据库(UTF-8 编码)
-- 若数据库已存在则跳过
-- LC_COLLATE / LC_CTYPE:仅当源与目标驱动相同时,从源库读取 collation 并应用
CREATE DATABASE "数据库名" WITH ENCODING 'UTF8'

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 [数据库名]

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 "用户名";

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;

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 驱动,需要安装两个组件。

先决条件

安装 ODBC 驱动前,系统需要 Microsoft Visual C++ Redistributable。大多数 Windows 已预装,如果安装失败,先安装它再重试。

步骤 1:安装 Microsoft ODBC Driver for SQL Server

这是底层通信组件,sqlsrv 扩展依赖它,必须安装。

步骤 2:下载 PHP 扩展 DLL

从微软官方 GitHub 下载:

解压后目录结构(平铺,无子目录):

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

版本匹配(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 驱动(迁移工具使用)
  1. 保存后重启 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. 重启终端或重启电脑使环境变量生效

安装 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.phpCLI 命令入口(参数解析、退出码)
app/service/DbMigrateService.php迁移引擎服务(连接、建库、建表、数据迁移、序列重置、行数校验)
app/service/DbSchemaBuilder.phpDDL 生成器(类型映射、CREATE TABLE / INDEX / FK / COMMENT 语句生成)
config/console.php命令注册

注意事项 ​

迁移前检查

  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.thinkphp6.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.thinkphp6.elevue 这类含点号的库名,各驱动均已适配。

Q: SQLite 作为目标有什么限制? ​

A: SQLite 不支持字段注释、表注释和 collation。迁移完成后数据完整,索引和外键正常迁移。

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