MySQL 8.0+ 使用 $table->json() 可生成原生 JSON 字段并支持 whereJsonContains 等查询,低版本仅 fallback 为 TEXT 导致 JSON 查询失效;必须设 utf8mb4 字符集防 emoji 插入失败,函数索引需用虚拟列+原生 SQL 实现。

MySQL 8.0+ 直接用 $table->json() 最省心
只要数据库是 MySQL 8.0+(或 MariaDB 10.2+),$table->json('meta') 就会生成原生 JSON 类型字段,Laravel 的 whereJsonContains、whereJsonLength 等方法才能真正生效。低版本 MySQL(如 5.7)虽也支持 JSON 类型,但部分函数(如 JSON_TABLE)不可用,导致某些查询降级失败。
常见错误现象:SQLSTATE[HY000]: General error: 1366 Incorrect string value——这往往不是 JSON 本身的问题,而是表字符集没设成 utf8mb4,导致 emoji 或生僻字写入失败。
- 迁移中必须显式指定字符集:
Schema::create('users', function (Blueprint $table) { $table->charset = 'utf8mb4'; $table->collation = 'utf8mb4_unicode_ci'; ... }); - 如果已有表,需先
DB::statement("ALTER TABLE users CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci"),再跑迁移 -
$table->json('meta')->nullable()是安全写法,避免空值插入时报错
MySQL 5.7 或更低版本不能依赖 $table->json()
Laravel 在旧版 MySQL 中遇到 $table->json() 会自动 fallback 到 TEXT 字段,但此时所有 JSON 查询方法(whereJsonContains 等)全部失效——因为底层没有 JSON 函数支持,Laravel 只是把数组 json_encode 成字符串存进去,查的时候根本没法解析路径。
如果你无法升级数据库,又必须用 JSON 查询逻辑,有两个选择:
- 改用
$table->text('meta')+ 模型$casts = ['meta' => 'array'],仅解决读写自动编解码,查询靠 PHP 层过滤(适合小数据量) - 手动在迁移中用
DB::statement()建原生 JSON 字段(不推荐,破坏迁移可移植性) - 最务实的方案:升级 MySQL,或换 PostgreSQL(原生
jsonb支持更稳定)
跨数据库兼容写法不存在,但可以收敛风险
别信“一套迁移跑遍 MySQL/PostgreSQL/SQLite”的说法。SQLite 根本没有 JSON 类型,Laravel 只是模拟;PostgreSQL 的 jsonb 和 MySQL 的 JSON 行为差异明显(比如索引方式、NULL 处理)。强行抽象只会让问题延迟爆发。
真正可行的兼容策略是分层控制:
- 迁移文件里只用
$table->json(),并明确文档标注“要求 MySQL 8.0+ 或 PostgreSQL” - 模型中
$casts统一用'meta' => 'array',不依赖数据库类型做判断 - JSON 查询逻辑(如
whereJsonContains)封装进 Repository 或 Service,内部按 DB 连接类型路由:MySQL 走原生函数,SQLite 走 Collection filter - CI 流程中加检查:
DB::select("SELECT VERSION()")[0]->VERSION,防止低版本 MySQL 被误部署
别在迁移里直接建函数索引,会触发 Doctrine DBAL 报错
想对 meta->name 加索引?别在 Schema::create() 里写 index('meta->name'),Doctrine DBAL 解析不了这种表达式,直接抛 Argument 1 passed to Doctrine\DBAL\Schema\Index::_addColumn() must be of the type string, null given。
正确做法分两步:
- 先建虚拟列:
$table->string('meta_name')->virtualAs("JSON_UNQUOTE(JSON_EXTRACT(meta, '$.name'))")->index();(MySQL 5.7+) - 再用原生 SQL 建函数索引:
DB::statement("CREATE INDEX idx_users_meta_name ON users((JSON_EXTRACT(meta, '$.name')));");(MySQL 8.0+) - 两者都必须放在独立迁移中,且确保该迁移在表创建之后执行
虚拟列和函数索引都不能靠 Laravel Schema Builder 自动回滚,down() 方法里得手写 DB::statement('DROP INDEX ...') 和 DB::statement('ALTER TABLE ... DROP COLUMN ...')。


















