チーム開発参加の記録【2023-10～2024-03】(1) Go言語用ORM「Bun」をDBファーストで使う試み（SQLite使用）

数か月前まで、あるオンラインサロンでチーム開発に参加していました。
その優勝チームと準優勝チームが合体し、そこにサロン主も加わり、継続部隊を結成して新規開発を行うことになりました。

私は再びバックエンド側メンバーに入りました。
継続部隊のバックエンドは、Go+Echoを使用することになりました。
チーム開発に参加しながら、私の学習の軌跡を記事にしていきたいと思います。

# 本シリーズのリンク

- チーム開発参加の記録【2023-10～2024-03】(1) Go言語用ORM「Bun」をDBファーストで使う試み（SQLite使用）
- [チーム開発参加の記録【2023-10～2024-03】(2) Go言語からTursoを使ってみた](https://qiita.com/kanedaq/items/0c81946306d2412df555)
- [チーム開発参加の記録【2023-10～2024-03】(3) slogを使ってリクエスト・レスポンス情報をログ出力してみた](https://qiita.com/kanedaq/items/3914de42cb111c8817bc)
- [チーム開発参加の記録【2023-10～2024-03】(4) Go言語用ORM「Bun」をDBファーストで使う試み（PostgreSQL使用）](https://qiita.com/kanedaq/items/ec9bd42fefc755559df6)
- [チーム開発参加の記録【2023-10～2024-03】(5) Go言語用ORM「Bun」でトランザクション、UPSERT、JOINを使ってみた](https://qiita.com/kanedaq/items/0d3f5f44202548839d78)
- [チーム開発参加の記録【2023-10～2024-03】(6) Go言語用ORM「Bun」で複数のクエリーをまとめてDBサーバーで実行](https://qiita.com/kanedaq/items/c446f937cee101e24c3e)
- [チーム開発参加の記録【2023-10～2024-03】(7) slogでErrorレベルのログをSlackに飛ばしてみた](https://qiita.com/kanedaq/items/90584280dbc248e21ff2)

# 本記事で行うこと

[前回（6月～8月）のチーム開発](https://qiita.com/kanedaq/items/dcc6b3e50a0b2690033d)ではPostgreSQLを使用しましたが、今回はSQLiteが最有力候補となりました。
それに合わせて、DBクライアントライブラリも見直すことにしました。
候補に挙がったのは、SQLファーストを謳うORM「[Bun](https://bun.uptrace.dev/)」です。
前回は[sqlc](https://sqlc.dev/)を使用して、私は大変気に入りましたが、Bunは気に入るでしょうか？

## DBファーストで開発したい

前回使ったsqlcはDBファーストでした。
DBファーストって何？
大雑把にコードファーストと比較すると：

- コードファースト：アプリ側でORMのModelを記述し、アプリを動かしてテーブルを作成する
- DBファースト：ERモデリングツールにDDLを生成してもらうなり、人がDDLを書くなりして、それをDB側で実行する

今回もsqlc同様、DBファーストにしたいのです。
本記事は、ORM「Bun」をDBファーストで使う試みです。

## 何故DBファーストか

理由が2つあります。

- SQLiteのテーブルを[STRICTモード](https://www.sqlite.org/stricttables.html)で作成したいから

Bunを使って、試しにSQLiteのテーブルをコードファーストで作成したところ、STRICTでないテーブルが作成されました。
SQLiteの世界観を否定する気はありませんが、私はSTRICTテーブルを使いたいです。
厳格な型付けのテーブルを作成するために、自分でDDLを書いてSTRICTテーブルを作りたい、というのが理由の一つです。

- DBとアプリを疎結合にしたいから

コードファーストでDBを作ると、DBとアプリが相互に依存して密結合になります。
一方でDBファーストで作れば、DBはアプリのコードに依存しません。

DBはアプリよりずっと長く使われるので、相対的に短命なアプリに依存させたくありません。
選択したORMがオワコン化する可能性もありますので、DBを特定のORMに依存させたくもありません。
また、DBはシステムの基盤的存在で、様々なアプリから共有されることもありますので、一つのアプリに依存させず疎結合にしたい、というのがもう一つの理由となります。


# テーブルを作る

それでは作業に移ります。
DBファーストですので、まずDDLを書きます。

```sql
CREATE TABLE foo (
    col_text           TEXT    NOT NULL,
    col_text_null      TEXT,
    col_int            INTEGER NOT NULL,
    col_int_null       INTEGER,
    col_real           REAL    NOT NULL,
    col_real_null      REAL,
    col_timestamp      REAL    NOT NULL,
    col_timestamp_null REAL,
    col_json           TEXT    NOT NULL,
    col_json_null      TEXT,
    col_array          TEXT    NOT NULL,
    col_array_null     TEXT
) STRICT;
```

## 上記DDLの意図

STRICTテーブルで使える型は以下に限られています。

- INT
- INTEGER
- REAL
- TEXT
- BLOB
- ANY

このうち、プロジェクトで使う可能性が高い以下の型を試します。

- TEXT
- INTEGER
- REAL

加えて、日付・時刻も試します。
日付・時刻は、以下の型で保存できます。

- TEXT：ISO-8601形式
- REAL：ユリウス通日
- INTEGER：1970年からの秒数

本記事では日付・時刻をREAL型で保存することにしました。理由は、

- INTEGER型で持つと精度が秒になるが、REAL型やTEXT型は秒の小数の精度まで持てる
- TEXT型は保存フォーマットに気を遣う必要があって面倒だし、バイト数もREAL型より大きい

日付・時刻に加えて、jsonと配列の保存も試します。
これらはTEXT型で保存します。

以上が上記DDLの意図です。

# テスト用データをINSERTする

作成したfooテーブルに、以下のSQLでテスト用データを2件INSERTしました。

```sql
INSERT INTO foo
(
    col_text,
    col_text_null,
    col_int,
    col_int_null,
    col_real,
    col_real_null,
    col_timestamp,
    col_timestamp_null,
    col_json,
    col_json_null,
    col_array,
    col_array_null
)
VALUES
(
    'text_data',
    NULL,
    1234567890123456789,
    NULL,
    3.141592653589793238462643383279502884197169399375105820974944592307816406286208998628034825342117,
    NULL,
    JULIANDAY('now'),
    NULL,
    '{"a":100,"b":"bbb"}',
    NULL,
    '[10,null]',
    NULL
),
(
    'text_data',
    'text_data',
    1234567890123456789,
    1234567890123456789,
    3.141592653589793238462643383279502884197169399375105820974944592307816406286208998628034825342117,
    3.141592653589793238462643383279502884197169399375105820974944592307816406286208998628034825342117,
    JULIANDAY('now'),
    JULIANDAY('now'),
    '{"a":200,"b":null}',
    '{"a":300,"z":"zzz"}',
    '[30,null,50]',
    '[]'
);
```

INSERTしたら、fooテーブルにSELECT文を発行してみます。

```sql
SELECT
    col_text,
    col_text_null,
    col_int,
    col_int_null,
    col_real,
    col_real_null,
    STRFTIME('%Y-%m-%dT%H:%M:%fZ', col_timestamp) AS col_timestamp,
    STRFTIME('%Y-%m-%dT%H:%M:%fZ', col_timestamp_null) AS col_timestamp_null,
    col_json,
    col_json_null,
    col_array,
    col_array_null
FROM
    foo;
```

画像が小さいですが、SELECT文の結果です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/327312/3cc03212-028d-c0fa-d981-86c603549320.png)

# EchoとBunを使って、fooテーブルにCRUD操作するWeb APIサーバーを作ってみる

それでは、Go言語からBunを使っていきます。
最初に、今回書いたソースコード全体を載せてしまいます。

## ディレクトリ構造とファイル一覧

```

Project Root
  ├── app.go
  └── go.mod
```

## app.go

```app.go
package main

import (
	"context"
	"database/sql"
	"net/http"
	"time"

	"github.com/labstack/echo/v4"
	"github.com/labstack/echo/v4/middleware"
	"github.com/uptrace/bun"
	"github.com/uptrace/bun/dialect/sqlitedialect"
	"github.com/uptrace/bun/driver/sqliteshim"
	"github.com/uptrace/bun/extra/bundebug"
)

type (
	httpError struct {
		Error string `json:"error"`
	}

	httpMessage struct {
		Message string `json:"message"`
	}
)

var (
	db *bun.DB
)

func selectFoo(c echo.Context) error {
	type (
		jsonType struct {
			A *int    `json:"a"`
			B *string `json:"b"`
		}

		resultSetType struct {
			ColText          string       `json:"colText"`
			ColTextNull      *string      `json:"colTextNull"`
			ColInt           int64        `json:"colInt"`
			ColIntNull       *int64       `json:"colIntNull"`
			ColReal          float64      `json:"colReal"`
			ColRealNull      *float64     `json:"colRealNull"`
			ColTimestamp     time.Time    `json:"colTimestamp"`
			ColTimestampNull bun.NullTime `json:"colTimestampNull"`
			ColJson          jsonType     `json:"colJson"`
			ColJsonNull      *jsonType    `json:"colJsonNull"`
			ColArray         []*int64     `json:"colArray"`
			ColArrayNull     *[]*int64    `json:"colArrayNull"`
		}
	)

	ctx := context.TODO()

	resultSetFoo := make([]resultSetType, 0)
	if err := db.NewSelect().
		Column("col_text").
		Column("col_text_null").
		Column("col_int").
		Column("col_int_null").
		Column("col_real").
		Column("col_real_null").
		ColumnExpr("STRFTIME('%Y-%m-%dT%H:%M:%fZ', col_timestamp) AS col_timestamp").
		ColumnExpr("STRFTIME('%Y-%m-%dT%H:%M:%fZ', col_timestamp_null) AS col_timestamp_null").
		Column("col_json").
		Column("col_json_null").
		Column("col_array").
		Column("col_array_null").
		Table("foo").
		Scan(ctx, &resultSetFoo); err != nil {

		return c.JSON(http.StatusInternalServerError, httpError{Error: err.Error()})
	}

	return c.JSON(http.StatusOK, resultSetFoo)
}

func updateFoo(c echo.Context) error {
	ctx := context.TODO()

	if _, err := db.NewUpdate().
		Table("foo").
		SetColumn("col_text_null", "?", "ABC").
		Where("col_text_null IS NULL").
		Exec(ctx); err != nil {

		return c.JSON(http.StatusInternalServerError, httpError{Error: err.Error()})
	}

	return c.JSON(http.StatusOK, httpMessage{Message: "ok"})
}

func insertFoo(c echo.Context) error {
	type dummyModel struct{}

	ctx := context.TODO()

	if _, err := db.NewInsert().
		Model((*dummyModel)(nil)).
		ModelTableExpr("foo").
		Value("col_text", "?", "XYZ").
		Value("col_text_null", "?", nil).
		Value("col_int", "?", 123).
		Value("col_int_null", "?", nil).
		Value("col_real", "?", 3.14).
		Value("col_real_null", "?", nil).
		Value("col_timestamp", "JULIANDAY('now')").
		Value("col_timestamp_null", "?", nil).
		Value("col_json", "?", "{}").
		Value("col_json_null", "?", nil).
		Value("col_array", "?", "[]").
		Value("col_array_null", "?", nil).
		Exec(ctx); err != nil {

		return c.JSON(http.StatusInternalServerError, httpError{Error: err.Error()})
	}

	return c.JSON(http.StatusOK, httpMessage{Message: "ok"})
}

func deleteFoo(c echo.Context) error {
	ctx := context.TODO()

	if _, err := db.NewDelete().
		Table("foo").
		Where("col_text = ?", "XYZ").
		Exec(ctx); err != nil {

		return c.JSON(http.StatusInternalServerError, httpError{Error: err.Error()})
	}

	return c.JSON(http.StatusOK, httpMessage{Message: "ok"})
}

func main() {
	sqlite, err := sql.Open(sqliteshim.ShimName, "/home/user/DataGripProjects/sqlite/exercise.sqlite3")
	if err != nil {
		panic(err)
	}
	sqlite.SetMaxOpenConns(1)

	db = bun.NewDB(sqlite, sqlitedialect.New())
	db.AddQueryHook(bundebug.NewQueryHook(
		bundebug.WithVerbose(true),
		bundebug.FromEnv("BUNDEBUG"),
	))

	// EchoでAPIサーバー
	e := echo.New()
	e.Use(middleware.Logger())
	e.Use(middleware.Recover())

	// Routing
	api := e.Group("/api")
	api.GET("/selectFoo", selectFoo)
	api.PUT("/updateFoo", updateFoo)
	api.POST("/insertFoo", insertFoo)
	api.DELETE("/deleteFoo", deleteFoo)

	e.Logger.Fatal(e.Start(":1323"))
}
```

## go.mod

```go.mod
module exercise_bun

go 1.21

require (
	github.com/dustin/go-humanize v1.0.1 // indirect
	github.com/fatih/color v1.15.0 // indirect
	github.com/golang-jwt/jwt v3.2.2+incompatible // indirect
	github.com/google/uuid v1.3.1 // indirect
	github.com/jinzhu/inflection v1.0.0 // indirect
	github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
	github.com/labstack/echo/v4 v4.11.3 // indirect
	github.com/labstack/gommon v0.4.0 // indirect
	github.com/mattn/go-colorable v0.1.13 // indirect
	github.com/mattn/go-isatty v0.0.19 // indirect
	github.com/mattn/go-sqlite3 v1.14.17 // indirect
	github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
	github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc // indirect
	github.com/uptrace/bun v1.1.16 // indirect
	github.com/uptrace/bun/dialect/sqlitedialect v1.1.16 // indirect
	github.com/uptrace/bun/driver/sqliteshim v1.1.16 // indirect
	github.com/uptrace/bun/extra/bundebug v1.1.16 // indirect
	github.com/valyala/bytebufferpool v1.0.0 // indirect
	github.com/valyala/fasttemplate v1.2.2 // indirect
	github.com/vmihailenco/msgpack/v5 v5.3.5 // indirect
	github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
	golang.org/x/crypto v0.14.0 // indirect
	golang.org/x/mod v0.12.0 // indirect
	golang.org/x/net v0.17.0 // indirect
	golang.org/x/sys v0.13.0 // indirect
	golang.org/x/text v0.13.0 // indirect
	golang.org/x/time v0.3.0 // indirect
	golang.org/x/tools v0.13.0 // indirect
	lukechampine.com/uint128 v1.3.0 // indirect
	modernc.org/cc/v3 v3.41.0 // indirect
	modernc.org/ccgo/v3 v3.16.15 // indirect
	modernc.org/libc v1.24.1 // indirect
	modernc.org/mathutil v1.6.0 // indirect
	modernc.org/memory v1.7.1 // indirect
	modernc.org/opt v0.1.3 // indirect
	modernc.org/sqlite v1.25.0 // indirect
	modernc.org/strutil v1.2.0 // indirect
	modernc.org/token v1.1.0 // indirect
)
```

# CRUD操作

それでは、CRUD操作について一つ一つ見ていきます。

## SELECT文

まずはSELECT文です。

### selectFooハンドラの中身

ハンドラの中身を見ていきます。
ます、クエリーの結果表（リザルトセット）を格納する受け皿の型を定義しています。
NULLが入る可能性のある変数は、日付・時刻はbun.NullTime型で定義し、それ以外はポインタ型で定義します。

```go
	type (
		jsonType struct {
			A *int    `json:"a"`
			B *string `json:"b"`
		}

		resultSetType struct {
			ColText          string       `json:"colText"`
			ColTextNull      *string      `json:"colTextNull"`
			ColInt           int64        `json:"colInt"`
			ColIntNull       *int64       `json:"colIntNull"`
			ColReal          float64      `json:"colReal"`
			ColRealNull      *float64     `json:"colRealNull"`
			ColTimestamp     time.Time    `json:"colTimestamp"`
			ColTimestampNull bun.NullTime `json:"colTimestampNull"`
			ColJson          jsonType     `json:"colJson"`
			ColJsonNull      *jsonType    `json:"colJsonNull"`
			ColArray         []*int64     `json:"colArray"`
			ColArrayNull     *[]*int64    `json:"colArrayNull"`
		}
	)
```

この型をモデルとみなすかどうかですが、広義ではモデルだと思いますが、私はORMのModelとは別物と認識しています。

### 本記事のResultSetと、ORMのModelの違い

ORMのModelは、コードファーストで必要です。
RDBの実テーブルとアプリのModelが密結合する形になります。

一方、本記事のようなDBファーストでは、「SELECT文では」実テーブルと結合したModelは不要と私は考えています。
代わりに必要なのが、クエリーの結果表に相当するResultSetです。

ResultSetとModelの違いですが、ここでデータベーススペシャリスト試験でも出題される「ANSI/SPARC 3層スキーマ」を思い出しましょう。
3層スキーマは、データベースのスキーマを「外部スキーマ／概念スキーマ／内部スキーマ」の3階層で定義する方式ですね。
ResultSetは外部スキーマに相当し、Modelは概念スキーマに相当します。

3層スキーマでは、ユーザー（アプリも含む）は「外部スキーマを介してデータにアクセスせよ」と言っています。
3層スキーマに従うなら、SELECT文において、概念スキーマと結合したModelをアプリで使うのは、行儀が悪いというのが私の意見です。
従って、本記事のselectFooハンドラではModelは登場せず、代わりにResultSetが登場しています。
アプリケーションは本来は、SQLの結果表をモデルとして扱うべきだと思います。

### クエリー～レスポンスを返す部分

続きのソースコードを見てみます。
BunはSQLファーストを謳っているだけあって、SQL文が透けて見えますね。
本記事では単純なクエリーしか生成してもらっていませんが、Bunでは複雑なクエリーも生成してくれるそうです。
私みたいにSQLを書きたい人、N+1問題を気にしながらORMを使うなんて馬鹿らしいと思っている人、必要に応じてSQLチューニングをしたい人にとって、Bunは嬉しいORMだと思います。

```go
	resultSetFoo := make([]resultSetType, 0)
	if err := db.NewSelect().
		Column("col_text").
		Column("col_text_null").
		Column("col_int").
		Column("col_int_null").
		Column("col_real").
		Column("col_real_null").
		ColumnExpr("STRFTIME('%Y-%m-%dT%H:%M:%fZ', col_timestamp) AS col_timestamp").
		ColumnExpr("STRFTIME('%Y-%m-%dT%H:%M:%fZ', col_timestamp_null) AS col_timestamp_null").
		Column("col_json").
		Column("col_json_null").
		Column("col_array").
		Column("col_array_null").
		Table("foo").
		Scan(ctx, &resultSetFoo); err != nil {

		return c.JSON(http.StatusInternalServerError, httpError{Error: err.Error()})
	}

	return c.JSON(http.StatusOK, resultSetFoo)
```

### Bunが生成したSQL

上記コードからBunが生成したSQLを、整形して見やすくしました。

```sql
SELECT
	"col_text",
	"col_text_null",
	"col_int",
	"col_int_null",
	"col_real",
	"col_real_null",
	STRFTIME('%Y-%m-%dT%H:%M:%fZ', col_timestamp) AS col_timestamp,
	STRFTIME('%Y-%m-%dT%H:%M:%fZ', col_timestamp_null) AS col_timestamp_null,
	"col_json",
	"col_json_null",
	"col_array",
	"col_array_null"
FROM
	"foo"
```

### selectFoo APIの実行結果

selectFoo APIを呼び出した結果です。
レスポンスは、ハンドラの最後の一行で、結果表をそのままjsonにして返しているだけですが、きれいなレスポンスが生成できていますね。
シンプルなコードできれいなレスポンスを返せているのはポイントが高いです。

```json
[
    {
        "colText": "text_data",
        "colTextNull": null,
        "colInt": 1234567890123456789,
        "colIntNull": null,
        "colReal": 3.141592653589793,
        "colRealNull": null,
        "colTimestamp": "2023-12-13T10:41:18.524Z",
        "colTimestampNull": null,
        "colJson": {
            "a": 100,
            "b": "bbb"
        },
        "colJsonNull": null,
        "colArray": [
            10,
            null
        ],
        "colArrayNull": null
    },
    {
        "colText": "text_data",
        "colTextNull": "text_data",
        "colInt": 1234567890123456789,
        "colIntNull": 1234567890123456789,
        "colReal": 3.141592653589793,
        "colRealNull": 3.141592653589793,
        "colTimestamp": "2023-12-13T10:41:18.524Z",
        "colTimestampNull": "2023-12-13T10:41:18.524Z",
        "colJson": {
            "a": 200,
            "b": null
        },
        "colJsonNull": {
            "a": 300,
            "b": null
        },
        "colArray": [
            30,
            null,
            50
        ],
        "colArrayNull": []
    }
]
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/327312/de1ebb69-4d62-f03d-3bd5-f77d2b7bf083.png)

### SELECT文振り返り

ORMはコードファースト前提で開発されていることが多く、Bunも例外ではないと思います。
でもBunをDBファーストで試したら、問題なく動いてくれました。
jsonや配列を格納した列もGoの型にマッピングできて、とても良いと思います。

## UPDATE文

次はUPDATE文を見ていきます。

### updateFooハンドラの中身

ハンドラのクエリー部分を見ます。
col_text_null列がNULLの行について、同じ列を"ABC"に更新する内容です。

```go
	if _, err := db.NewUpdate().
		Table("foo").
		SetColumn("col_text_null", "?", "ABC").
		Where("col_text_null IS NULL").
		Exec(ctx); err != nil {

		return c.JSON(http.StatusInternalServerError, httpError{Error: err.Error()})
	}
```

### Bunが生成したSQL

上記コードからBunが生成したSQLを、整形して見やすくしました。

```sql
UPDATE "foo" SET
	col_text_null = 'ABC'
WHERE
	(col_text_null IS NULL)
```

### updateFoo APIの実行結果

updateFoo APIを呼び出した結果です。
col_text_nullがNULLだった行の意図した列が、意図した値に更新されました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/327312/4c755c39-d89f-b469-c563-b891acc62bda.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/327312/42720050-fc95-848b-004b-c49a623a81dc.png)

### Bunとsqlcを比較：Bunは動的SQLを組み立てやすそう

ここでBunとsqlcを比較してみます。
UPDATE文では、テーブルの一部の列を更新したいケース、それもどの列を更新するか実行時に決まるケースがしばしば出てきます。
その場合、事前にSQL文を用意しようとすると大量のSQLが必要になるため、そうせずに実行時に動的にSQL文を組み立てることが多いです。
sqlcは事前にSQL文を用意する方式であり、動的にSQL文を組み立てる機能はなさそうです。
その点Bunは、updateFooハンドラのコードで言えば、必要なSetColumn()を実行時に付け加えていけば良いので、簡単に動的SQL文を組み立てられそうです。
そんな訳で、私はsqlcよりもBunを使っていきたいと思いました。

## INSERT文

次はINSERT文を見ていきます。

### insertFooハンドラの中身

ハンドラのクエリー部分を見ます。
NULLを入れたい列にnilを設定しています。

```go
	type dummyModel struct{}

	ctx := context.TODO()

	if _, err := db.NewInsert().
		Model((*dummyModel)(nil)).
		ModelTableExpr("foo").
		Value("col_text", "?", "XYZ").
		Value("col_text_null", "?", nil).
		Value("col_int", "?", 123).
		Value("col_int_null", "?", nil).
		Value("col_real", "?", 3.14).
		Value("col_real_null", "?", nil).
		Value("col_timestamp", "JULIANDAY('now')").
		Value("col_timestamp_null", "?", nil).
		Value("col_json", "?", "{}").
		Value("col_json_null", "?", nil).
		Value("col_array", "?", "[]").
		Value("col_array_null", "?", nil).
		Exec(ctx); err != nil {

		return c.JSON(http.StatusInternalServerError, httpError{Error: err.Error()})
	}
```

このコード、Model周りの記述が変ですみません。
本当はUPDATE文の時みたいに「Table("foo")」と書きたかったのですが、Bunが「"bun: Model(nil)"」とエラーメッセージを吐いて受け付けてくれませんでした。
やり方を変えれば、変じゃないコードを書くことが可能でしょう。

### Bunが生成したSQL

上記コードからBunが生成したSQLを、整形して見やすくしました。

```sql
INSERT INTO foo (
	"col_text",
	"col_text_null",
	"col_int",
	"col_int_null",
	"col_real",
	"col_real_null",
	"col_timestamp",
	"col_timestamp_null",
	"col_json",
	"col_json_null",
	"col_array",
	"col_array_null"
)
VALUES (
	'XYZ',
	NULL,
	123,
	NULL,
	3.14,
	NULL,
	JULIANDAY('now'),
	NULL,
	'{}',
	NULL,
	'[]',
	NULL
)
```

### insertFoo APIの実行結果

insertFoo APIを呼び出した結果です。
3行目に、意図した行がINSERTされたことを確認できました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/327312/f6b149a4-07ce-e1ee-4f21-4e4bd50fc75b.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/327312/e28bc460-d42c-3cac-bdf9-653c94475d0d.png)

## DELETE文

次はDELETE文を見ていきます。

### deleteFooハンドラの中身

ハンドラのクエリー部分を見ます。
col_text列が"XYZ"の行を削除する内容です。

```go
	if _, err := db.NewDelete().
		Table("foo").
		Where("col_text = ?", "XYZ").
		Exec(ctx); err != nil {

		return c.JSON(http.StatusInternalServerError, httpError{Error: err.Error()})
	}
```

### Bunが生成したSQL

上記コードからBunが生成したSQLを、整形して見やすくしました。

```sql
DELETE FROM
	"foo"
WHERE
	(col_text = 'XYZ')
```

### deleteFoo APIの実行結果

deleteFoo APIを呼び出した結果です。
意図した行（3行目）が正常に削除されました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/327312/df6fc84d-2f7f-2c4e-b709-a6dfb5f4a710.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/327312/5b07e5ef-0856-852d-b482-75ae1e96e16b.png)

# まとめ

BunをDBファーストで使ってみたところ、問題なく動いてくれました。
BunはSQLファーストを謳っているだけあって、SQLを書きたい私でも使いたいと思わせてくれるORMでした。
記事中でBunとsqlcを比較しましたが、私はsqlcからBunに乗り換えたいと思いました。

# おまけ

Bunで[Relation](https://bun.uptrace.dev/guide/relations.html)という名のメソッドがありますが、これはRelationshipと命名してほしかったです。
リレーショナルモデルのリレーション（関係、集合）と、ERモデルのリレーションシップ（関連、関係性）は全く別物ですので。