sync() 先清空再插入,非增量更新;返回被删ID数组;带额外字段需传键值对;syncWithoutDetaching不检查重复;主模型须已保存。

sync() 会清空再插入,不是增量更新
很多人以为 sync() 是“把传入的 ID 加进去”,实际它会先删掉所有现有关联,再插入你给的 ID 列表。比如用户已有角色 [1,2],调用 $user->roles()->sync([2,3]),结果是角色变成 [2,3],而不是 [1,2,3] —— ID 1 被删了。
- 只适合“当前列表即全部合法值”的场景,比如权限组、标签集合
- 如果要保留原有数据并追加,得用
attach();要删特定项,用detach() -
sync()返回的是被删除的 ID 数组(Laravel 9+),不是新插入的,别靠返回值判断“新增了哪些”
带额外字段的多对多同步必须用 sync() + 额外数据数组
中间表有额外字段(比如 role_user.pivot.activated_at 或 priority)时,sync() 依然能用,但必须传关联 ID 和字段组成的键值对,不能只传 ID 列表。
- 正确写法:
$user->roles()->sync([1 => ['activated_at' => now()], 3 => ['activated_at' => now()]]) - 错误写法:
$user->roles()->sync([1,3])—— 额外字段全丢,且 Laravel 不报错,只是不存 - 没传的字段会设为 NULL,除非你在迁移里设了默认值或数据库级 DEFAULT
- 如果只想更新部分记录的额外字段,其他保持原样,
sync()不适用——它强制覆盖整行,得手动查出旧记录再逐条updateExistingPivot()
syncWithoutDetaching 不是“只增不删”,而是“跳过删除阶段”
syncWithoutDetaching() 常被误读成“智能去重后插入”,其实它只是跳过“删旧”的步骤,然后对每个传入 ID 执行 attach() —— 意味着重复 ID 会触发唯一约束错误(如果中间表有唯一索引)。
- 它不会检查该 ID 是否已存在,直接尝试插入,所以中间表必须允许重复(比如没设
UNIQUE(user_id, role_id)),否则抛出SQLSTATE[23000]: Integrity constraint violation - 真正安全的“只增不删”逻辑需要自己实现:
collect($newIds)->diff($existingIds)->each(...) - 这个方法极少用到,多数时候不如明确用
attach()+ 前置去重来得可控
使用 sync() 前务必确认模型已保存
sync() 要求主模型(比如 User)已经写入数据库,否则中间表外键会指向 0 或 NULL,导致静默失败或报错 Integrity constraint violation: 1452。
- 新建模型后立刻调用:
$user = new User; $user->sync($roles);—— 必炸 - 正确顺序:
$user = User::create([...]); $user->roles()->sync($roles); - 如果用
make()或构造函数实例化但未save(),同样不行;Eloquent 不会自动帮你兜底 - 测试时容易漏掉这点,因为内存模型 ID 看起来像有值,实际数据库里还没生成
sync() 自己能处理的。得自己补逻辑,别指望它变聪明。


















