🐁

Go ORM の sqlboiler と xo を比較

2024/02/23に公開

はじめに

コードなんて人間が書かなければ書かないほどいいもんです。
さすがに言い過ぎな気もしますが、まあ自分で管理するコードは少ない方が楽かと思います。

今回は Go 実装におけるデータベースまわりにてコード自動生成ができる sqlboilerxo について触ってみて比較をしてみたのでメモとして残しておきます。

前提条件としてすでにスキーマは作成されているものとします。
(なにかしらのマイグレーションツールにてスキーマを管理している状態です。)
この条件でなければもっとほかに良い ORM はあるかと思います。

https://github.com/RyotaroSeto/star-golang-orms

上記リポジトリを眺めて sqlboilerxo がパッとみ目的を達成できそうだったので選定しています。
他に今回の条件に合う 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 を使います。

https://www.prisma.io/

prisma の管理には bun を使います。

curl -fsSL https://bun.sh/install | bash
bun install prisma
bun run prisma init

prisma-client-go というツールもあるようですが、今回はスキーマ管理にのみ prisma を使います。

https://github.com/steebchen/prisma-client-go

インストール

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

https://github.com/volatiletech/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

https://github.com/xo/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 も自動生成される
  • SELECTsqlboiler の方が利便性が高い

おわりに

ちょっと触ってみただけですが、使うなら sqlboiler かなって感じがしました。

今回実装したコードは以下に置いておきます。

https://github.com/otakakot/sample-go-generate-orm

おまけ

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