ORM を GORM から SQLBoiler に変えた理由

こんにちは。AI サービス開発部の千坂です。 Go に generics が入るのを密かに楽しみにしています。

今回は、Go 言語で使う ORM を GORM から SQLBoiler に変えた経緯を説明します。 個人で書く場合は ORM より生 SQL 派ですが、業務では開発効率重視で ORM を使います。

ORM とは O/R (Object-relational) マッピング(またはマッパー)の略で、プログラム上のオブジェクトと RDB のデータを対応付けること、およびそれを行うツールを表します。 Go の ORM では通常、構造体と RDB のデータを対応付けます。

本記事では GORM と SQLBoiler の概要および長所・短所を紹介していますが、それらは数ある特徴のうちの一部です。 これらの2つ以外にも ORM はありますので、ORM を利用するかどうか、またどの ORM を利用するかを検討する際には、他の情報も参照して要件に合ったものを選ぶようにしましょう。

本記事に記載しているソースコードはエラー処理等を省略しています。 そのまま利用しないようにお願いします。

GORM

概要

GORM (https://gorm.io) は Go で利用可能な ORM の1つで、オートマイグレーションの機能も持っています。

長所

GORM の主な長所の1つは、やはり構造体を定義するだけで ORM としての機能とマイグレーションの両方が使えることかと思います。

例えば、以下のような構造体を定義して AutoMigrate 関数を呼ぶだけでテーブルが生成できます:

type User struct {
    ID   int
    Name string
}

この Users を元に users という名前のテーブルが生成され、ID に対応するカラム id が主キーとなります。 テーブル名やカラム名はカスタマイズ可能です。

レコードを作成するには、例えば以下のように書きます:

user := User{
    ID:   1,
    Name: "OPTiM",
}
db.Create(&user)

最低限のコード量で目的が実現できているのが分かるかと思います。

短所

GORM の関数には、至るところに interface{} 型が使われています。

例えば、条件に合うレコード一覧を取得する Find 関数のシグネチャは以下のようになっています:

// Find find records that match given conditions
func (db *DB) Find(dest interface{}, conds ...interface{}) (tx *DB) {

引数 conds には主キーに対応する型のスライス等を指定できますが、この関数のコメントだけを見てもそれは分かりません。 (GORM のドキュメントから対応するコード例を探すことで使い方が分かります。) 間違えてもコンパイルエラーにならないため、単純なコーディングミスに気づくのが遅れがちです。

実行時にエラーが発生した時、どこをどう直せば良いのか探るのに多くの時間を費やしました。

例えば、以下のようなテーブル(に対応する構造体)を用意し、

type Data struct {
    ID      int
    Content [4]byte
}

マイグレーションしてレコードを作成します。

db.AutoMigrate(&Data{})
db.Create(&Data{ID: 1, Content: [4]byte{1, 2, 3, 4}})

すると、Create の実行時に以下のエラーが発生します:

ERROR: column "content" is of type bytea but expression is of type record (SQLSTATE 42804)

マイグレーションには成功しているのですが、これは Content の型を []byte に変更するなどして回避可能です。

SQLBoiler

概要

SQLBoiler (https://github.com/volatiletech/sqlboiler) は、GORM と同じく Go で利用可能な ORM の1つですが、マイグレーション機能は持っていません。

SQLBoiler はコードから DB のテーブルを生成するのではなく、あらかじめ作成済みの DB のテーブルからコードを生成します。 そのため、他のマイグレーションツール等と組み合わせて使うことが想定されています。

長所

事前のコード生成により、各テーブルに対応した型や、そのテーブルに対する操作を行う関数が生成されます。

型が分かっている状態で関数が生成されるため、適切な型の関数が生成されていて、単純なコーディングミスが比較的起こりにくくなっています。

レコードを取得するには、例えば以下のように書きます:

user, _ := models.FindUser(ctx, db, 1)

FindUser の返り値はもちろん User (と error )であり、interface{} ではありません。

また、テーブル名やカラム名にも対応する定数が生成されるため、例えば where 句を書く際に typo をすることもありません。

where 句は、例えば以下のように書きます:

users, _ := models.Users(
    models.UserWhere.Name.EQ("OPTiM"),
).All(ctx, db)

短所

事前に DB のテーブルを作成しておく必要があるため、マイグレーションツール等は別途必要になります。 (マイグレーションツールを自由に選べるというメリットにも見えますが、GORM でも別途マイグレーションツールを利用することはできるため、その観点では GORM と変わりません。)

また、コード生成を行うために実際に DB を立ち上げる必要があります。 もっとも、DB は Docker で簡単に立ち上げられる場合がほとんどかと思いますので、これはあまり負担になりません。

おわりに

実行時エラーと格闘する時間を減らすため、私のチームでは SQLBoiler を採用することにしました。 マイグレーションには sql-migrate を利用しています。

ORM の比較と言いつつ型がありがたいという話ばかりでしたが、皆様の ORM 選定の参考になれば幸いです。

オプティムは一緒に働く仲間を募集しています。