☠️

SQLBoilerに代わるORMapperを探す

2024/12/23に公開

はじめに

こんにちは。株式会社AI Shift開発チームの由利です。
この記事はAI Shift Advent Calendar 2024の23日目になります。
今回は以前ご紹介したSQLBoilerに代わるORMapperを探索してみたいと思います。

背景

なぜ今さらORMapperを探すのか。の背景についてです。
弊社でも利用しているGolang製ORMapperのSQLBoilderですが、つい先日メンテナンスモードに入ったとの告知がされました。(なんてことだ。。)

https://github.com/volatiletech/sqlboiler

上記にも記載がありますが、新機能の追加はされないのはもちろんのこと、Bug修正やバージョン互換の変更などもコミュニティの手に委ねられることになった模様です。
よく使う機能は既に実装されていましたし、template機構でのカスタマイズも出来たので、直近すぐに困ることはないとはいえ、継続利用に黄色信号となりました。

公式の推奨移行先

SQLBoiler公式READMEには下記が紹介されています。

  1. Bob - https://github.com/stephenafamo/bob
  2. sqlc - https://github.com/sqlc-dev/sqlc

Bob

https://github.com/stephenafamo/bob
こちらは公式記載にある通りSQLBoilerのメインメンテナが開発したORMです。
Star数は900弱、バージョン0.29.0(2024/12現在)と、まだまだ開発途上のようではありますが、SQLBoilerにinspireされて開発したとあるので、移行先として検討の余地はありそうです。

sqlc

https://github.com/sqlc-dev/sqlc
公式にも説明がある通り「It is Not an ORM」、ORMapperではなくSQLを記述してコード生成するツールです。type-safeに結果を取り扱うことができます。
Star数14k弱(2024/12現在)で、golang系のORMとしてよく話題になるentと同程度で安心して使えそう。
とは言え、そもそもORMではないため、既にSQLBoilerを導入しているProjectでの採用は難しそうです。。

ただ、sqlc公式にある「開発の動機」ですが
https://conroy.org/introducing-sqlc

For years, software engineers have been generating SQL queries from annotated objects in programming languages. SQL is already a structured, typed language; we should be generating correct, type-safe code in every programming language from the source of truth: SQL itself.

SQL自体が構造化されて型安全な言語なので、「SQLを生成する」のではなくて、「SQLからコードを生成する」が正しいと。
おっしゃる通りですね。。筆者個人的にはかなり共感しました。

弊社の既存コード群の中にはRaw SQLで記述されたものも残っているので、その部分にはsqlcで対応するのも良さそうです。
また、ORMを採用したプロジェクトでの開発中盤によくある議論として「複雑なQueryをどうORMapperで表現するか」という課題が出ることがあります。
筆者はRaw SQLで良いとする派閥ですが、そういったケースの多発が予想されるProjectは最初からsqlcを採用し「Raw SQLで対応する」と割り切るのも良さそうです。

公式推奨以外の移行先の模索

ent

https://github.com/ent/ent

SQLBoilerと異なり、通常entはスキーマをGoコードとして定義する利用方法になると思います。
今回はSQLBoilerのようにデータベースファースト(スキーマはDDLで定義して管理する)で利用したいため、既存DBからコード生成する必要がありますが、これはentimportを利用すれば解決出来そうです。
https://github.com/ariga/entimport

ただ、entimportのFuture Workを読むと
https://github.com/ariga/entimport/blob/master/README.md?tab=readme-ov-file#future-work

## Future Work

- Index support (currently Unique index is supported).
- Support for all data types (for example `uuid` in Postgres).
- Support for Default value in columns.
- Support for editing schema both manually and automatically (real upsert and not only overwrite)
- Postgres special types: postgres.NetworkType, postgres.BitType, *schema.SpatialType, postgres.CurrencyType,
  postgres.XMLType, postgres.ArrayType, postgres.UserDefinedType.

未対応のスキーマ定義が多々あることを伺わせます。
ent自体の指向とも異なるため、初期導入時のスキーマ定義コードの自動生成位に使うのが妥当と思われます。
これを継続的に利用してSQLBoilerのように運用するのは現実的ではなさそうです。

xo

https://github.com/xo/xo
xoはスキーマからmodelとCRUD操作のコードを生成することができるコマンドラインツールです。
templateでのカスタマイズも可能なため、もう少し高度な操作のコードを吐き出すことも出来そうではありますが、代替とするにはハードルが高そうです。
※そもそもの思想が異なるという背景もありそうです。

Part of xo's goals is to avoid writing an ORM, or an ORM-like in Go,
and to instead generate static, type-safe, fast, and idiomatic Go code across languages and databases.

https://github.com/xo/xo?tab=readme-ov-file#goals

模索結果としては...

他にもいくつか検討してみましたが、そもそもの思想が異なり大幅な書き換えが必要だったり、機能自体が不足しているなど、いずれもハードルは高そうという結論に至りました。。
模索の道は振り出しに戻りました。

