sync()仅适用于多对多关系的中间表批量更新,会清空旧记录再写入新数据;在hasOne、belongsTo或hasMany关系上调用将报错;支持带额外字段的写入但不触发模型事件,Laravel 9+起可监听pivot事件。

Sync 方法不是万能的数据同步工具,它只适用于多对多关系的中间表数据批量更新,且会清空原有记录再写入新数据。
sync() 只作用于多对多关系(pivot 表)
如果你在 hasOne、belongsTo 或 hasMany 关系上调用 sync(),会直接报错:Call to undefined method Illuminate\\Database\\Eloquent\\Relations\\HasMany::sync()。Laravel 的 sync() 是 BelongsToMany 关系专属方法,底层依赖中间表(pivot table)的存在。
常见使用场景:
- 用户分配多个角色(
users↔roles,中间表role_user) - 文章打多个标签(
posts↔tags,中间表post_tag) - 商品关联多个分类(需确保是多对多建模,而非单向外键)
sync() 会删除不在数组中的旧关联
调用 $user->roles()->sync([1, 3, 5]) 的实际行为是:先删掉该用户当前所有角色记录,再插入 ID 为 1、3、5 的三条新记录。它不比对差异,也不保留历史 —— 这是和 attach() / detach() 最本质的区别。
如果只想新增不删旧,改用:
-
$user->roles()->attach([2, 4])(跳过已存在项) -
$user->roles()->syncWithoutDetaching([1, 3, 5, 2, 4])(Laravel 5.8+,保留已有,仅追加缺失)
注意:syncWithoutDetaching 仍可能触发重复插入异常(如中间表有唯一索引),需确保数据幂等或提前去重。
sync() 支持带额外字段的批量写入
当 pivot 表含额外字段(如 created_by、priority、expires_at),可传入关联数组:
$user->roles()->sync([
1 => ['assigned_at' => now(), 'notes' => 'admin setup'],
3 => ['assigned_at' => now()->subDay(), 'notes' => 'temp access'],
]);
未显式指定的字段将设为 null(除非数据库默认值生效)。若想让某些字段保持原值(如更新时间戳),sync() 本身不支持 —— 此时应放弃 sync(),改用 upsert() 或手动分步处理(detach + attach + updateExistingPivot)。
sync() 不触发模型事件,但会触发 pivot 事件(Laravel 9+)
默认情况下,sync() 不会触发 creating、updating 等模型事件,因为操作对象是 pivot 记录,而非主模型。但从 Laravel 9 开始,可通过监听 PivotAttached、PivotDetached、PivotToggled 等事件捕获中间表变更。
容易忽略的点:
- 事务安全:若 sync 失败(如外键约束失败),整个操作回滚,但不会自动包装在事务里 —— 你得自己用
DB::transaction() - 性能影响:大批量 sync(如上万 ID)会导致中间表频繁 DELETE + INSERT,建议拆分批次或改用原生 SQL
- 软删除关联:如果 pivot 表用了软删除(如
deleted_at),sync()默认不恢复已软删记录 —— 需配合withTrashed()或自定义逻辑
真正需要“同步”业务逻辑时,别迷信 sync() —— 先确认是不是多对多,再判断要不要清旧,最后看字段和事件是否满足需求。一步写错,中间表就丢了历史。










