最初に
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点ほど思いつく対処方法を記載します。
- Gormのバージョンを1.35以下に変更する。
バージョンを下げることになるので、最適化がされていない場合などのデメリットがあります - 参照先ではなく、外部キーで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 で発見した内容になります。最新のコードを確認した限り、改善されていないのかなと思います。どなたかの役に立てれば幸いです。