Golangの新しいORM「Bun」を触ってみた

# はじめに

本記事は [フューチャー Advent Calendar 2023](https://qiita.com/advent-calendar/2023/future) 24日目の記事です。(クリスマスイブですね。)
今回はGolangのORM「Bun」を触ってみたのでその、個人的に気になっていた点である、データの挿入周りついて記事にしたいと思います。

## Bunとは
BunはGo言語用のSQLファーストのORM（Object-Relational Mapping）ライブラリで、PostgreSQL、MySQL（MariaDBを含む）、MSSQL、SQLiteなどのデータベースシステムに対応しています​。
以下公式サイトです。

https://bun.uptrace.dev/

[Why Not ...?](https://bun.uptrace.dev/guide/#why-not) の章や公式サイトからも [GORM](https://gorm.io/docs/index.html) と比較していることが多いです。
「Bun」は「gorm」と比較してBunは、余計な機能などが少なく、ライブラリのサイズが小さいことや、コードベースでの記述が少なく、シンプルであるため処理が高速です。

## 環境
go 1.19
github.com/uptrace/bun/dialect/pgdialect v1.1.16
github.com/uptrace/bun/driver/pgdriver v1.1.16
github.com/uptrace/bun/extra/bundebug v1.1.16
psql (PostgreSQL) 14.10

## Bunのインストール

```
go get github.com/uptrace/bun@latest
```

## DB接続
bunはdatabase/sql　上で動作するので、まず最初に`sql.DB`を作成する必要があります。
今回はPostgresで実行します。また、データベース名はbunで作成しております。
```golang
// Open a PostgreSQL database.
sqldb, err := sql.Open("postgres", "user=postgres host=localhost dbname=bun sslmode=disable")
if err != nil {
    panic(err)
}
defer sqldb.Close()
// 使用するデータベースに合わせて、第二引数を変える。
db := bun.NewDB(sqldb, pgdialect.New())

// クエリーフックを追加
db.AddQueryHook(bundebug.NewQueryHook(
 bundebug.WithVerbose(true),
))
```
クエリーフックを追加することで、SQLを実行したクエリーが標準出力されるので追加することをおすすめします。

## テーブル作成
Textテーブルを作成したいと思います。カラムは`id`と`content`でpkは`id`です。まずはTextの構造体を作成します。
またbunではテーブルにエイリアスを指定することができます。今回は、`t`を指定します。
```golang
type Text struct {
	Id            int64  `bun:",pk,autoincrement"`
	Text          string `bun:"text"`
	bun.BaseModel `bun:"table:text,alias:t"`
}
```

実際テーブルを作成するコードは下記です。
データの中身は空で作成します。
```golang
res, err := db.NewCreateTable().Model((*Text)(nil)).Exec(context.TODO())
if err != nil {
    panic(err)
}
 ```
 クエリーフックを追加してあるため、実行すると下記のようなログが出力されます。
 ```
 [bun]  18:44:56.599   CREATE TABLE          12.65ms  CREATE TABLE "text" ("id" BIGSERIAL NOT NULL, "text" VARCHAR, PRIMARY KEY ("id"))
 ```

 ## データ挿入
 今回データのInsertは二つの方法で試しました。
 １つ目がバルクインサートで2つ目がyamlファイルを使ったインサートです。

 ### バルクインサート
Bunのバルクインサートは非常に簡単でスライスを使ってインサートすることができます。
`res`には挿入されたデータ件数が渡されます。
```golang
texts := []Text{{Text: "hoge"}, {Text: "fuga"}}
res, err := db.NewInsert().Model(&texts).Exec(context.TODO())
if err != nil {
    panic("バルクインサートに失敗しました")
}
fmt.Println(res)
```
実行結果
```
[bun]  19:14:09.499   INSERT               11.433ms  INSERT INTO "text" ("id", "text") VALUES (DEFAULT, 'hoge'), (DEFAULT, 'fuga') RETURNING "id"
2 //resの値
```

 
### YAMLファイルを使ったデータ挿入
自分が今回一番気になっていた点のYAMLファイルを使ったデータ挿入です。
公式ドキュメントに、`Provide initial data for your application with YAML fixtures.`とあるようにYAMLファイルを使って初期データ挿入ができることが「Bun」の特徴の一つとして紹介されています。
構造体とYAMLファイルを以下のように記述しました。

```golang
type User struct {
	ID        int64     `bun:"id,pk,autoincrement"`
	Name      string    `bun:"name"`
	Email     string    `bun:"email"`
	CreatedAt time.Time `bun:"created_at"`
}

type Org struct {
	Name    string `bun:"name"`
	OwnerID int64  `bun:"owner_id"`
}
```
UserとOrgテーブルになる構造体です。

```data.yaml
- model: User
  rows:
    - _id: smith
      name: John Smith
      email: john@smith.com
      created_at: '{{ now }}'
    - _id: doe
      name: Jonh Doe
      email: john@doe.com
      created_at: '{{ now }}'

- model: Org
  rows:
    - name: "{{ $.User.smith.Name }}'s Org"
      owner_id: '{{ $.User.smith.ID }}'
    - name: "{{ $.User.doe.Name }}'s Org"
      owner_id: '{{ $.User.doe.ID }}'
```
UserテーブルとOrgテーブルに挿入したいデータを記述しております。
テンプレート変数を使用することで簡単に挿入することができます。

実際にインサートする実装を書いていきます。

```golang
// モデルをデータベースに登録
db.RegisterModel((*User)(nil), (*Org)(nil))

// フィクスチャインスタンスの作成
fixture := dbfixture.New(db, dbfixture.WithRecreateTables())

// フィクスチャからデータをロード
err = fixture.Load(context.TODO(), os.DirFS("data"), "data.yaml")
if err != nil {
    panic(err)
}
 ```
カレントディレクトリ配下に`data`を作成し、data.yamlを配置します。
実際に実行してみます。


実行結果
```
[bun]  19:40:43.084   DROP TABLE            8.783ms  DROP TABLE IF EXISTS "users" CASCADE
[bun]  19:40:43.190   CREATE TABLE        106.285ms  CREATE TABLE "users" ("id" BIGSERIAL NOT NULL, "name" VARCHAR, "email" VARCHAR, "created_at" TIMESTAMPTZ, PRIMARY KEY ("id"))
[bun]  19:40:43.194   INSERT                2.752ms  INSERT INTO "users" ("id", "name", "email", "created_at") VALUES (DEFAULT, 'John Smith', 'john@smith.com', '2023-12-24 10:40:43.191087+00:00') RETURNING "id"
[bun]  19:40:43.195   INSERT                  924µs  INSERT INTO "users" ("id", "name", "email", "created_at") VALUES (DEFAULT, 'Jonh Doe', 'john@doe.com', '2023-12-24 10:40:43.19418+00:00') RETURNING "id"
[bun]  19:40:43.195   DROP TABLE              459µs  DROP TABLE IF EXISTS "orgs" CASCADE
[bun]  19:40:43.219   CREATE TABLE         23.824ms  CREATE TABLE "orgs" ("name" VARCHAR, "owner_id" BIGINT)
[bun]  19:40:43.240   INSERT               20.324ms  INSERT INTO "orgs" ("name", "owner_id") VALUES ('John Smith''s Org', 1)
[bun]  19:40:43.241   INSERT                  883µs  INSERT INTO "orgs" ("name", "owner_id") VALUES ('Jonh Doe''s Org', 2)
```
`WithRecreateTables`メソッドを使用したことで、最初にDROP TABLEされてからCREATE TABLEされてインサートされるようになっています。実際にテーブルの中身を見てみます。

- Userテーブル
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/684470/9ed40b90-37bf-456f-086c-53d9edb69af2.png)

- Orgテーブル
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/684470/7e39ca6a-e450-12f5-33a9-357c5e1bc70f.png)

データが挿入されていることが確認できました。
フィクスチャを使用すれば、開発やテスト中に一貫したデータセットを簡単に再利用できることで簡単にテストできるようになると感じました。



## 最後に
フィクスチャを使って一貫したテストデータセットを再利用できることでテスト用データをテスト毎に書かなく済むのではないかと思いました。
サイズが小さいのでgormのようなORMよりサイズが小さいのはORM選定の一つの判断基準になると思います。
余談ですが、「Bun」で検索するとJavaScriptの「Bun」関連のサイトがひっかかるので、「Bun Golang」や「Bun ORM」で検索するのがおすすめです。

明日は @wsysuper さんの記事です。