Laravel 9.2+ 稳定支持 upsert(),依赖数据库原生语法;Laravel 8 及以下版本不支持,调用会抛出 BadMethodCallException。

PHP 的 Eloquent 不支持原生 upsert(插入或更新)原子操作,直到 Laravel 9 才通过 upsert() 方法提供有限支持;Laravel 8 及更早版本必须手动实现,且需注意数据库驱动、唯一索引和字段映射限制。
哪些 Laravel 版本能用 upsert()?
Laravel 9.2+ 开始稳定支持 upsert(),底层依赖数据库的原生语法(如 MySQL 的 INSERT ... ON DUPLICATE KEY UPDATE,PostgreSQL 的 ON CONFLICT DO UPDATE)。Laravel 8 及以下版本调用该方法会抛出 BadMethodCallException。
- MySQL 用户:必须在目标字段上建
UNIQUE或PRIMARY KEY索引,否则ON DUPLICATE KEY UPDATE不生效 - SQLite 用户:Laravel 9.2+ 支持,但仅限于
REPLACE INTO模拟,不保证事务原子性 - PostgreSQL 用户:需确保冲突字段明确指定
ON CONFLICT (col1, col2),Laravel 默认只推断主键
upsert() 的参数怎么填才不丢数据?
三个参数缺一不可:upsert($values, $uniqueBy, $updateColumns)。最容易错的是 $uniqueBy —— 它不是“哪个字段用来判断重复”,而是“哪些字段组合构成唯一约束”,必须与数据库实际索引字段完全一致。
-
$values:二维数组,每项是待插入/更新的字段名 => 值映射,不能含自增主键(如id),否则 MySQL 报错 -
$uniqueBy:字符串数组,例如['email']或['tenant_id', 'slug'],必须对应数据库中已存在的唯一索引列 -
$updateColumns:字符串数组,指定哪些字段允许被更新,不在此列表中的字段即使传了也不会更新,比如['name', 'updated_at']
示例:
立即学习“PHP免费学习笔记(深入)”;
DB::table('users')->upsert(
[
['email' => 'a@b.com', 'name' => 'Alice', 'score' => 100],
['email' => 'c@d.com', 'name' => 'Bob', 'score' => 85],
],
['email'], // ← 必须是数据库里 email 字段有 UNIQUE 约束
['name', 'score', 'updated_at'] // ← 只有这三项会被 UPDATE
);
Laravel 8 及更早版本怎么安全模拟 upsert?
不能靠 firstOrCreate() + save() 组合,那是两次查询,非原子操作,高并发下必然产生重复记录或丢失更新。
- 推荐用原生 DB 查询:显式写
INSERT ... ON DUPLICATE KEY UPDATE,并用DB::statement()或DB::insert()执行 - 若必须走 Eloquent 模型,可重写
upsert()方法(Laravel 8.76+ 提供了Model::upsert()静态方法,但底层仍是 DB 原生调用) - 切忌在事务里用
find()+updateOrCreate():事务无法防止其他连接同时插入相同唯一键
简单兜底方案(MySQL):
DB::insert(
"INSERT INTO users (email, name, score) VALUES (?, ?, ?)
ON DUPLICATE KEY UPDATE name = VALUES(name), score = VALUES(score), updated_at = NOW()",
['a@b.com', 'Alice', 100]
);
为什么 upsert() 更新时间戳经常失效?
updated_at 不会自动更新,除非你显式把它写进 $updateColumns,且数据库字段默认值不能设为 CURRENT_TIMESTAMP(MySQL 5.6+ 要求 ON UPDATE CURRENT_TIMESTAMP 才行,但 Laravel upsert() 不触发该机制)。
- 正确做法:把
'updated_at'加入$updateColumns,并在$values中传入now()或Carbon::now() - 错误做法:依赖模型的
$timestamps = true或protected $dates = ['updated_at'],这些对upsert()无效 - PostgreSQL 注意:
NOW()函数需用DB::raw('NOW()')包裹,否则被当字符串插入
真正难的不是写对语法,而是确认数据库索引是否覆盖了你要 upsert 的业务维度——漏掉一个字段,整个操作就退化成单纯插入,还报唯一键冲突。别信文档里“自动推断”的说法,自己 SHOW INDEX FROM table_name 看一眼最稳。



















