描述
作为一个 gorm 的重度使用者,在习惯了 gorm 带来的便利,也遇到了很多的问题,日前看到一些小伙伴仍然会因为对 gorm 特性的不了解,导致出现 panic 或者数据的错误更新的问题,所以这边提供一些小技巧和避坑指南,持续更新…
插入
gorm 解决并发插入报错
当你有并发插入的情况,就难免会出现数据冲突的问题,如果不做处理的话,会出现报错 Duplicate key,gorm 提供了中间件来解决这个问题。
如果你期望 upsert:
doc := &model.Table{Title: "table"}
err := db.Table("table").Clauses(clause.OnConflict{UpdateAll: true}).Create(&doc).Error
你也可以根据实际使用场景选择其他不同的冲突处理方案:
type OnConflict struct {
	Columns      []Column
	Where        Where
	TargetWhere  Where
	OnConstraint string
	DoNothing    bool
	DoUpdates    Set
	UpdateAll    bool
}
Clauses on conflict do nothing 带来的问题
如果你和我一样习惯用结构体指针插入,那么如果用 Clauses OnConflict do nothing 解决冲突的同时可能会带来另外一个问题,那就是数据不冲突时,insert id 会回写,但是冲突之后不会进行回写。
部分源码如下:
if !db.DryRun && db.Error == nil {
	result, err := db.Statement.ConnPool.ExecContext(db.Statement.Context, db.Statement.SQL.String(), db.Statement.Vars...)
	if err == nil {
		db.RowsAffected, _ = result.RowsAffected()
		if db.RowsAffected > 0 {
			if db.Statement.Schema != nil && db.Statement.Schema.PrioritizedPrimaryField != nil && db.Statement.Schema.PrioritizedPrimaryField.HasDefaultValue {
				if insertID, err := result.LastInsertId(); err == nil && insertID > 0 {
					switch db.Statement.ReflectValue.Kind() {
					case reflect.Slice, reflect.Array:
						if config.LastInsertIDReversed {
							for i := db.Statement.ReflectValue.Len() - 1; i >= 0; i-- {
								rv := db.Statement.ReflectValue.Index(i)
								if reflect.Indirect(rv).Kind() != reflect.Struct {
									break
								}
								_, isZero := db.Statement.Schema.PrioritizedPrimaryField.ValueOf(rv)
								if isZero {
									db.Statement.Schema.PrioritizedPrimaryField.Set(rv, insertID)
									insertID -= db.Statement.Schema.PrioritizedPrimaryField.AutoIncrementIncrement
								}
							}
						} else {
							for i := 0; i < db.Statement.ReflectValue.Len(); i++ {
								rv := db.Statement.ReflectValue.Index(i)
								if reflect.Indirect(rv).Kind() != reflect.Struct {
									break
								}
								if _, isZero := db.Statement.Schema.PrioritizedPrimaryField.ValueOf(rv); isZero {
									db.Statement.Schema.PrioritizedPrimaryField.Set(rv, insertID)
									insertID += db.Statement.Schema.PrioritizedPrimaryField.AutoIncrementIncrement
								}
							}
						}
					case reflect.Struct:
						if _, isZero := db.Statement.Schema.PrioritizedPrimaryField.ValueOf(db.Statement.ReflectValue); isZero {
							db.Statement.Schema.PrioritizedPrimaryField.Set(db.Statement.ReflectValue, insertID)
						}
					}
				} else {
					db.AddError(err)
				}
			}
		}
	} else {
		db.AddError(err)
	}
}
大概意思就是,如果影响行数 > 1 才会进行 insert id 赋值,也就是说,如果此时直接使用 doc 中的 id,那么这个 id 的值会是 0。
批量 upsert id 错误
doc := []*model.Table{{Title: "table",Content:"xx"},{Title:"table1",Content:"yy"}}
err := db.Table("table").Clauses(clause.OnConflict{UpdateAll: true}).Create(&doc).Error
Title 为唯一主键,如果数据库里已经存在了 title 为 table 的数据,那么 content 可以成功更新,响应行数也 >1 ,但是 id 的回写会从 lastInsertID 向下自增,也就是说,两个结构体回写的 id 都是错误的,这种情况直接使用,风险很大,可以考虑判断影响行数是否符合预期,不符合预期重新查询来解决这个问题。
