【GORM】V1からV2へアップグレードにチャレンジした話

f:id:kaminashi-developer:20210803192112j:plain こんにちは、株式会社カミナシのエンジニア @imu です。

はじめに

弊社のアプリケーションのバックエンドはGoでDBアクセスライブラリはGORMを使っています。
サービスリリース時からGORMを使っており、V1からV2にすることでパフォーマンス改善につながると思い、アップグレードにチャレンジしました。
色々詰まるところがあったので、共有を含めて何をやったのか書いてみます!

結論

早速結論ですが…
GORM V2へのアップグレードは現時点で保留となりました...
保留にした理由は以下の通りです。

  • V2で仕様変更になった関数が多く影響範囲が広い
  • V2で廃止された関数があり代替案の検証が必要である
  • そもそもAPIのテストが完全でないのでアップグレードが怖い

APIの網羅テストがあれば思い切った変更ができたのですが、完全ではないので一旦網羅テストを書いてからアップグレードすべきという結論に至りました。

ここから先はどういった変更対応をしたのかを共有したいと思います。
一部未確認の箇所がありますのでご了承ください。

対応内容

Logger, Datadog Trace, サードパーティ製のBulkInsertも変更しておりますが、あまり記事で見かけない対応を共有したいと思います。

  • PrimaryKeyの指定方法を変更
// V1
`gorm:"primary_key" json:"id"`

// V2
`gorm:"primaryKey" json:"id"`
  • jointable_foreignkey, association_jointable_foreignkeyの指定方法を変更
// V1
`gorm:"jointable_foreignkey:hoge;association_jointable_foreignkey:huga;" json:"ids"`

// V2
`gorm:"joinForeignKey:hoge;joinReferences:huga;" json:"ids"`
  • foreignkey, association_foreignkeyの指定方法を変更
// V1
`gorm:"foreignkey:hoge;association_foreignkey:huga;" json:"id"`

// V2
`gorm:"foreignKey:hoge;references:huga;" json:"id"`

詳細はこちらを確認してください。
gorm.io

  • V2でRecordNotFound()判定

エラーハンドリング方法が変わっているため、自前で共通関数を作成し、errorオブジェクトを渡して判定するようにしました。

import (
    "errors"

    "gorm.io/gorm"
)

func RecordNotFound(err error) bool {
    return errors.Is(err, gorm.ErrRecordNotFound)
}

gorm.io

  • First, Findのエラーハンドリング

RecordNotFound()メソッドがV2から廃止になったことで、エラーハンドリングで注意が必要なケースがありました。

// V2
var db *gorm.DB
user := &orm.User{}
...
err := db.Where("uuid = ?", library.UUID()).First(user).Error
pretty.Println(err)
pretty.Println(errors.Is(err, gorm.ErrRecordNotFound))

> &errors.errorString{s:"record not found"}
> bool(true)

user := &orm.User{}
err := db.Where("uuid = ?", library.UUID()).Find(user).Error
pretty.Println(err)
pretty.Println(errors.Is(err, gorm.ErrRecordNotFound))

> nil
> bool(false)

.Find().Errorとした場合、gorm.ErrRecordNotFoundにならないので注意してください。
.First().Errorにメソッドを変更するか、nilならNotFoundにするようにしましょう。

  • テーブル名取得
// V1
var db *gorm.DB
var st orm.Users
...  
pretty.Println(db.NewScope(st).TableName())

> users

// V2
var db *gorm.DB
var st orm.Users
...
stmt := &gorm.Statement{DB: db}
stmt.Model = st
stmt.Parse(stmt.Model)
pretty.Println(stmt.Table)

> users

V2でテーブル名を取得する処理は、Execute()時点で何かしらしていると踏んでコードを読んで書きました。 f:id:kaminashi-developer:20210802190729p:plain

  • カラムチェック
// V1
var db *gorm.DB
var st orm.Users
...  
pretty.Println(db.NewScope(st).HasColumn("Hoge"))

// orm.UsersにHogeカラムがあればtrue
> bool(true)

// V2
var db *gorm.DB
var st orm.Users
...  
pretty.Println(db.Migrator().HasColumn(st, "Hoge"))

> bool(true)
  • QueryExpr
// V1
var db *gorm.DB
var st orm.Users
...  
pretty.Println(db.Table("sample").Where("id = 1").QueryExpr())

> &gorm.SqlExpr{
    expr: "SELECT * FROM `sample`  WHERE (id = 1)",
    args: nil,
}

// V2
var db *gorm.DB
var st orm.Users
...  
pretty.Println(db.Table("sample").Where("id = 1"))

> V1の処理はサブクエリだと思っているのでコレで大丈夫なはず…(未確認です)

おわりに

個人的にはやりきりたいと思って取り組みましたが、思いの外時間が掛かりすぎるのと、V2に変更したときの安心感(テスト)がないので保留という着地をしました…。
限られたリソース内で出来なかった悔しさを晴らすために(?)、弊社では一緒に働くメンバーを幅広く募集しております!

特に…

  • 一つの課題に向き合うことが好きな方
  • 難易度が高い課題が好きな方
  • 作ったモノがどういった使われ方をしているのか興味があり、現場に行きたい方

です!

興味がある、話を聞いてみたい、応募したいという方はお気軽にご応募ください!

herp.careers