公式推奨のBobを試してみる

となると、公式推奨のBobを試してみるしか手はなさそうです。
実際に使用してみましょう。Let's try!!

MySQL DBの用意

まずローカルにDBを準備します。
今回はsampleとして下記テーブルを作成して試してみようと思います。

CREATE table users (
  id int PRIMARY KEY,
  name varchar(100)
);

下記コマンドで作成しました。

$ docker run --rm -d --name bob-demo -p 3306:3306 -e MYSQL_ROOT_PASSWORD=pass -e MYSQL_DATABASE=example mysql:8.4
$ docker exec bob-demo mysql -ppass -e 'CREATE table example.users(id int PRIMARY KEY, name varchar(100))'
$ docker exec bob-demo mysql -ppass -e 'INSERT into example.users VALUES (1, "user1"), (2, "user2"), (3, "user3")'

modelの生成

公式手順に沿ってmodelを生成してみましょう。
https://bob.stephenafamo.com/docs/code-generation/mysql

SQLBoilerとは異なりdriverなどの準備は不要そうです。これは楽!

$ MYSQL_DSN=root:pass@tcp(localhost:3306)/example go run github.com/stephenafamo/bob/gen/bobgen-mysql@latest

実行したところ、modelsディレクトリ配下にコードが生成されました。
このあたりはSQLBoilerと同じですね。

https://bob.stephenafamo.com/docs/code-generation/mysql#driver-configuration
出力先も変更できそうです。

生成したmodelでQueryを実行してみる

早速出力したmodelでSelectしてみましょう!

package main

import (
    "context"
    "fmt"
    "github.com/ayuri-kn/sample/models"
    "github.com/stephenafamo/bob"
)

func main() {
	db, err := bob.Open("mysql", "root:pass@tcp(localhost:3306)/example")
	if err != nil {
		panic(err)
	}

	user, err := models.FindUser(context.Background(), db, 1)
	if err != nil {
		panic(err)
	}
	fmt.Println(user)

	users, err := models.Users.Query(
		models.SelectWhere.Users.ID.GT(1),
	).All(context.Background(), db)
	if err != nil {
		panic(err)
	}
	for i, user := range users {
		fmt.Printf("user[%d]:%v\n", i, user)
	}
}

上記コードを実行してみると、目的のデータが取得できました!
bob.Open()というのが慣れませんが、これは*sql.DBのQueryContext()メソッドが *sql.Rowsオブジェクトを返すため、モックや実装が非常に難しいから、とのこと。
https://bob.stephenafamo.com/docs/sql-executor/why-not

さあ、プロジェクトで試してみよう!

いよいよ、自分のプロジェクトのDBで試してみました。が、、

$ MYSQL_DSN=user:pass@tcp(host:port)/dbname go run github.com/stephenafamo/bob/gen/bobgen-mysql@latest

2024/12/23 00:45:53 unable to fetch table data: unable to load indexes: Error 1054 (42S22): Unknown column 's.expression' in 'field list'
exit status 1

生成できません。。
色々と試してみた結果、自環境のMySQLが古かったため(5.7)のようです。
そもそもアップデートの計画もあるため、こちらはMySQL自体のアップデート後に再度検証しようと思います。

この他、うまく生成できたmodelでcolumnのmappingに失敗してしまいました。
こういったトラブルシューティングをある程度覚悟する必要がありそうです。

ただ、下記の記述の通りDBが得意なところはRDB側に任せるといった割り切りもあり、筆者としては応援したくなるプロダクトでした。
https://bob.stephenafamo.com/docs/code-generation/ignored-features

## 自動でのTimestampの付与(createdAt/updatedAt)
ORMにこれがあるのは便利ですが、DBレベルで実装する方がずっと良いでしょう。
ですから、Bobにこれを実装する予定はありません。

## 論理削除
論理削除についての当面の計画はありません。
エッジケースが多く、特にリレーションシップや連鎖的な論理削除を考慮すると、非常に複雑になります。

まとめ

筆者の個人的な見解としては、SQLBoilerの移行先として適切なレベルのものはまだなさそうですが、現状の選択肢としては公式推奨のBobが挙げられるのではないかと感じました。
Bobが候補となれるように、個人での利用や周知を含めた応援をしようと考えています。

また、この機会に改めて様々なORMに触れることが出来ました。
自分が関与するプロジェクトでは採用を見送りましたが、今回魅力的なプロダクトに出会えたので、別の機会に試してみたいと思っています!

最後に

AI Shiftではエンジニアの採用に力を入れています!
少しでも興味を持っていただけましたら、カジュアル面談でお話しませんか?
(オンライン・19時以降の面談も可能です!)
【面談フォームはこちら】
https://hrmos.co/pages/cyberagent-group/jobs/1826557091831955459

AI Shift Tech Blog

Discussion