画竜点睛を衝く@mapyo

日々やった事をつらつらと書くブログです

Gormのv1で想定外の挙動になることを防ぐために BlockGlobalUpdate(true) を設定しましょう

2024/03/31 追記

https://pkg.go.dev/github.com/jinzhu/gorm#DB.BlockGlobalUpdate

結論、 BlockGlobalUpdate で trueを設定しましょう。 v1の場合これがデフォルトでfalseになっており、v2からはデフォルトでtrueになっています。なので、v1のGormを使っていればwhere句なしのupdate/deleteはガードされます。 deleted_atを使っていても有効です。

ただし、structのzero valueの見落としがちな特性が完全になくなるわけではなく、引き続き残るため、この観点は引き続き注意が必要です。

完全にv1にこのメソッドがあることに気がついていなかった。。。


今更感あるが改めて現象の整理と対応方針を考えてみる Gormのv1とは以下で、

https://github.com/jinzhu/gorm

今現在は

https://github.com/go-gorm/gorm

こちらが最新(v2)になっている。

v1のドキュメントとv2のドキュメントは違うので、検索する時にはバージョンを確認するのを忘れないようにするべし。

https://v1.gorm.io/ja_JP/docs/index.html

今回はv1の話です。

どういう状態の時に想定外の挙動になるのか

structを使った際にzero valueの仕様の部分で想定外の挙動になるので、そこを注意さえできれば問題はなさそう。sliceの中がstructの場合も注意。

ライブラリとしては意図した挙動で仕様通りではあるのだけど、型でガード出来る部分でもないので、意識して書く必要がある。

Where句

whereの検索条件にstructを使った時に、zero valueな値が設定されていると、その部分がSQLのwhere句から設定されない状態になる。書いた人視点では明示的に書いているつもりだけど、それがSQLのwhere句に反映されていない状態になるため、意図しない検索結果となる。where句はみなさんご承知の通り、select, update, deleteの全てに使われるものであり、全クエリでこれを意識した上で書く必要がある。

詳しくは以下。

https://v1.gorm.io/docs/query.html

NOTE When query with struct, GORM will only query with those fields has non-zero value, that means if your field’s value is 0, '', false or other zero values, it won’t be used to build query conditions, for example:

Structを使った時に、zero valueな値を設定するとこれはクエリの検索結果には使われなくなる。

db.Where(&User{Name: "jinzhu", Age: 0}).Find(&users)
//// SELECT * FROM users WHERE name = "jinzhu";

公式のサンプルにかかれてあるが、zero valueな値を指定すると、その値は検索条件を設定することが出来ない。

Update

データを更新する時に意図した更新がされないケース。こちらもまたzero valueの問題で、こちらも公式ドキュメントにWarningとして記載がある。

https://v1.gorm.io/docs/update.html

// WARNING when update with struct, GORM will only update those fields that with non blank value
// For below Update, nothing will be updated as "", 0, false are blank values of their types
db.Model(&user).Updates(User{Name: "", Age: 0, Actived: false})

zero valueはupdateのクエリが一切発行されない。上記は何も更新されない状態となる。

また、db.Modelにわたす際のstructがzero valueが含まれている時も同様に、where句の条件から除外されるので、ここも注意しないともれなく全件更新となる。

普通にそんな書き方なんてしないよと思ったあなた!検索した結果がもし存在しない場合、db.Modelメソッドに渡すStructはid等がzero valueとなり、もれなく全件更新となる。

var result User
db.Where(&User{Num: 10}).First(&result)
db.Model(&result).Update(User{Bool: true})

Delete

https://v1.gorm.io/docs/delete.html

WARNING When deleting a record, you need to ensure its primary field has value, and GORM will use the primary key to delete the record, if the primary key field is blank, GORM will delete all records for the model

例えば、以下のような書き方をした場合、

// Delete an existing record
db.Delete(&email)
//// DELETE from emails where id=10;

emailのprimary_keyがzero valueだと、全件削除になってしまう。

普通にそんな書き方なんてしないよと思ったあなた。検索した結果をそのままidに設定して削除するなんて処理書くとおもうのだけど、検索した結果を適切にハンドリングしておいかないと、検索対象が存在しなかった場合に、id=0のzero valueで構造体が返ってくるので、それをそのまま指定すると、もれなく全件削除になるというわけです。こういうやつ。(あとから見直すと、Updateと同じパターンだ)

var email Email
db.Where(&Email{ID: 99999999}).First(&email)
// emailの検索結果がないので、zero valueのid=0がセットされたまま。
db.Delete(&email)

あと、あるあるなパターンとしてもう一個思ったのが、バッチ処理で特定のレコードを検索、そして削除処理を実行する場合。一般的にバッチ処理は冪等性を持った作りにしているはず。一度実行したから、もう一度実行しても、特に問題ないはずだよな。。という気持ちで実行したら、特定のレコードは初回の実行で削除されてしまい、検索結果に出てこないので、今度は全件削除されてしまう。なんてこともあるかもしれない。

対応方針

チームやコードベースによって違う

挙動と対応方針について上記のサイトで詳しくまとめてくださっていた。大変ありがたい!!!! ここを読み込んだうえでどういう対応を取ればよいのか各自で検討するのがよさそう。

個人的には最低限UpdateやDeleteに関わる部分はstructを使うことを中止した方がよいような気はする。。。が、どうなんだろうか。

structを引き続き使うことを決定したとしても、仕様をチーム全員が理解したうえでレビューしたり、プルリクのチェックリストに追加したりは最低限実施する必要があると思う。また、ゼロ値が入っていないかを全ソースに毎回チェックを入れる。。。?

MySQLの場合、 sql_safe_updatesを有効にする

これはとりあえず、ONにしておくとよいのではないかと考えた。

クライアントから接続する場合は、--safe-updates も選択肢としてはありそうだが、これをつけると sql_select_limit=1000, max_join_size=1000000; この設定も合わせて有効になってしまうので、使われ方によっては影響が何かしらに出てくるかもしれない。 その辺を考慮して、既存のコードのへの影響を考えると、 sql_safe_updates をグローバルにONにしておくのがいいのではないかと考えた。

※これを実施したとしても、Soft Deleteの機能(DeletedAtを使っている)を使っている場合は、where句に  deleted_at IS NULL などの条件が入るため、無意味。。

※gormのv2では、BlockGlobalUpdateの機能がデフォルトで入っており、明確に書かないとwhere句なしの全件更新は出来なくなっている。Soft Deleteを使っていても効く。

https://gorm.io/ja_JP/docs/v2_release_note.html#BlockGlobalUpdate

所感

Gorm V2では、全件更新に対する対策や、個別のカラムを指定してUpdateする機能追加は入っているものの、structのzero valueは言語仕様上、どうしようもないと思われるので、どのみち意識して引き続き書く必要がある。ここは皆さんどんな方針で対応してるんだろうか。気になる。ただ、DeletedAtの時もガードしてくれるので、普通にアップデートした方が良い。いろいろ違うようだけど。。

気になった方はサンプルを残しておくのでいろいろと挙動を試してみてください。

https://github.com/mapyo/GolangSample/pull/1/files

参考文献