最終更新:2020/09/18
GolangでデファクトライブラリになっているORM、gormのversion2がリリースされました。
早速version2にアップデートしましたが、version1からversion2への変更は破壊的な変更を含んでいて、ただ単にライブラリをupdateしただけでは既存のアプリケーションが動かなくなります。
この記事を読んでいる開発者の中にも、少し試してみて
「これは苦労しそうだ」
とアップデートすべきか悩んでいる人もいるのでないでしょうか。
開発において、アップデートをするかどうか悩むポイントは以下だと思います。
- verison1とverison2の違い
- アップデートにかかる工数
- 影響がでる点
- テストコードで使っている場合は、sqlmockの修正
この記事では、これらの疑問を解決します。
目次
1. verison1とverison2の違い
verison2ではversion1ではなかった機能が、多く追加されています。
実際の開発で特に役立ちそうな新機能は
- Batch Insert
- 複数データベースのオフィシャルプラグイン
- DBをread/writeに分割
です。
Batch InsertとDBのread/writeの分割は、大規模アプリケーションの場合は必要になるので、嬉しいですね。
また、verison2はverison1のコード更新ではなくリライトで、パッケージ自体が変更されています。
なので、コードを書き換える場合は import文から変更が必要になります。
パッケージ自体が異なるので、両方のversionを利用することも可能です。
2. アップデートにかかる工数
アップデートにかかる工数は、プロジェクトの規模や採用しているアーキテクチャーによりますが、
最初の予測より多め
に見積もっておいたほうが良いと思います。
verison1とverison2では、微妙に異なるクエリが発行されるので、sqlmockなどを使ってテストでsqlを確認している場合は、テストコードの書き直しが発生します。
また、version1では許された処理がverison2では不可能になっているパターンもあります。
特にraw関数で生SQLを扱っている場合は、修正が必要になる可能性が高いので、注意が必要です。
3. 影響のある点
gorm1からgorm2にupdateするにあたって、影響が出る箇所をまとめました。 他にも沢山あると思いますが、ここに記載した箇所は多くのプロジェクトでも遭遇するでしょう。
3-1. count文
count文を発行するcountメソッドが変わりました。 version1では、引数にinterface型を宣言していました。
func (s *DB) Count(value interface{}) *DB {}
version1では、型としてinterfaceを使っていました。 型にinterfaceを使うことで、型の制約を受けなくなります。
しかし、count文を発行したSQLで受け取るのは数値であることが確定しているので、意味のないinterface型の宣言だと言えるでしょう。
これがversion2だとint64型に変更されました。
func (db *DB) Count(count *int64) (tx *DB) {}
数値型に型が固定されました。 これは正しい変更だと思います。
一方で
int64型
で宣言されているので、intを使って値を受け取る処理を記述していた場合は、実装の変更が必要になります。
配列のlen関数と比較チェックなどをしている場合は、len関数をint64(len(hoge))のように変更する必要があります。
count文の実装が多いプロジェクトで、テストコードまで含めると結構な手間になります。
3-2. sql文
gorm1とgorm2では、発行されるsqlが若干変わります。
例えば、version1でusersテーブルからidでデータを取得するsql文は
SELECT * FROM `users` WHERE (id = ? and deleted_at is not null )
が発行されました。
これがversion2では
SELECT * FROM `users` WHERE id = ? AND `users`.`deleted_at` IS NULL
に変更されていました。
取得結果は同じですが、見ての通り、where文の順番、カッコやバッククオートの有無が異なっています。
他にもcount文がcount(*)からcount(1)になるなど、細かい修正が所々で行われています。
3-3. structを使ったwhere文
where文の条件をstructで実装している人も多いと思います。
version1で以下のようなコードで、where文が生成できました。
func (p *userRepositoryImpl) CountByQuery(user *model.User) (int64, error) {
var count int64
if err := p.db.Model(&model.User{}).Where(&user).Count(&count).Error; err != nil {
return -1, err
}
return count, nil
}
Whereに渡すuserのオブジェクトは、ポインタやアドレスでも構いません。
これは、version2では動きません。
version2で動かすには、
func (p *userRepositoryImpl) CountByQuery(user *model.User) (int64, error) {
var count int64
if err := p.db.Model(&model.User{}).Where(user).Count(&count).Error; err != nil {
return -1, err
}
return count, nil
}
のようにポインタに修正する必要があります。
version2は型に対して厳しくなっています。
3-4. rawを使ってLimit, offsetのチェーンができない
version1ではraw関数に生SQLを書いて、Limit, offsetを関数をチェーンでつなぐことができました。
sql := p.db.Raw("select * from users")
sql = sql.Limit(10)
sql = sql.Offset(0)
しかし、version2ではRaw関数を使った場合、チェーンはできなくなりました。 チェーンで繋いでSQLを作成したい場合は、Raw関数に生sqlを書くのでなく、Raw関数以外の関数を使ってチェーンで繋ぐ必要があります。
3-5. Limit, offsetの仕様変更
Limit, offsetは数値型になりました。
version1はなぜか数値型だったので、良い変更です。
また、offsetの値が0の場合はsqlに offset 0 と出力されるのではなく、非表示になりました。
これも無駄なクエリだったので、良い変更です。
3-6. insert, updateのカラム順の変更
insert, update文のカラム順が変わりました。
version2では、カラム名idがvalues文、set文の最後に指定されます。
3-7. ErrRecordNotFoundの変更
データが存在しない場合のエラー処理も変更になりました。
version1では、以下のようにロジックを実装しました。
import (
"github.com/jinzhu/gorm"
)
if gorm.IsRecordNotFoundError(error) {
// something
}
version2では以下のように書き直します。
import (
"errors"
"gorm.io/gorm"
)
if errors.Is(dbError, gorm.ErrRecordNotFound) {
// something
}
errorsのIs関数でエラーがErrRecordNotFoundがどうかを判定します。
4. sqlmockの修正と方法
sqlmockを使っている場合は、version1とversion2では実装方法が変わります。
version1ではsqlmockを使う場合、以下のようにコードを書きました。
db, mock, err := sqlmock.New()
if err != nil {
t.Fatalf("an error '%s' was not expected when opening a stub database connection", err)
}
dbClientMock, err := gorm.Open("mysql", db)
if err != nil {
t.Fatalf("an error '%s' was not expected when opening a stub database connection", err)
}
version2でsqlmockを使う場合、以下のようにコードを書き換えます。
db, mock, err := sqlmock.New()
if err != nil {
t.Fatalf("an error '%s' was not expected when opening a stub database connection", err)
}
dbClientMock, err := gorm.Open(
mysql.Dialector{Config: &mysql.Config{DriverName: "mysql", Conn: db, SkipInitializeWithVersion: true}}, &gorm.Config{},
)
if err != nil {
t.Fatalf("an error '%s' was not expected when opening a stub database connection", err)
}
5. version1の削除
全てのコードを修正したらターミナルで
go tidy -v
を実行します。
go tidyは不要なpackageを削除するコマンドです。
version1が使われている箇所が完全に修正されていれば、ライブラリが自動で削除されます。