メンテナンスモードとなった Go ORM の SQLBoiler と代替候補 Bob を比較
はじめに
データベースファーストで Go の ORM を自動生成することができるライブラリとして SQLBoiler があります。
単純な Web アプリケーションであれば SQLBoiler が生成したコードだけでシステムを構築できるのでとても重宝します。
しかし、そんな SQLBoiler が 2024年11月 にメンテナンスモードとなってしまいました。
Maintenance Mode
This package is currently in maintenance mode, which means:
- It generally does not accept new features.
- It does accept bug fixes and version compatability changes provided by the community.
- Maintainers usually do not resolve reported issues.
- Community members are encouraged to help each other with reported issues.
SQLBoiler は移行先として Bob と sqlc を提示しています。
今回の記事で SQLBoiler と似ている Bob を取り上げ、実装の比較を行い移行性について確認してみたのでご紹介します。
※ sqlc は SQLBoiler とは思想が異なると思うので今回の記事では扱いません。
TL;DR
SQLBoiler
「データベースファースト」と呼ばれるデータベーススキーマに合わせて Go の ORM を自動で生成するツールです。
データベーススキーマのライフサイクル管理は別途ツールが必要となります。
コード生成時に実際のデータベースへとアクセスしスキーマの型や各スキーマを操作する一般的な CRUD 操作クエリが用意されます。
Bob
基本的には SQLBoiler と同様のことができます。
公式ドキュメントに SQLBoiler との違いが概要として列挙されていますのでこちらもご参照ください。
ツールのインストール & バージョン
検証は PostgreSQL を用いたものになります。
他の RDBMS を扱う場合はドキュメントにてご確認ください。
SQLBoiler を以下のコマンドにて導入します。
go get -tool github.com/volatiletech/sqlboiler/v4@latest
go get -tool github.com/volatiletech/sqlboiler/v4/drivers/sqlboiler-psql@latest
go tool github.com/volatiletech/sqlboiler/v4 --version
SQLBoiler v4.18.0
以下のコマンドにてコードを自動生成します。
go tool github.com/volatiletech/sqlboiler/v4 psql
Bob を以下のコマンドにて導入します。
go get -tool github.com/stephenafamo/bob/gen/bobgen-psql@latest
go tool github.com/stephenafamo/bob/gen/bobgen-psql --version
bobgen-psql version v0.31.0
以下のコマンドにてコードを自動生成します。
go tool github.com/stephenafamo/bob/gen/bobgen-psql
設定(コンフィグ)ファイル
コード自動生成のためにデータベースドライバーの構成やオプションをファイルに記載することができます。
SQLBoiler は下記のような toml ファイルを用いて管理します。
pkgname = "models"
output = "models"
wipe = true
no-tests = true
add-enum-types = true
[psql]
dbname = "postgres"
host = "localhost"
port = 5432
user = "postgres"
pass = "postgres"
schema = "public"
sslmode = "disable"
[[types]]
[types.match]
db_type = "uuid"
[types.replace]
type = "uuid.UUID"
[types.imports]
third_party = [ "\"github.com/google/uuid\"" ]
ref: https://github.com/volatiletech/sqlboiler?tab=readme-ov-file#configuration
一方で Bob は下記のような yaml ファイルを用いて管理します。
no_factory: true
no_tests: true
psql:
dsn: "postgres://postgres:postgres@localhost:5432/postgres?sslmode=disable"
driver_name: "github.com/lib/pq"
output: "models"
pkgname: "models"
uuid_pkg: "github.com/google/uuid"
ref: https://bob.stephenafamo.com/docs/code-generation/configuration
※ 今回自動生成したコードはどちらも models
パッケージとしています。
オプションが微妙に異なるので SQLBoiler の設定をそのまま Bob へと適応するというのは難しそうです。
基本的な使い方
SQLBoiler の使い方は https://github.com/volatiletech/sqlboiler?tab=readme-ov-file#features--examples を参考にしています。
Bob の使い方は https://bob.stephenafamo.com/docs/models/table および https://bob.stephenafamo.com/docs/code-generation/usage を参考にしています。
Executor
クエリを実行するにあたり DB との接続に必要なインスタンスを用意する必要があります。
SQLBoiler も Bob も生成されたクエリを実行する際に用意したインスタンスを渡します。
SQLBoiler は database/sql.DB
を利用します。
ref: https://pkg.go.dev/database/sql#DB
実装は以下の通りとなります。
package main
import (
"database/sql"
"os"
// postgres driver.
_ "github.com/lib/pq"
)
func main() {
db, err := sql.Open("postgres", os.Getenv("DSN"))
if err != nil {
panic(err)
}
defer db.Close()
}
一方で Bob は独自の DB インスタンスを利用できます。
ref: https://pkg.go.dev/github.com/stephenafamo/bob#DB
実装は以下の通りです。
package main
import (
"os"
"github.com/stephenafamo/bob"
)
func main() {
db, err := bob.Open("postgres", os.Getenv("DSN"))
if err != nil {
panic(err)
}
defer db.Close()
}
スキーマ(モデル)
サンプルコードを提示するにあたり以下のようなIDと名前をフィールドに持つ users
テーブルを扱います。
CREATE TABLE users (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name TEXT NOT NULL,
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP
);
SQLBoiler
// Code generated by SQLBoiler 4.18.0 (https://github.com/volatiletech/sqlboiler). DO NOT EDIT.
// This file is meant to be re-generated in place and/or deleted at any time.
package models
// ...
// User is an object representing the database table.
type User struct {
ID uuid.UUID `boil:"id" json:"id" toml:"id" yaml:"id"`
Name string `boil:"name" json:"name" toml:"name" yaml:"name"`
CreatedAt time.Time `boil:"created_at" json:"created_at" toml:"created_at" yaml:"created_at"`
UpdatedAt time.Time `boil:"updated_at" json:"updated_at" toml:"updated_at" yaml:"updated_at"`
R *userR `boil:"-" json:"-" toml:"-" yaml:"-"`
L userL `boil:"-" json:"-" toml:"-" yaml:"-"`
}
※ userR
... users テーブルとのリレーション情報
※ userL
... 各リレーションのロード情報
Bob
// Code generated by BobGen psql v0.31.0. DO NOT EDIT.
// This file is meant to be re-generated in place and/or deleted at any time.
package models
// ...
// User is an object representing the database table.
type User struct {
ID uuid.UUID `db:"id,pk" `
Name string `db:"name" `
CreatedAt time.Time `db:"created_at" `
UpdatedAt time.Time `db:"updated_at" `
}
SELECT
SELECT * FROM "users";
SQLBoiler
users, err := models.Users().All(ctx, db)
if err != nil {
panic(err)
}
Bob
users, err := models.Users.View.Query().All(ctx, db)
if err != nil {
panic(err)
}
SELECT WHERE
SELECT * FROM "users" WHERE "id" = $1;
SQLBoiler
user, err := models.FindUser(ctx, db, uuid.MustParse("$1"))
if err != nil {
panic(err)
}
Bob
user, err := models.FindUser(ctx, db, uuid.MustParse("$1"))
if err != nil {
panic(err)
}
SELECT * FROM "users" WHERE "name" = $1;
SQLBoiler
users, err := models.Users(
models.UserWhere.Name.EQ("$1"),
).All(ctx, db)
if err != nil {
panic(err)
}
Bob
users, err := models.Users.View.Query(
models.SelectWhere.Users.Name.EQ("$1"),
).All(ctx, db)
if err != nil {
panic(err)
}
INSERT
INSERT INTO "users" ("name","created_at","updated_at") VALUES ($1,$2,$3) RETURNING "id"
SQLBoiler
user := models.User{
Name: "$1",
}
if err := user.Insert(ctx, db, boil.Infer()); err != nil {
panic(err)
}
Bob
user, err := models.Users.Insert(&models.UserSetter{
Name: omit.From("$1"),
}).One(ctx, db)
if err != nil {
panic(err)
}
UPDATE
UPDATE "users" SET "name"=$1,"updated_at"=$2 WHERE "id"=$3
SQLBoiler
user := models.User{
ID: uuid.MustParse("$3"),
Name: "$1",
}
if _, err := user.Update(ctx, db, boil.Infer()); err != nil {
panic(err)
}
Bob
user := models.User{
ID: uuid.MustParse("$3"),
}
if err := user.Update(ctx, db, &models.UserSetter{
Name: omit.From("$1"),
}); err != nil {
panic(err)
}
UPSERT
INSERT INTO "users" ("id", "name", "created_at", "updated_at") VALUES ($1,$2,$3,$4) ON CONFLICT (id) DO NOTHING
SQLBoiler
user := models.User{
ID: uuid.MustParse("$1"),
Name: "$2",
}
if err := user.Upsert(
ctx,
db,
false,
nil,
boil.Columns{},
boil.Infer(),
); err != nil {
panic(err)
}
Bob
if _, err := models.Users.Insert(&models.UserSetter{
ID: omit.From("$1"),
Name: omit.From("$2"),
}, im.OnConflict("id").DoNothing()).One(ctx, db); err != nil && err != sql.ErrNoRows {
panic(err)
}
INSERT INTO "users" ("id", "name", "created_at", "updated_at") VALUES ($1,$2,$3,$4) ON CONFLICT DO UPDATE SET "name" = EXCLUDED."name"
SQLBoiler
user := models.User{
ID: uuid.MustParse("$1"),
Name: "$2",
}
if err := user.Upsert(
ctx,
db,
true,
[]string{"id"},
boil.Infer(),
boil.Infer(),
); err != nil {
panic(err)
}
Bob
if _, err := models.Users.Insert(&models.UserSetter{
ID: omit.From("$1"),
Name: omit.From("$2"),
}, im.OnConflict("id").DoUpdate(
im.SetExcluded("name"),
)).One(ctx, db); err != nil {
panic(err)
}
DELETE
DELETE FROM "users" WHERE "id"=$1
SQLBoiler
user := models.User{
ID: uuid.MustParse("$1"),
}
if _, err := user.Delete(ctx, db); err != nil {
panic(err)
}
Bob
user := models.User{
ID: uuid.MustParse("$1"),
}
if err := user.Delete(ctx, db); err != nil {
panic(err)
}
TRANSACTION
BEGIN;
COMMIT;
SQLBoiler
tx, err := db.BeginTx(ctx, nil)
if err != nil {
panic(err)
}
// ...
if err := tx.Commit(); err != nil {
panic(err)
}
Bob
tx, err := db.BeginTx(ctx, nil)
if err != nil {
panic(err)
}
if err := tx.Commit(); err != nil {
panic(err)
}
おわりに
SQLBoiler と Bob の実装を比較してみて移行について調査してみました。
SQLBoiler からの移行の一助になれば幸いです。
今回試したコードは以下に置いておきます。
Discussion