LoginSignup
4
0

More than 1 year has passed since last update.

Gormのモデルで外部キーにポインターを利用すると、想定外のデータが取得された

Last updated at Posted at 2022-12-19

最初に

Gormを利用していて、テストの最中にバグにぶつかってしまい、苦労しました。
そのため、今回の記事を共有のために、記事として残そうと思いました。

事象

Gorm (v1.23.8) で、モデルの外部キーにポインターを利用してNULLを許可した際に、Joinを用いてデータを取得すると、参照先のデータが想定と異なる場合がありました。

以下のデータを例に説明します。

Userテーブル

ID Name CorporateID
1 Aさん 1
2 Bさん NULL
3 Cさん NULL
4 Dさん 2

Corporateテーブル

ID Name
1 A社
2 B社

下記のプログラムは単純にユーザーと企業テーブルをJoinして、ユーザーに企業が紐付いていれば企業名を表示するプログラムです。
ただ、実行すると想定した挙動と異なってしまいました。

type Corporate struct {
	ID   uuid.UUID `gorm:"primaryKey;type:char(36);comment:企業ID"`
	Name string    `gorm:"type:varchar(32);not null;comment:企業名"`
}

type User struct {
	ID          uuid.UUID  `gorm:"primaryKey;type:char(36);comment:ユーザーID"`
	Name        string     `gorm:"type:varchar(32);not null;comment:ユーザー名"`
	CorporateID *uuid.UUID `gorm:"type:char(36);comment:企業ID"`
	Corporate   *Corporate `gorm:"references:ID"`
}

users := make([]User, 0)
db.Model(&User).Joins("Corporate").Find(&users)

for _, user := range users {
    if user.Corporate != nil {
        fmt.Println(user.Corporate.Name)
    }
}

// 実行結果
A社
A社
A社
B社

本来であれば、以下のように表示されてほしいところでした。

// 想定結果
A社
B社

原因

GitHubで調べてみると、Gormv1.23.6以降のバグであることが判明しました。
プルリク: https://github.com/go-gorm/gorm/pull/5388
コミット: https://github.com/Bexanderthebex/gorm/commit/90cceca6e7eb72216bf5d20e8808ae648721315b

上記のプルリクエストで、Scan()関数の最適化により、変数を毎回作成するのではなく、再利用するように修正されています。外部キーがNullの場合、初期化されずに前回の値を引き継いでしまうため、外部キーがNullにも関わらず、一つ前のレコードの内容が取得できてしまう場合があるみたいです。

先程の例を用いて原因を説明します。

Userテーブル

ID Name CorporateID
1 Aさん 1
2 Bさん NULL
3 Cさん NULL
4 Dさん 2

Corporateテーブル

ID Name
1 A社
2 B社

SQLで上記のデータをLeft Joinすると以下のデータになるかと思います。

User.ID User.Name Corporate.ID Corporate.Name
1 Aさん 1 A社
2 Bさん NULL NULL
3 Cさん NULL NULL
4 Dさん 2 B社

ただ、Gorm の機能を利用してデータを取得すると、変数を再利用する影響で、以下のようになってしまいました。

User.ID User.Name User.CorporateID Corporate.Name
1 Aさん 1 A社
2 Bさん NULL A社
3 Cさん NULL A社
4 Dさん 2 B社

外部キーである CorporateID が Nullの場合、前回のデータを引き継ぐので、上記の例だとBさんおよびCさんの会社名はNullが正しいにも関わらず、A社となってしまいます。
そのため、以下のような出力となってしまいました。

// 実行結果
A社
A社
A社
B社

対処法

2点ほど思いつく対処方法を記載します。

  1. Gormのバージョンを1.35以下に変更する。
    バージョンを下げることになるので、最適化がされていない場合などのデメリットがあります
  2. 参照先ではなく、外部キーでnil判定する
    今回の例ですと、Corporateではなく、CorporateIDがnil判定するようにします
users := make([]User, 0)
db.Model(&User).Joins("Corporate").Find(&users)
for _, user := range users {
    if user.CorporateID != nil {
        fmt.Println(user.Corporate.Name)
    }
}

// 実行結果
A社
B社

処理をする上で特に問題がないのであれば、今回の対処としても2は採用しました。

まとめ

今回紹介した内容は Gorm 1.23.8 で発見した内容になります。最新のコードを確認した限り、改善されていないのかなと思います。どなたかの役に立てれば幸いです。

4
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
0