🎒

【Go】ORM、Bun について

2023/10/22に公開

はじめに

Go の有識者がオススメしている ORM、Bun についてまとめてみました。

Bun とは

Bun とは Go 製の SQL ファーストなデータベースクライアントのことです。
Bun には2つの目的があります。

  • 古き良き SQL を使ってクエリを書けるようにすること。
  • 実行結果を一般的な Go の型(構造体、マップ、スライスなど)に割り当てられるようにすること。

Bun を使うことで、以下のような複雑なクエリを綺麗に生成できます。

WITH regional_sales AS (
    SELECT region, SUM(amount) AS total_sales
    FROM orders
    GROUP BY region
), top_regions AS (
    SELECT region
    FROM regional_sales
    WHERE total_sales > (SELECT SUM(total_sales)/10 FROM regional_sales)
)
SELECT region,
       product,
       SUM(quantity) AS product_units,
       SUM(amount) AS product_sales
FROM orders
WHERE region IN (SELECT region FROM top_regions)
GROUP BY region, product
regionalSales := db.NewSelect().
	ColumnExpr("region").
	ColumnExpr("SUM(amount) AS total_sales").
	TableExpr("orders").
	GroupExpr("region")

topRegions := db.NewSelect().
	ColumnExpr("region").
	TableExpr("regional_sales").
	Where("total_sales > (SELECT SUM(total_sales) / 10 FROM regional_sales)")

err := db.NewSelect().
	With("regional_sales", regionalSales).
	With("top_regions", topRegions).
	ColumnExpr("region").
	ColumnExpr("product").
	ColumnExpr("SUM(quantity) AS product_units").
	ColumnExpr("SUM(amount) AS product_sales").
	TableExpr("orders").
	Where("region IN (SELECT region FROM top_regions)").
	GroupExpr("region").
	GroupExpr("product").
	Scan(ctx)

Bun の公式ドキュメントによると、メジャーな ORM の Gorm よりも高速に動作するみたいです。

DB に接続する

前提

事前に DB を起動しておく必要があります。
Bun がサポートしている DB は以下です。

  • PostgreSQL
  • MySQL 5以上 (+ MariaDB)
  • MSSQL(SQL Server v2019.CU4 v1.1.x 以降)
  • SQLite

必要なパッケージのインストール

実行するために必要なパッケージをインストールします。

Bun 本体

go get github.com/uptrace/bun@latest

Bun の SQL ダイアレクト

SQL ダイアレクトをインストールすることで、DB 固有の SQL クエリが使えるようになります。
使用する DB に対応する SQL ダイアレクトをインストールしてください。

# PostgreSQL
go get github.com/uptrace/bun/dialect/pgdialect@latest

# MySQL(MariaDB)
go get github.com/uptrace/bun/dialect/mysqldialect@latest

# MSSQL
go get github.com/uptrace/bun/dialect/mssqldialect@latest

# SQLite
go get github.com/uptrace/bun/dialect/sqlitedialect@latest

クエリを標準出力するために必要なパッケージ

必須ではありませんが、便利なのでインストールしておくことをオススメします。

go get github.com/uptrace/bun/extra/bundebug@latest

実際に接続する

Bun で DB に接続する準備が整いました。
実際に接続してみます。

※ 記事内では PostgreSQL v14.9 を使用します

package main

import (
	"context"
	"database/sql"

	_ "github.com/lib/pq"
	"github.com/uptrace/bun"
	"github.com/uptrace/bun/dialect/pgdialect"
	"github.com/uptrace/bun/extra/bundebug"
)

func main() {
	pool, err := sql.Open("postgres", "user=wasu host=localhost dbname=bun sslmode=disable")
	if err != nil {
		panic(err)
	}
	defer pool.Close()

	// pgdialect.New()の部分は、使用する SQL ダイアレクトごとに適宜変更してください。
	db := bun.NewDB(pool, pgdialect.New())

	// クエリを標準出力するコードです。
	// 動作が分かりやすいため、入れておくことをオススメします。
	db.AddQueryHook(bundebug.NewQueryHook(
		bundebug.WithVerbose(true),
	))

	// 「SELECT 1」を実行する
	if _, err := db.ExecContext(context.TODO(), "SELECT 1"); err != nil {
		panic(err)
	}
}

ターミナルに以下のようなログに出力されたら成功です。

[bun]  21:59:12.032   SELECT                  147µs  SELECT 1

テーブルの操作

Bun は構造体ベースのモデルを使用してクエリを構築し、実行結果を読み取ります。
モデルの例は以下です。

