こんにちは。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 選定の参考になれば幸いです。
オプティムは一緒に働く仲間を募集しています。