必须先保存主模型再调用attach/sync,否则外键写入0或报integrity constraint violation;sync()强制清空再插入,删除原有数据;attach()只增不删,syncwithoutdetaching()跳过删除但不查重;中间表字段需withpivot()声明并用pivot访问;自定义pivot模型仅在需业务逻辑时使用,且必须有id主键。

必须先保存主模型,再调用 attach/sync,否则外键写入 0 或报 Integrity constraint violation。
为什么 sync() 会删掉原有数据?
sync() 的设计语义就是“当前传入的 ID 列表 = 全部合法关系”,它不区分新增/保留/删除,而是强制先清空中间表中所有该主模型的记录,再批量插入新列表。
- 比如用户已有角色
[1, 2],执行$user->roles()->sync([2, 3])后,结果是[2, 3],1被静默移除 - 返回值是被删除的 ID 数组(Laravel 9+),不是新增的,别误当成功列表用
- 若中间表含额外字段(如
activated_at),必须传键值对:[1 => ['activated_at' => now()]],只传[1]会导致字段全为NULL
attach() 和 syncWithoutDetaching() 有什么区别?
attach() 是纯增量插入:只加不删,重复 ID 会触发数据库唯一约束错误(如果中间表有 UNIQUE(user_id, role_id));syncWithoutDetaching() 表面看像“只增”,实际是跳过删除阶段后对每个 ID 执行 attach() —— 它不做去重判断,也不查是否已存在。
- 真正安全的“追加且去重”得自己写:
collect($newIds)->diff($existingIds)->each(...) -
syncWithoutDetaching()极少用,除非你明确允许中间表重复(比如没设唯一索引),否则容易爆SQLSTATE[23000]: Integrity constraint violation -
attach()支持单个 ID、ID 数组,也支持带字段的键值对:$user->roles()->attach(1, ['assigned_at' => now()])
中间表字段怎么读和写?
默认 belongsToMany 不暴露中间表字段。要访问或写入额外列(如 level、notes),必须显式声明 withPivot(),并在操作时传对应字段。
- 在模型中定义:
return $this->belongsToMany(Role::class)->withPivot('level', 'notes')->withTimestamps(); - 读取:
$role->pivot->level,注意是$role->pivot,不是$user->pivot - 写入:
$user->roles()->attach($roleId, ['level' => 'admin', 'notes' => 'trial']) - 更新已有记录:
$user->roles()->updateExistingPivot($roleId, ['level' => 'full']),不能用sync()替代,因为sync()会重建整行
自定义 Pivot 模型什么时候必须用?
只有当你需要为中间表添加业务逻辑(如访问器、修改器、事件监听、软删除、关联其他模型)时,才值得升格为完整模型。普通字段读写用 withPivot() 就够了。
- 创建 Pivot 模型:
php artisan make:model RoleUser --pivot(手动补继承Pivot) - 在关联中指定:
->using(RoleUser::class),同时仍需保留withPivot()声明字段 - 关键限制:自定义 Pivot 模型的表必须有
id主键,否则sync()等方法内部会出错 - 常见坑:Pivot 模型里不能定义
$fillable,字段控制要放在主模型的withPivot()或updateExistingPivot()参数里
最容易被忽略的是:所有 pivot 操作都依赖主模型已持久化。新建一个 User 实例但没 save() 或 create(),直接调 roles()->attach(),外键会写 0 或 NULL,数据库报错却不提示具体哪步失败。











