gorm でフィールドにあるリソースの更新を余計に走らせないための工夫
きらぼしシステム株式会社でエンジニアをしている mickamy です。
今期の目標で、当社のプレゼンスを高める、というものをおいたので、開発を行う上で得た知見をご紹介したいと思います。
0. はじめに
当社では、バックエンドで使用するメインの開発言語として、Go を採用しています。その際、ORM として一番使われている gorm を採用することが多いです。
今回は、素朴に gorm を使った際に陥りがちな課題のひとつとして、不要なテーブルを更新してしまうケースについて紹介します。その後、それを避けるために私が採った手法をご紹介します。
1. 課題
以下のような構造体定義があるとします。
type User struct {
ID string
Profile UserProfile
}
type UserProfile struct {
UserID string
LastName string
FirstName string
}
// ユーザのプロフィールを必要とする適当なリソース
type SomeResource struct {
ID string
Users []User `gorm:"many2many:some_resource_users`
}
SomeResource
を作成するにあたり、User#Profile
はゼロ値でなく、gorm#Joins
などにより、値が読み込まれているものとします。
この時、以下のように gorm#Create
を行った時の DB 更新として、期待するものはSomeResource
の insert のみですが、実際には、以下のクエリが走ります(くどいですが、UserProfile
がゼロ値でない場合です)。
-
users
テーブルへのINSERT INTO ON CONFLICT DO UPDATE
-
user_profiles
テーブルへのINSERT INTO ON CONFLICT DO UPDATE
つまり、モデルが持つ他テーブルを参照するフィールドがゼロ値でない時、それらのテーブル全てに対して DB 操作が行われます。
これは、gorm がフィールドの dirty check をデフォルトで提供していないことを意味します。
今回のケースでは読み込まれているフィールドがひとつだからまだ許容できますが、これが多数ある場合、そのコストは無視できないものになります。この課題を解決するには、どうしたらいいでしょうか。
2. ありうる解決策
gorm#Select
第一にありうる選択肢として、gorm#Select
によるカラムの制御が挙げられるでしょう。
gorm のドキュメントに、以下の例があります
db.Select("Name", "Age", "CreatedAt").Create(&user)
// INSERT INTO `users` (`name`,`age`,`created_at`) VALUES ("jinzhu", 18, "2020-07-04 11:05:21.775")
ただ、今回の例では many2many を使った更新のため、この手法を採ることはできません。
中間テーブルの insert を自前で処理する
今回のケースでは、gorm の many2many の機能を使っているために事象が発生しているため、それを迂回して自前で処理を記述すれば、この問題は回避できます。
some_resources
の gorm#Create
と別で、some_resource_users
の挿入を行います。
これで一旦回避はできますが、処理が煩雑になり得ます。やはり、ORM の機能に乗りたいところです。
3. 今回採用した解決策
gorm の機能に乗るために、以下のようなメソッドを User
モデルに定義します。
func (m User) Detached() User {
return User{
ID: m.ID
// ここで Profile を指定しない
}
}
これにより、User#Profile
がゼロ値のオブジェクトを取得できます。SomeResource#Users
への append は、この User#Detached
の返り値を使用します。
u := repo.Get(ctx, id)
r := SomeResource{}
r.Users = append(r.Users, u.Detached())
これにより、user_profiles
への不要な更新を防ぐことができます。
一方、この手法では、users
への更新は防ぐことができません。これは、ORM を使うコストとして、受け入れています。
4. 結び
今回は、gorm の many2many を使った場合に発生する不要な DB 更新を防ぐ方法について紹介しました。
3 でご紹介した手法に対し、2. ありうる解決策
でご紹介した中間テーブルを自前で処理するパターンにもメリットはあります。それは、users
への無用な更新を避けることができる、ということです。一方で、その選択は gorm の機能を十分に発揮できないことを意味します。
プロジェクトによっては、2 でご紹介した選択肢を採ることもあるでしょうが、ちょうどいいそれとして、今回ご紹介した手法を検討してみるのもいいかもしれません。
Discussion