必须使用 whereJsonContains 查询 JSON 字段,因其底层调用数据库原生 JSON 查询函数(如 MySQL 的 JSON_CONTAINS、PostgreSQL 的 @>),确保结构化匹配;而 where 方法仅作字符串模糊匹配,不安全且不可靠。

JSON字段查询必须用 whereJsonContains,不能用 where 直接比较
MySQL 5.7+ 和 PostgreSQL 原生支持 JSON 字段,但 Laravel 的 where 方法对 JSON 列执行的是字符串级模糊匹配,不是结构化查询。比如字段 meta 存了 {"tags": ["php", "laravel"]},写 where('meta', 'like', '%laravel%') 可能误中其他字段或被转义干扰。
whereJsonContains 才是语义正确的选择,它底层调用数据库的 JSON_CONTAINS(MySQL)或 @>(PostgreSQL),确保只匹配到 JSON 数组中的值或对象中的键值对。
- 数组场景:查含某个 tag →
whereJsonContains('meta->tags', 'laravel') - 对象场景:查 status 为 active →
whereJsonContains('meta', ['status' => 'active']) - 注意:第二个参数必须是 PHP 数组或标量;传 JSON 字符串会失败
-> 箭头语法只适用于 SELECT 和 UPDATE,不能用于 WHERE 条件中的路径计算
Laravel 的 -> 是 Eloquent 对 JSON 字段的“虚拟列”映射语法,仅在读取(select)、更新(update)和部分写入(json_set)时生效。它不参与 SQL 的 WHERE 解析逻辑 —— 换句话说,where('meta->tags', 'laravel') 实际发出去的是字符串比较,不是 JSON 查询。
真正安全的写法是显式使用 whereJsonContains 或 whereJsonLength 等专用方法。如果真要按路径做等值判断(如确定某个 key 的值完全等于某字符串),可用 whereRaw 配合数据库函数:
->whereRaw("JSON_UNQUOTE(JSON_EXTRACT(meta, '$.tags[0]')) = ?", ['php'])
但这种写法失去可移植性,且无法利用 JSON 索引(除非你手动建了生成列 + 索引)。
更新 JSON 字段要用 json_set 或模型属性赋值,避免全量覆盖
直接给 JSON 字段赋整个新数组(如 $model->meta = ['tags' => ['js']])会导致其他未提及的 key(比如 author、published_at)被清空。正确做法分两种:
- 局部更新(推荐):用数据库原生函数
json_set,例如
DB::table('posts')
->where('id', 1)
->update([
'meta' => DB::raw("JSON_SET(meta, '$.tags', JSON_MERGE_PATCH(JSON_EXTRACT(meta, '$.tags'), '["vue"]'))")
])
- 模型方式:先取再改再存,适合逻辑简单、并发低的场景
$post = Post::find(1); $tags = $post->meta->tags ?? []; $tags[] = 'vue'; $post->meta->tags = $tags; $post->save();
注意:Eloquent 会把 $post->meta 当作 stdClass 或数组(取决于 casts 配置),但修改后保存仍是全量写入 —— 所以仅当确认没有其他并行写入时才用这种方式。
性能关键:JSON 字段必须配合生成列 + 索引,否则 whereJsonContains 很慢
MySQL 的 JSON_CONTAINS 默认无法走索引,即使字段加了普通 B-tree 索引也没用。必须额外创建一个生成列(generated column),再对其建索引:
ALTER TABLE posts ADD COLUMN tags_json JSON AS (meta->'$.tags'); ALTER TABLE posts ADD INDEX idx_tags_json (tags_json);
之后 whereJsonContains('meta->tags', 'laravel') 才可能命中索引。PostgreSQL 同理,需用 jsonb_path_ops 索引:
CREATE INDEX CONCURRENTLY idx_posts_meta_tags ON posts USING GIN ((meta->'tags'));
没建对应索引时,千万条数据上 whereJsonContains 可能秒变全表扫描。这个步骤常被跳过,却是线上慢查的根源之一。









