Gorm を使用して最近フィールドを更新しているときに、問題が発生しました。ステータス フィールドを更新しようとすると、フィールドの値が変更されていないにもかかわらず、Gorm から「キー 'PRIMARY' のエントリ 'xxxx' が重複しています」というプロンプトが表示されます。
Save
まず、 Gorm の公式ドキュメントのメソッドの説明を見てみましょう。
Save
このメソッドは、フィールドがゼロであっても、すべてのフィールドを保存します。
db.First(&user)
user.Name = "jinzhu 2"
user.Age = 100
db.Save(&user)
// UPDATE users SET name='jinzhu 2', age=100, birthday='2016-01-01', updated_at = '2013-11-17 21:34:10' WHERE id=111;
Save
メソッドは複合関数です。保存されたデータに主キーが含まれていない場合に実行されますCreate
。逆に、保存されたデータに主キーが含まれている場合は、そのキーが (すべてのフィールドで) 実行されますUpdate
。
db.Save(&User{
Name: "jinzhu", Age: 100})
// INSERT INTO `users` (`name`,`age`,`birthday`,`update_at`) VALUES ("jinzhu",100,"0000-00-00 00:00:00","0000-00-00 00:00:00")
db.Save(&User{
ID: 1, Name: "jinzhu", Age: 100})
// UPDATE `users` SET `name`="jinzhu",`age`=100,`birthday`="0000-00-00 00:00:00",`update_at`="0000-00-00 00:00:00" WHERE `id` = 1
この説明によれば、ID フィールドを指定したため、期待される動作は更新操作であるはずです。ただし、実際に行われるのは挿入操作です。これは私を混乱させます。
この問題を理解するために、Gorm のソース コードを詳しく読みました。
// Save updates value in database. If value doesn't contain a matching primary key, value is inserted.func (db *DB) Save(value interface{}) (tx *DB) {
tx = db.getInstance()
tx.Statement.Dest = value
reflectValue := reflect.Indirect(reflect.ValueOf(value))
for reflectValue.Kind() == reflect.Ptr || reflectValue.Kind() == reflect.Interface {
reflectValue = reflect.Indirect(reflectValue)
}
switch reflectValue.Kind() {
case reflect.Slice, reflect.Array:
if _, ok := tx.Statement.Clauses["ON CONFLICT"]; !ok {
tx = tx.Clauses(clause.OnConflict{
UpdateAll: true})
}
tx = tx.callbacks.Create().Execute(tx.Set("gorm:update_track_time", true))
case reflect.Struct:
if err := tx.Statement.Parse(value); err == nil && tx.Statement.Schema != nil {
for _, pf := range tx.Statement.Schema.PrimaryFields {
if _, isZero := pf.ValueOf(tx.Statement.Context, reflectValue); isZero {
return tx.callbacks.Create().Execute(tx)
}
}
}
fallthrough
default:
selectedUpdate := len(tx.Statement.Selects) != 0
// when updating, use all fields including those zero-value fields
if !selectedUpdate {
tx.Statement.Selects = append(tx.Statement.Selects, "*")
}
updateTx := tx.callbacks.Update().Execute(tx.Session(&Session{
Initialized: true}))
if updateTx.Error == nil && updateTx.RowsAffected == 0 && !updateTx.DryRun && !selectedUpdate {
return tx.Create(value)
}
return updateTx
}
return
}
ソース コードの主なロジックは次のとおりです。
- データベース インスタンスを取得し、SQL ステートメントを実行する準備をします。
value
操作対象のデータです。 - リフレクション メカニズムを使用して決定されるタイプ
value
。 value
スライスまたは配列で、競合解決戦略 (「ON CONFLICT」) が定義されていない場合は、競合するすべてのフィールドを更新する競合解決戦略を設定し、挿入操作を実行します。value
Struct の場合は、構造を解析して主キー フィールドを走査しようとします。主キー フィールドの値がゼロの場合、挿入が実行されます。- Slice、Array、Struct 以外の型の場合は、更新操作が試行されます。更新操作後に影響を受ける行がなく、更新対象として特定のフィールドが選択されなかった場合は、挿入操作が実行されます。
value
この関数から、渡された対応するデータベース レコードが存在しない場合 (主キーに基づいて判断)、Gorm は新しいレコードの作成を試みることがわかります。Gorm は、更新操作が行に影響を与えない場合にも、新しいレコードの作成を試みます。
この動作は、私たちが通常理解している「更新/挿入」(更新 + 挿入) ロジックとは少し異なります。この場合、Gorm は、更新されたデータがデータベース内のデータとまったく同じであっても、挿入を試行します。Duplicate entry 'xxxx' for key 'PRIMARY'
これが主キー違反のエラー メッセージであるため、エラーが表示されるのです。
私は Gorm のこの動作に当惑しており、公式ドキュメントの説明にもこの部分の情報が記載されていないため失望しています。
この問題を解決するにはどうすればよいでしょうか?
GORM の Create メソッドと競合解決戦略を使用して、Save メソッドを自分で実装できます。
// Update all columns to new value on conflict except primary keys and those columns having default values from sql func
db.Clauses(clause.OnConflict{
UpdateAll: true,
}).Create(&users)
// INSERT INTO "users" *** ON CONFLICT ("id") DO UPDATE SET "name"="excluded"."name", "age"="excluded"."age", ...;
// INSERT INTO `users` *** ON DUPLICATE KEY UPDATE `name`=VALUES(name),`age`=VALUES(age), ...; MySQL
Gorm の Create メソッドのドキュメントで、この使用法を見ることができます。ID を指定すると、他のすべてのフィールドが更新されます。ID が指定されていない場合は、新しいレコードが挿入されます。