复合主键模型必须显式标注所有主键字段为gorm:"primarykey",否则save/update会误判无主键导致重复插入;事务中应先first加载完整主键实例再操作,避免where缺失;updates结构体时零值字段(含主键)被跳过,须改用map或select指定字段。

复合主键模型必须显式定义主键标签
GORM 默认只认 ID 字段为主键,遇到复合主键(如 user_id + role_id)时,若不显式声明,Save 和 Update 会误判“无主键”,转而执行插入,导致重复记录或主键冲突错误。
正确做法是在结构体字段上用 gorm:"primaryKey" 标注所有主键字段:
type UserRole struct {
UserID uint `gorm:"primaryKey"`
RoleID uint `gorm:"primaryKey"`
Status string
}
注意:不能只标一个;也不能用 gorm:"compositePrimaryKey"(该标签不存在,是常见误解)。
事务中 Update/Updates 对复合主键的 WHERE 条件生成逻辑
在事务内调用 db.Transaction(func(tx *gorm.DB) error { ... }) 时,GORM 的 Update 或 Updates 方法仍依赖模型实例是否携带完整主键值来构造 WHERE 子句。
常见错误现象:tx.Model(&UserRole{}).Where("user_id = ? AND role_id = ?", 1, 2).Update("status", "active") 看似安全,但若漏写任一条件,或误用空结构体,WHERE 就可能缺失关键字段,造成批量误更新。
更稳妥的做法是先查再改:
- 用
tx.First(&ur, UserRole{UserID: 1, RoleID: 2})加载带完整主键的实例 - 再调用
tx.Save(&ur)或tx.Model(&ur).Updates(...)—— 此时 GORM 能自动拼出WHERE user_id = 1 AND role_id = 2 - 避免直接对空结构体或 map 做
Model(&UserRole{})操作
Updates(struct) 在复合主键场景下零值字段被跳过的问题
这是最易踩的坑:Updates 接收结构体时,**不会更新任何零值字段**,哪怕该字段是复合主键的一部分。
例如:
ur := UserRole{UserID: 1, RoleID: 0, Status: "inactive"} // RoleID 是 0(int 零值)
tx.Updates(ur) // 生成 SQL: UPDATE user_roles SET status='inactive' WHERE user_id = 1
结果:WHERE 条件丢失 role_id,可能匹配多行,甚至更新错记录。
解决方式只有两个:
- 改用
map[string]interface{}:tx.Updates(map[string]interface{}{"role_id": 0, "status": "inactive"}) - 或强制指定字段:
tx.Select("role_id", "status").Updates(UserRole{UserID: 1, RoleID: 0, Status: "inactive"})
事务中 Save 对复合主键的“存在性判断”不可靠
Save 在事务里仍按“是否有主键值”决定 INSERT 还是 UPDATE。但对复合主键,只要任一主键字段为零值(如 RoleID == 0),GORM 就认为“主键不完整”,倾向执行 INSERT,触发唯一键冲突(ERROR: duplicate key value violates unique constraint)。
这不是 bug,是设计使然 —— GORM 不做“部分主键存在性推断”。所以:
- 绝不要传入主键字段未赋值(或为零值)的结构体给
Save - 如果业务允许,优先用
FirstOrCreate或FirstOrInit替代Save - 在事务中尤其要检查日志输出的 SQL,确认 WHERE 是否包含全部主键列
复合主键本身不复杂,复杂的是 GORM 把“字段零值”和“字段未设置”混为一谈 —— 这个边界,在事务这种强一致性场景里,一点都不能含糊。











