🦔

gorm でフィールドにあるリソースの更新を余計に走らせないための工夫

2024/12/03に公開

きらぼしシステム株式会社でエンジニアをしている 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_resourcesgorm#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