Go ORM の sqlboiler と xo を比較
はじめに
コードなんて人間が書かなければ書かないほどいいもんです。
さすがに言い過ぎな気もしますが、まあ自分で管理するコードは少ない方が楽かと思います。
今回は Go 実装におけるデータベースまわりにてコード自動生成ができる sqlboiler
と xo
について触ってみて比較をしてみたのでメモとして残しておきます。
前提条件としてすでにスキーマは作成されているものとします。
(なにかしらのマイグレーションツールにてスキーマを管理している状態です。)
この条件でなければもっとほかに良い ORM はあるかと思います。
上記リポジトリを眺めて sqlboiler
と xo
がパッとみ目的を達成できそうだったので選定しています。
他に今回の条件に合う ORM があれば教えていただきたいです。
準備
PostgreSQL
compose.yaml
にて PostgreSQL を用意します。
docker compose up --file compose.yaml -d
services:
postgres:
container_name: postgres
image: postgres:16.1-alpine
ports:
- 5432:5432
environment:
TZ: UTC
LANG: ja_JP.UTF-8
POSTGRES_DB: postgres
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_INITDB_ARGS: "--encoding=UTF-8"
POSTGRES_HOST_AUTH_METHOD: trust
restart: always
マイグレーションツール
使いこなせてないけど開発体験が非常に良い prisma を使います。
prisma の管理には bun を使います。
curl -fsSL https://bun.sh/install | bash
bun install prisma
bun run prisma init
prisma-client-go
というツールもあるようですが、今回はスキーマ管理にのみ prisma を使います。
インストール
sqlboiler
は以下のコマンドにてインストールします。
go install github.com/volatiletech/sqlboiler/v4@latest
go install github.com/volatiletech/sqlboiler/v4/drivers/sqlboiler-psql@latest
ref: https://github.com/volatiletech/sqlboiler?tab=readme-ov-file#download
xo
は以下のコマンドにてインストールします。
go install github.com/xo/xo@latest
ref: https://github.com/xo/xo?tab=readme-ov-file#installing-via-go
※ 私は go install
でツールをインストールすることが多いです。 Windows だろうが Mac だろうが Linux だろうが同じコマンドでインストールできるので。
バージョン
go version
go version go1.22.0 darwin/arm64
sqlboiler --version
SQLBoiler v4.16.2
xo --version
xo 0.0.0-dev
※ github だと v1.0.1でした ...
スキーマ
schema.prisma
にて以下のテーブルを用意しています。
model users {
id Int @id @default(autoincrement())
name String @unique
created_at DateTime @default(now()) @db.Timestamptz
updated_at DateTime @default(now()) @db.Timestamptz
}
pq
PostgreSQL へ接続するための Go での実装です。
postgres drivers のブランクインポートをお忘れなきように。
package main
import (
"database/sql"
"os"
// postgres driver.
_ "github.com/lib/pq"
)
func main() {
dsn := os.Getenv("DATABASE_URL")
db, err := sql.Open("postgres", dsn)
if err != nil {
panic(err)
}
defer db.Close()
if err := db.Ping(); err != nil {
panic(err)
}
}
sqlboiler
以下の準備およびコマンドにてコード自動生成ができます。
-
sqlboiler.toml
を作成pkgname = "sqlboiler" output = "pkg/sqlboiler" wipe = true no-tests = true add-enum-types = true [psql] dbname = "postgres" host = "localhost" port = 5432 user = "postgres" pass = "postgres" schema = "public" sslmode = "disable" blacklist = ["migrations"]
- 以下のコマンドを実行
sqlboiler psql
Create (INSERT)
user := sqlboiler.User{
Name: uuid.NewString(),
CreatedAt: now,
UpdatedAt: now,
}
if err := user.Insert(ctx, db, boil.Infer()); err != nil {
panic(err)
}
トランザクション
tx, err := db.BeginTx(ctx, nil)
if err != nil {
panic(err)
}
user1 := sqlboiler.User{
Name: uuid.NewString(),
CreatedAt: now,
UpdatedAt: now,
}
if err := user1.Insert(ctx, tx, boil.Infer()); err != nil {
if err := tx.Rollback(); err != nil {
panic(err)
}
}
user2 := sqlboiler.User{
Name: uuid.NewString(),
CreatedAt: now,
UpdatedAt: now,
}
if err := user2.Insert(ctx, tx, boil.Infer()); err != nil {
if err := tx.Rollback(); err != nil {
panic(err)
}
}
if err := tx.Commit(); err != nil {
panic(err)
}
※ boil.Infer()
が何者なのか深掘りしていないです ...
Read (SELECT)
単体取得(id検索)
user, err := sqlboiler.FindUser(ctx, db, 1)
if err != nil {
panic(err)
}
全取得
users, err := sqlboiler.Users().All(ctx, db)
if err != nil {
panic(err)
}
条件取得
users, err := sqlboiler.Users(sqlboiler.UserWhere.ID.IN([]int{user1.ID}), sqlboiler.UserWhere.Name.EQ(user1.Name)).All(ctx, db)
if err != nil {
panic(err)
}
Update (UPDATE)
user.Name = uuid.NewString()
if _, err := user.Update(ctx, db, boil.Infer()); err != nil {
panic(err)
}
Delete (DELETE)
if _, err := user.Delete(ctx, db); err != nil {
panic(err)
}
xo
以下のコマンドにてコード自動生成ができます。
-
以下のコマンドを実行
xo schema postgres://postgres:postgres@localhost:5432/postgres?sslmode=disable -o pkg/xo
Create (INSERT)
user := &xo.User{
Name: uuid.NewString(),
CreatedAt: now,
UpdatedAt: now,
}
if err := user.Insert(ctx, db); err != nil {
panic(err)
}
トランザクション
tx, err := db.BeginTx(ctx, nil)
if err != nil {
panic(err)
}
user1 := &xo.User{
Name: uuid.NewString(),
CreatedAt: now,
UpdatedAt: now,
}
if err := user1.Insert(ctx, tx); err != nil {
if err := tx.Rollback(); err != nil {
panic(err)
}
}
user2 := &xo.User{
Name: uuid.NewString(),
CreatedAt: now,
UpdatedAt: now,
}
if err := user2.Insert(ctx, tx); err != nil {
if err := tx.Rollback(); err != nil {
panic(err)
}
}
if err := tx.Commit(); err != nil {
panic(err)
}
Read (SELECT)
user, err := xo.UserByID(ctx, db, 1)
if err != nil {
panic(err)
}
(さらっと README.md を読んで使っているだけなので生成できるかも知れませんが)
sqlboiler
のように All みたいなクエリが生成されていませんでした ...
また、検索条件もインデックスが張られているものだけのようです。
Update (UPDATE)
user.Name = uuid.NewString()
if err := user.Update(ctx, db); err != nil {
panic(err)
}
Delete (DELETE)
if err := user.Delete(ctx, db); err != nil {
panic(err)
}
所感
- どちらも
sql.DB
を渡して使う -
INSERT
,UPDATE
,DELETE
は使い勝手としてはどちらも同じ感じ - 触っていないですがどちらも
UPSERT
も自動生成される -
SELECT
はsqlboiler
の方が利便性が高い
おわりに
ちょっと触ってみただけですが、使うなら sqlboiler
かなって感じがしました。
今回実装したコードは以下に置いておきます。
おまけ
prisma でマイグレーションをしたときに PostgreSQL だと DataTime を設定すると型が timestamp(3) without time zone
となるのですが、この形式に xo
が対応していないようです。
自動生成するコードの期待値は time.Time
ですが Timestamp3WithoutTimeZone
という型になります。
// User represents a row from 'public.users'.
type User struct {
ID int `json:"id"` // id
Name string `json:"name"` // name
CreatedAt Timestamp3WithoutTimeZone `json:"created_at"` // created_at
UpdatedAt Timestamp3WithoutTimeZone `json:"updated_at"` // updated_at
// xo fields
_exists, _deleted bool
}
xo
のコードを追うとおそらくここかなって感じです。
case "date", "timestamp with time zone", "time with time zone", "time without time zone", "timestamp without time zone":
goType, zero = "time.Time", "time.Time{}"
if typNullable {
goType, zero = "sql.NullTime", "sql.NullTime{}"
}
// ...
default:
goType, zero = schemaType(d.Type, typNullable, schema)
}
return goType, zero, nil
これ修正めんどくさそう ... あ、正規化で表現すればいいのか ... ???
コントリビュートチャンス ...
Discussion