GORMのsaveメソッドはupsertをやってくれて便利だが、update時にCreatedAtは更新されるのか?あれupdateなのになんかinsert動いてない?みたいなことがあって調べた
知ってないとハマるポイント1
SaveメソッドはUpdateして0件だとINSERTするよ
ちょっと意外だったのが、Updateを実行した際に結果が0件だと、INSERTが実行されます。
これは便利ともいえるけど、何らかのバグでプライマリフィールドに意図しないIDが渡るとエラーにならずに、レコードがINSERTされてしまいます。
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))
// 構造体の場合
// プライマリーキーにあたるものが0ならCreateを実行
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 {
// Create実行
return tx.callbacks.Create().Execute(tx)
}
}
}
fallthrough
default:
// プライマリーキーにあたるものが0でないならUpdateを実行
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, "*")
}
// Update実行
updateTx := tx.callbacks.Update().Execute(tx.Session(&Session{Initialized: true}))
// Updateの結果がエラーもしくは0件だったらCreateを実行する
if updateTx.Error == nil && updateTx.RowsAffected == 0 && !updateTx.DryRun && !selectedUpdate {
return tx.Session(&Session{SkipHooks: true}).Clauses(clause.OnConflict{UpdateAll: true}).Create(value)
}
return updateTx
}
return
}
知ってないとハマるポイント2
Updateの際、CreatedAtは値指定がなければゼロ値で更新、UpdatedAtは自動生成されるよ
Saveメソッドを利用してUpdateを実施する際、気を利かせてCreatedAtは更新対象外になり、UpdatedAtだけ更新されるのかな?みたいな期待を持っていましたが違いました。
CreatedAtは他のカラム同様更新対象のカラムとして扱われ、値がなければゼロ値がセットされます。UpdatedAtだけ特別扱いされる形です。
例
type Sample struct {
ID int `gorm:"primarykey" name:"ID"`
CreatedAt time.Time
UpdatedAt time.Time
}
func (e Sample) SaveSample(tx *gorm.DB) {
// デバッグモードでSQLを出力するようにする
tx = tx.Debug()
tx.Create(&sample{ID: 1})
// 生成されるSQL
// INSERT INTO `samples` (`created_at`,`updated_at`,`id`) VALUES ('2024-07-26 05:30:45.075','2024-07-26 05:30:45.075',1)
tx.Save(&sample{ID: 1})
// 生成されるSQL
// UPDATE `samples` SET `created_at`='0000-00-00 00:00:00',`updated_at`='2024-07-26 05:30:47.888' WHERE `id` = 1
// ポイント:UPDATEの際はcreated_atがゼロ値でセット。updated_atは自動生成。
そもそもcreatedAtとupdatedAtをgorm側でどう判定しているのか
構造体でgormのタグを指定してないとうまく動作しないのでは?と思っていたが、以下の通りタグを指定しているもしくは、フィールド名が CreatedAt
になっていて、それがTime型
かInt型
かUint型
だったら自動でCreatedAtの時刻を生成するよというのがわかった。
// 作成日時フィールドに対する処理
if v, ok := field.TagSettings["AUTOCREATETIME"]; (ok && utils.CheckTruth(v)) || (!ok && field.Name == "CreatedAt" && (field.DataType == Time || field.DataType == Int || field.DataType == Uint)) {
if field.DataType == Time {
field.AutoCreateTime = UnixTime
} else if strings.ToUpper(v) == "NANO" {
field.AutoCreateTime = UnixNanosecond
} else if strings.ToUpper(v) == "MILLI" {
field.AutoCreateTime = UnixMillisecond
} else {
field.AutoCreateTime = UnixSecond
}
}
// 更新日時フィールドに対する処理
if v, ok := field.TagSettings["AUTOUPDATETIME"]; (ok && utils.CheckTruth(v)) || (!ok && field.Name == "UpdatedAt" && (field.DataType == Time || field.DataType == Int || field.DataType == Uint)) {
if field.DataType == Time {
field.AutoUpdateTime = UnixTime
} else if strings.ToUpper(v) == "NANO" {
field.AutoUpdateTime = UnixNanosecond
} else if strings.ToUpper(v) == "MILLI" {
field.AutoUpdateTime = UnixMillisecond
} else {
field.AutoUpdateTime = UnixSecond
}
}
// ~
// GORM time types
const (
UnixTime TimeType = 1
UnixSecond TimeType = 2
UnixMillisecond TimeType = 3
UnixNanosecond TimeType = 4
)
フィールド名が CreatedAt
なら自動で日時を生成。
フィールド名が CreatedAt
でない場合でも、フィールドにタグをつけることで自動で日時生成対象にすることも可能。
https://gorm.io/ja_JP/docs/models.html#%E4%BD%9C%E6%88%90%E3%83%BB%E6%9B%B4%E6%96%B0%E6%97%A5%E6%99%82%E3%81%AE%E3%83%88%E3%83%A9%E3%83%83%E3%82%AD%E3%83%B3%E3%82%B0%EF%BC%8FUnix-%E3%83%9F%E3%83%AA%E3%83%BB%E3%83%8A%E3%83%8E-%E7%A7%92%E3%81%A7%E3%81%AE%E3%83%88%E3%83%A9%E3%83%83%E3%82%AD%E3%83%B3%E3%82%B0
// gormは自動でcreatedAtの日付を生成
type sampleA struct {
ID int
CreatedAt time.Time
}
// gormは自動でcreatedAtの日付を生成
type sampleB struct {
ID int
CreatedTestTime time.Time `gorm:"autoCreateTime"`
}
SaveメソッドからUpdateを実施する際の処理はどうなってる?
- Update関数
以下がSaveメソッドから呼び出されるUpdate関数です。
ConvertToAssignmentsがポイントです。
https://github.com/go-gorm/gorm/blob/master/callbacks/update.go#L56
func Update(config *Config) func(db *gorm.DB) {
supportReturning := utils.Contains(config.UpdateClauses, "RETURNING")
return func(db *gorm.DB) {
if db.Error != nil {
return
}
if db.Statement.Schema != nil {
for _, c := range db.Statement.Schema.UpdateClauses {
db.Statement.AddClause(c)
}
}
if db.Statement.SQL.Len() == 0 {
db.Statement.SQL.Grow(180)
db.Statement.AddClauseIfNotExists(clause.Update{})
if _, ok := db.Statement.Clauses["SET"]; !ok {
// この関数でクエリを組み立てています!!!
if set := ConvertToAssignments(db.Statement); len(set) != 0 {
defer delete(db.Statement.Clauses, "SET")
db.Statement.AddClause(set)
} else {
return
}
}
db.Statement.Build(db.Statement.BuildClauses...)
}
// ~
- ConvertToAssignments関数
ここでは、UpdatedAtのフィールドには自動で値をセットすること。
CreatedAtを対象から除外するなどのロジックは入っていないことを確認できます。
(※SelectやOmitで指定・除外していれば除外されます)
https://github.com/go-gorm/gorm/blob/master/callbacks/update.go#L131
// ~
switch updatingValue.Kind() {
case reflect.Struct:
set = make([]clause.Assignment, 0, len(stmt.Schema.FieldsByDBName))
for _, dbName := range stmt.Schema.DBNames {
if field := updatingSchema.LookUpField(dbName); field != nil {
if !field.PrimaryKey || !updatingValue.CanAddr() || stmt.Dest != stmt.Model {
// フィールド名がupdatedAtのとき field.AutoUpdateTImeが1になり、自動生成の値がセットされます!!
if v, ok := selectColumns[field.DBName]; (ok && v) || (!ok && (!restricted || (!stmt.SkipHooks && field.AutoUpdateTime > 0))) {
value, isZero := field.ValueOf(stmt.Context, updatingValue)
if !stmt.SkipHooks && field.AutoUpdateTime > 0 {
if field.AutoUpdateTime == schema.UnixNanosecond {
value = stmt.DB.NowFunc().UnixNano()
} else if field.AutoUpdateTime == schema.UnixMillisecond {
value = stmt.DB.NowFunc().UnixNano() / 1e6
} else if field.AutoUpdateTime == schema.UnixSecond {
value = stmt.DB.NowFunc().Unix()
} else {
// ここで自動生成された値がupdatedAtにセットされる
value = stmt.DB.NowFunc()
}
isZero = false
}
if (ok || !isZero) && field.Updatable {
// ここでupdateのset対象を追加していく
set = append(set, clause.Assignment{Column: clause.Column{Name: field.DBName}, Value: value})
assignField := field
if isDiffSchema {
if originField := stmt.Schema.LookUpField(dbName); originField != nil {
assignField = originField
}
}
assignValue(assignField, value)
}
}
} else {
if value, isZero := field.ValueOf(stmt.Context, updatingValue); !isZero {
stmt.AddClause(clause.Where{Exprs: []clause.Expression{clause.Eq{Column: field.DBName, Value: value}}})
}
}
}
}
default:
SaveメソッドでCreatedAtの更新を除外したいときは?
Omit
を使おう
type Sample struct {
ID int `gorm:"primarykey" name:"ID"`
CreatedAt time.Time
UpdatedAt time.Time
}
func (e Sample) SaveSample(tx *gorm.DB) {
// デバッグモードでSQLを出力するようにする
tx = tx.Debug()
tx.Create(&sample{ID: 1})
// 生成されるSQL
// INSERT INTO `samples` (`created_at`,`updated_at`,`id`) VALUES ('2024-07-26 05:30:45.075','2024-07-26 05:30:45.075',1)
tx.Omit("CreatedAt").Save(&sample{ID: 1})
// 生成されるSQL
// UPDATE `samples` SET `updated_at`='2024-07-26 06:20:45.558' WHERE `id` = 1
// ポイント:UPDATEの際はOmitを使うことでcreated_atの更新を除外できた
感想
便利だけど、思わぬ挙動とかでハマることもあるかも。
UpsertはSaveメソッドではなく、Create or Updatesなど自前で実装したほうが安全かもしれない。
→SaveでUpdateする際は事前にレコードチェックしてやれば安全かも。
GORMを使って以下を実現したいときは、結局Updatesでマップを渡すのがいいのかな・・・
Saveメソッドはシンプルに実装できるのが利点だけど、今回のようなハマるくらいならUpdatesを使って構造体をマップに変換して実行するほうがよいのかも?
→ただし構造体をmapに変換するのはリフレクションを使ったりする必要がありそうなのでちょと抵抗あり。。
- ゼロ値も更新したい
- UpdatedAtを更新したい
関連 GORM Updateメソッド一覧
https://qiita.com/yukiyoshimura/items/a6fa5ac9ff1bd8d03be5