1. 从一次线上事故说起:为什么选错方法会出大问题
几年前,我在一个用户管理系统中踩过一个坑,至今记忆犹新。当时有个需求是同步外部系统的用户数据到我们自己的数据库,逻辑很简单:如果用户ID存在就更新信息,不存在就创建新记录。我心想,这不就是典型的“保存”操作吗?于是很自然地用了 Save() 方法。
代码大概长这样:
func SyncUser(user User) error {
return db.Save(&user).Error
}
看起来没问题对吧?但上线后没多久,监控就报警了——数据库出现了大量主键冲突的错误。我查了半天才发现,问题出在有些用户的ID虽然是数字,但在外部系统里是字符串类型,同步过来后GORM认为这些ID是零值(因为Go里int的零值是0),于是每次都执行插入操作,自然就冲突了。
这就是我今天想跟你聊的核心问题:在GORM里,.Save()和.Create()看起来都能“保存”数据,但它们的底层逻辑完全不同,用错了轻则报错,重则数据混乱。特别是当你的表主键策略比较复杂时,更需要理解这两个方法的脾气。
简单来说,.Create()是个直肠子,它只干一件事:插入新记录。如果你给的数据主键不为零,它会认为你想插入一个已存在的记录,直接报错。而.Save()是个聪明人,它会先看看主键是否存在,存在就更新,不存在就插入。但正是这个“聪明”,有时候会带来意想不到的麻烦。
2. 深入骨髓:.Create() 的纯粹插入哲学
2.1 当主键为零值时:标准的插入操作
让我们先彻底搞懂 .Create()。这个方法的设计哲学很纯粹:我就是来创建新记录的,别让我干别的。
最典型的场景就是自增主键。假设我们有个用户表:
type User struct {
ID uint `gorm:"primaryKey;autoIncrement"`
Name string
Age int
}
// 场景1:标准插入
user := User{Name: "张三", Age: 25}
result := db.Create(&user)
// 生成的SQL:INSERT INTO users (name, age) VALUES ('张三', 25)
// user.ID 会被自动填充为数据库生成的自增ID
这里的关键点在于,ID字段是零值(0),GORM看到后就知道:“哦,这是个新用户,我要生成新ID”。插入成功后,user.ID会被自动赋值为数据库返回的自增ID。
我建议你在实际开发中养成一个习惯:每次.Create()之后,都检查一下生成的主键值。这不仅能确认插入成功,还能在需要关联操作时拿到正确的ID。
if err := db.Create(&user).Error; err != nil {
log.Printf("插入失败: %v", err)
return err
}
log.Printf("新用户创建成功,ID: %d", user.ID)
2.2 当主键非零时:为什么.Create()会报错
现在来看一个容易出错的场景。有时候我们从外部系统拿到数据,里面已经包含了ID:
// 场景2:带ID的插入尝试
user := User{ID: 100, Name: "李四", Age: 30}
err := db.Create(&user).Error
// 这里会报错!错误信息类似:
// Error 1062: Duplicate entry '100' for key 'PRIMARY'
为什么?因为.Create()看到ID字段不是零值(0),它就认为你想插入一个ID为100的记录。如果数据库里已经存在ID=100的用户,就会触发主键冲突;即使不存在,GORM也会严格按照你给的ID去插入,而不是使用自增。
这里有个重要细节:GORM判断“主键是否存在”的标准是“是否为零值”。对于数值类型是0,对于字符串是"",对于指针是nil。所以如果你用了*uint作为ID类型,那么nil时GORM会认为是新记录。
2.3 批量插入的陷阱与技巧
批量插入是.Create()的强项,但也有一些坑需要注意:
// 正确的批量插入
users := []User{
{Name: "王五", Age: 28},
{Name: "赵六", Age: 32},
{Name: "孙七", Age: 45},
}
// 方法1:直接Create切片
result := db.Create(&users)
fmt.Printf("插入了%d条记录", result.RowsAffected)
// 方法2:分批插入(大数据量时推荐)
batchSize := 100
result = db.CreateInBatches(&users, batchSize)
但这里有个性能陷阱:如果你一次性插入几万条记录,可能会遇到数据库连接超时或者内存问题。我建议超过1000条就考虑分批,并且最好在事务中操作:
err := db.Transaction(func(tx *gorm.DB) error {
// 在事务中批量插入
if err := tx.CreateInBatches(&users, 1000).Error; err != nil {
return err // 自动回滚
}
return nil // 自动提交
})
另外,批量插入时钩子函数(Hooks) 的行为也需要注意。BeforeCreate和AfterCreate会对每个记录分别调用,如果钩子里有复杂逻辑,批量插入的性能优势可能会被抵消。
3. 双重人格:.Save() 的智能与危险
3.1 Save() 的核心逻辑:先更新,后插入的保障机制
如果说.Create()是单纯的,那么.Save()就是复杂的。它的行为可以概括为:尽一切可能让数据保存成功。
官方文

181

被折叠的 条评论
为什么被折叠?



