💥

GoのORM "Bun" を使ってみた

2023/12/16に公開

GoのORMで、比較的新しいBunを使ってみたので、その使い方をまとめていこうと思います。

その他のORMは以下の記事が参考になります。
https://zenn.dev/ryoneko/articles/4c1267d7d0e0ca

Bunの特徴

Bunは簡単にまとめると「軽い、分かりやすい」が特徴です。

Bun の目標は、慣用的な SQL を記述できるようにすることであり、扱いにくい構文の背後に SQL を隠すことではありません。データベースの CLI (psql など) を使用してクエリの作成とテストを開始し、次に Bun のクエリ ビルダーを使用して結果のクエリを再構築することをお勧めします。

主な機能は次のとおりです。

  • 長いクエリを論理的に分離されたブロックに分割します。
  • プレースホルダーを適切にエスケープされた値に置き換えます (bun.Identbun.Safe)。
  • 構造体ベースのモデルから列のリストといくつかの結合を生成します。

https://bun.uptrace.dev/

サンプル

本記事のコードは全て以下のリポジトリにまとまっています。
MySQLのdocker-compose.yamlも置いてあるので、参考にしてみてください。
https://github.com/totsumaru/go-bun-sample

Bunのインストール

go get github.com/uptrace/bun@latest

DBへの接続

MySQLに接続するコードは以下の通りです。

main.go
	// DSNを作成
	dsn := fmt.Sprintf(
		"%s:%s@tcp(localhost:3306)/%s",
		os.Getenv("DB_USER"),
		os.Getenv("DB_PASS"),
		os.Getenv("DB_NAME"),
	)
	// DBに接続
	sqldb, err := sql.Open("mysql", dsn)
	if err != nil {
		panic(err)
	}

CREATE,INSERT,SELECT

それぞれのメソッドはこちらです。

user.go
package user

import (
	"context"

	"github.com/uptrace/bun"
)

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

	ID    int64  `bun:"id,pk,autoincrement"`
	Name  string `bun:"name,notnull"`
	email string // unexported fields are ignored
}

// Create: テーブルを作成します
//
// すでにテーブルが存在する場合はエラーを返します。
func Create(ctx context.Context, db *bun.DB) {
	_, err := db.NewCreateTable().Model((*User)(nil)).Exec(ctx)
	if err != nil {
		panic(err)
	}
}

// Insert: レコードを挿入します
func (u *User) Insert(ctx context.Context, db *bun.DB) {
	_, err := db.NewInsert().Model(u).Exec(ctx)
	if err != nil {
		panic(err)
	}
}

// Select: IDでレコードを取得します
func (u *User) FindByID(ctx context.Context, db *bun.DB) {
	if err := db.NewSelect().Model(u).Where("id = ?", u.ID).Scan(ctx); err != nil {
		panic(err)
	}
}

構造体のタグについて

タグをつけることによって、構造体でテーブルやカラムの設定を記述できます。
以下のページに一覧は載っていますが、いくつか抜粋しておきます(使用する際は公式ページを参照してください)
https://bun.uptrace.dev/guide/models.html#struct-tags

Tag Comment
bun.BaseModel bun:"table:table_name" デフォルトのテーブル名をオーバーライドします。
bun.BaseModel bun:"alias:table_alias" デフォルトのテーブル別名をオーバーライドします。
bun:",pk" 列を主キーとしてマークし、notnullオプションを適用します。複数の/複合主キーがサポートされています。
bun:",notnull" CreateTableNOT NULL 制約を追加するよう指示します。
bun:",nullzero" Go のゼロ値をSQLとしてマーシャルします。NULLまたはDEFAULT(サポートされている場合)。

実行してみる

main.go
package main

import (
	"context"
	"database/sql"
	"fmt"
	"go-bun-sample/user"
	"os"

	_ "github.com/go-sql-driver/mysql"
	"github.com/joho/godotenv"
	"github.com/uptrace/bun"
	"github.com/uptrace/bun/dialect/mysqldialect"
)

func init() {
	if err := godotenv.Load(); err != nil {
		panic(err)
	}
}

func main() {
	// DSNを作成
	dsn := fmt.Sprintf(
		"%s:%s@tcp(localhost:3306)/%s",
		os.Getenv("DB_USER"),
		os.Getenv("DB_PASS"),
		os.Getenv("DB_NAME"),
	)
	// DBに接続
	sqldb, err := sql.Open("mysql", dsn)
	if err != nil {
		panic(err)
	}

	// bunのDBインスタンスを作成
	db := bun.NewDB(sqldb, mysqldialect.New())

	// 検証用: 最初にテーブルを削除しておく
	db.NewDropTable().Model((*user.User)(nil)).Exec(context.Background())

	// usersテーブルを作成
	user.Create(context.Background(), db)

	u1 := &user.User{
		ID:   1,
		Name: "suzuki",
	}

	// レコードを作成
	u1.Insert(context.Background(), db)

	// レコードを取得
	u2 := &user.User{ID: 1}
	u2.FindByID(context.Background(), db)
	fmt.Println(u2.Name) // suzuki
}

DBの中を見てみると、期待通りの値が入っています。

mysql> SELECT * FROM users;
+----+--------+
| id | name   |
+----+--------+
|  1 | suzuki |
+----+--------+

githubのリポジトリは今後も変更していく可能性があるので、本記事の内容とずれる場合もあります。
ご承知下さい!

Discussion