Laravel跨数据库迁移需规避timestamps()、enum()、foreignIdFor()等方法的平台差异,显式定义默认值、禁用时区字段、手动启用SQLite外键,并避免DB::raw()硬编码SQL。

MySQL 和 SQLite 迁移里 timestamps() 行为不一致
SQLite 默认不支持微秒级时间戳,而 MySQL 5.6+ 默认启用 useCurrentOnUpdate();Laravel 的 timestamps() 在两个驱动下生成的字段定义实际不同。
常见错误现象:php artisan migrate 在 SQLite 跑通,切到 MySQL 后报错 SQLSTATE[42000]: Syntax error or access violation: 1067 Invalid default value for 'updated_at'。
- 明确写死默认值:用
$table->timestamps()->useCurrent()->useCurrentOnUpdate();替代裸调timestamps() - SQLite 测试时加
DB_CONNECTION=sqlite php artisan migrate:fresh,但别依赖它“能过”就认为 MySQL 没问题 - 如果项目需长期双平台支持,避免在迁移中使用
datetimeTz()或timestampTz()—— SQLite 不支持时区字段
Laravel 9+ 的 enum() 字段在 PostgreSQL 和 MySQL 上语法冲突
enum() 是 Laravel 9.29+ 新增的 Schema 构建方法,但它底层直接映射数据库原生 ENUM 类型,而 PostgreSQL 和 MySQL 对枚举值的引号、大小写、空格容忍度完全不同。
使用场景:你正在写一个跨 MySQL/PostgreSQL 的 SaaS 多租户系统,需要统一用户状态字段。
- 别用
$table->enum('status', ['active', 'inactive'])—— PostgreSQL 要求枚举值必须提前CREATE TYPE,Laravel 不自动处理 - 改用
string()+ 模型层面约束(protected $casts = ['status' => StatusEnum::class];)更稳妥 - 若坚持用原生 ENUM,请按数据库分条件写迁移:
if (DB::getDriverName() === 'pgsql') { ... },但会增加维护成本
foreignIdFor() 在 SQLite 中无法创建外键约束
SQLite 3.32+ 才开始默认启用外键支持,且 Laravel 的 foreignIdFor() 生成的约束语句(如 FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE)在旧版 SQLite 或未启用外键时静默失效。
容易踩的坑:本地用 SQLite 开发时删数据没触发级联,上线 MySQL 后突然级联删除,导致数据不一致。
- 开发时强制开启 SQLite 外键:
DB::statement('PRAGMA foreign_keys = ON');放在AppServiceProvider::boot()里 - 不要依赖
foreignIdFor()自动推断类型 —— 显式指定类型,比如$table->foreignIdFor(User::class, 'author_id')->constrained()->onDelete('cascade'); - CI 环境跑测试前加检查:
php -r "echo (new PDO('sqlite::memory:'))->query('PRAGMA foreign_keys')->fetchColumn() ?: 'off';"
迁移文件里用 DB::raw() 写死 SQL 导致平台不可移植
比如在 up() 里写 DB::raw('ALTER TABLE users MODIFY name VARCHAR(255) COLLATE utf8mb4_unicode_ci'),这个语法在 PostgreSQL 就完全无效。
性能影响不大,但会让一次迁移在多个平台反复失败、回滚混乱,尤其涉及字符集、索引类型(如 MySQL 的 FULLTEXT)、JSON 函数时更明显。
- 优先用 Schema 构建器原语:
$table->string('name')->collation('utf8mb4_unicode_ci')(注意:collation 在 SQLite 会被忽略,但不会报错) - 真要执行平台特有 SQL?拆成多个迁移文件,用
if (DB::getDriverName() === 'mysql') { ... }包裹,并在文件名中标注平台,例如2023_01_01_000000_add_fulltext_index_for_mysql.php - 所有带
DB::raw()的行,旁边加注释说明目标平台,不然半年后你自己都看不懂为什么这里不能动
跨平台迁移最麻烦的不是语法差异,而是「看起来一样,实际行为不同」——比如 nullableTimestamps() 在 SQLite 里存 NULL 是字符串 'NULL',MySQL 里才是真正的 NULL。这种细节查日志都难定位,只能靠早期平台对齐测试卡住。


