type User struct {
	bun.BaseModel `bun:"table:users,alias:u"`

	ID   int64 `bun:",pk,autoincrement"`
	Name string
}

Bun はエクスポートされていないフィールドを無視します。
例えば UserIDid とすると、id はないものとして扱われるので注意しましょう。

モデル定義の詳細は、公式の Defining models を参照してください。

作成(CREATE TABLE)

テーブルを作成します。

if _, err := db.NewCreateTable().Model((*User)(nil)).Exec(context.TODO()); err != nil {
	log.Fatalln(err)
}

実行結果は以下です。

[bun]  16:58:02.275   CREATE TABLE         67.249ms  CREATE TABLE "users" ("id" BIGSERIAL NOT NULL, "name" VARCHAR, PRIMARY KEY ("id"))

削除(DROP TABLE)

テーブルを削除します。

if _, err := db.NewDropTable().Model((*User)(nil)).Exec(context.TODO()); err != nil {
	log.Fatalln(err)
}

実行結果は以下です。

[bun]  17:23:00.804   DROP TABLE           21.366ms  DROP TABLE "users"

レコードの操作

作成(INSERT)

users テーブルのレコードを作成します。

user := &User{Name: "鈴木太郎"}
if _, err := db.NewInsert().Model(user).Exec(context.TODO()); err != nil {
	log.Fatalln(err)
}

実行結果は以下です。

[bun]  17:36:14.011   INSERT               15.802ms  INSERT INTO "users" ("id", "name") VALUES (DEFAULT, '鈴木太郎') RETURNING "id"

また、バルクインサートも簡単に実行することができます。

user1 := &User{Name: "鈴木太郎"}
user2 := &User{Name: "田中太郎"}
users := []*User{user1, user2}
if _, err := db.NewInsert().Model(&users).Exec(context.TODO()); err != nil {
	log.Fatalln(err)
}

バルクインサートの実行結果は以下です。

[bun]  17:40:11.849   INSERT                14.17ms  INSERT INTO "users" ("id", "name") VALUES (DEFAULT, '鈴木太郎'), (DEFAULT, '田中太郎') RETURNING "id"

更新(UPDATE)

users テーブルのレコードを更新します。

user := &User{ID: 1, Name: "斎藤ゆうき"}
if _, err := db.NewUpdate().Model(user).Column("name").WherePK().Exec(context.TODO()); err != nil {
	log.Fatalln(err)
}

実行結果は以下です。

[bun]  17:44:09.274   UPDATE               13.202ms  UPDATE "users" AS "u" SET "name" = '斎藤ゆうき' WHERE ("u"."id" = 1)

取得(SELECT)

users テーブルの単一レコードを取得します。

user := new(User)
if err := db.NewSelect().Model(user).Where("id = ?", 1).Scan(context.TODO()); err != nil {
	log.Fatalln(err)
}
fmt.Println(*user)

単一レコードの取得結果は以下です。

# User 構造体
{{} 1 斎藤ゆうき}

# クエリのログ
[bun]  17:47:00.624   SELECT                9.074ms  SELECT "u"."id", "u"."name" FROM "users" AS "u" WHERE (id = 1)

次に users テーブルの複数レコードを取得します。

users := make([]User, 0)
if err := db.NewSelect().Model(&users).OrderExpr("id ASC").Limit(3).Scan(context.TODO()); err != nil {
	log.Fatalln(err)
}
fmt.Println(users)

複数レコードの取得結果は以下です。

# User 構造体のスライス
[{{} 1 斎藤ゆうき} {{} 2 鈴木太郎} {{} 3 田中太郎}]

# クエリのログ
[bun]  17:50:12.932   SELECT                14.17ms  SELECT "u"."id", "u"."name" FROM "users" AS "u" ORDER BY id ASC LIMIT 3

削除(DELETE)

users テーブルのレコードを削除します。

user := &User{ID: 1}
if _, err := db.NewDelete().Model(user).WherePK().Exec(context.TODO()); err != nil {
	log.Fatalln(err)
}

実行結果は以下です。

[bun]  17:53:44.886   DELETE               11.865ms  DELETE FROM "users" AS "u" WHERE ("u"."id" = 1)

さいごに

Bun を初めて触ってみましたが、SQL ファーストを謳うだけあって直感的で使いやすかったです。
これから新規プロダクトの ORM には Bun を採用します。
公式ドキュメントも綺麗にまとめられているので、ぜひ見てみてください。

引用

Discussion