【Go】ORM、Bun について
はじめに
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 はエクスポートされていないフィールドを無視します。
例えば User
の ID
を id
とすると、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