laravel 迁移中使用 enum() 必须传入字符串数组,不可直接传枚举类或 cases() 实例;推荐标量枚举(如 enum status: string),用 array_column(status::cases(), 'value') 提取值,并注意 mysql/postgresql/sqlite 兼容性差异及对应约束与回滚处理。

MySQL 中用 enum() 定义枚举字段
MySQL 原生支持 ENUM 类型,Laravel 的 Schema::create() 和 Schema::table() 提供了 enum() 方法,但只接受字符串数组——不是枚举类本身,也不是 cases() 返回的 UnitEnum 实例数组。
常见错误是直接写 UserType::cases(),结果报错:Array to string conversion 或类型不匹配。
- 必须显式提取每个 case 的
name(标量枚举)或value(带底层类型的枚举) - 推荐用
enum枚举(enum Status: string),这样->name和->value一致,语义清晰 - 迁移中写法示例:
use App\Enums\Status;
Schema::create('posts', function (Blueprint $table) {
$table->id();
$table->enum('status', array_column(Status::cases(), 'value'))->default(Status::Draft->value);
});
注意:array_column(Status::cases(), 'value') 是最安全的提取方式;若用 name,需确保枚举定义里 case Draft = 'draft' 这类显式赋值,否则 name 是常量名(如 'Draft'),和数据库习惯不符。
PostgreSQL 不支持 ENUM 字段原生写法
Laravel 的 enum() 方法在 PostgreSQL 驱动下会静默退化为 string(),不会创建真正的 ENUM 类型,也不会报错——这容易导致本地开发(MySQL)和生产(PostgreSQL)行为不一致。
真正兼容 PostgreSQL 的做法是手动建类型 + 字段约束:
- 先用
DB::statement()创建自定义类型:CREATE TYPE "user_status" AS ENUM ('active', 'inactive', 'pending') - 再用
$table->string('status')->default('active'),并在模型中加casts和访问器约束值域 - 或者更稳妥:放弃数据库层 ENUM,改用
string()+ 模型层校验 + 数据库 CHECK 约束(需手动DB::statement()添加)
示例 CHECK 约束写法(PostgreSQL):
DB::statement("ALTER TABLE users ADD CONSTRAINT users_status_check CHECK (status IN ('active', 'inactive', 'pending'))");
这个约束无法通过 Schema 构建器生成,必须在迁移中用 DB::statement() 补上。
SQLite 完全不支持 ENUM,迁移会失败
SQLite 没有 ENUM 类型,Laravel 在 SQLite 下调用 $table->enum() 会抛出异常:SQLSTATE[HY000]: General error: 1 near "enum": syntax error。
解决方案只有两个:
- 测试/本地开发时换用 MySQL 或 PostgreSQL(推荐 Docker Compose 统一环境)
- 坚持用 SQLite,则必须改用
$table->string('status'),并在模型中用casts映射到枚举类,靠 PHP 层保证合法性
模型中配合写法:
protected $casts = [
'status' => Status::class,
];
此时数据库只是字符串字段,但读写时自动转成枚举实例,兼顾可移植性与类型安全。缺点是无法在 SQL 层做枚举值过滤(如 WHERE status = 'active' 仍可工作,但没类型约束)。
跨数据库迁移时 enum 字段的回滚陷阱
很多人在 down() 方法里直接写 $table->dropColumn('status'),看起来没问题——但在 PostgreSQL 或 SQLite 下,如果该字段绑定了自定义类型或 CHECK 约束,单纯删字段会失败。
关键点:
- MySQL:删字段即可,ENUM 类型随表销毁
- PostgreSQL:必须先
DROP CONSTRAINT(如果是 CHECK),或DROP TYPE(如果是自定义 ENUM 类型),再删字段 - SQLite:无额外清理成本,但要注意
down()里删字段前,确认没其他迁移依赖它
所以,只要用了非 MySQL 的适配逻辑,down() 就不能偷懒。比如 PostgreSQL 下的完整回滚:
public function down(Blueprint $table)
{
DB::statement("ALTER TABLE users DROP CONSTRAINT IF EXISTS users_status_check");
$table->dropColumn('status');
}
多数据库适配不是“写一次跑三处”,而是每种数据库都要单独验证字段定义、约束、回滚路径——最容易被忽略的是,你本地用 MySQL 跑通了,CI 流水线用 PostgreSQL 却卡在 migrate:rollback 上,问题就出在这里。











