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

# 本シリーズのリンク

- [チーム開発参加の記録【2023-10～2024-03】(1) Go言語用ORM「Bun」をDBファーストで使う試み（SQLite使用）](https://qiita.com/kanedaq/items/97ca682602b626d5d717)
- [チーム開発参加の記録【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使用）
- [チーム開発参加の記録【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)

# 本記事で行うこと

本シリーズの[1番目の記事](https://qiita.com/kanedaq/items/97ca682602b626d5d717)で、ORMの[Bun](https://bun.uptrace.dev/)からSQLiteを使用しました。
そして[2番目の記事](https://qiita.com/kanedaq/items/0c81946306d2412df555)で、Bunから[Turso](https://turso.tech/)を使ってみて、動くことを確認しました。
しかし、2番目の記事で使用した[libSQLのGo言語用クライアントライブラリ](https://github.com/tursodatabase/libsql/tree/main/bindings/go)は、記事執筆時点でまだ[wip（開発途中）](https://github.com/tursodatabase/libsql/blob/main/README-libsql.md)だそうです。
チームメンバーから、wipのライブラリを使うのは不安という声が上がりましたので（当然ですね）、使用するデータベースをチームで再検討しました。
その結果、SQLiteとPostgreSQLのどちらにも対応できるように開発を行い、どっちを使うかは後日動かしてみて判断することにしました。

本記事では、1番目の記事でBunからSQLiteに試したのと同様の作業を、PostgreSQLにも試すことにします。

# テーブルを作る

それでは作業に移ります。
1番目の記事と同様にDBファーストで行きますので、まずDDLを書きます。
DBファーストで行く理由は1番目の記事に書きましたので、そちらをお読みいただければと思います。

```sql
CREATE TYPE type_composite AS (
    i INTEGER,
    j TEXT
);

CREATE TABLE foo
(
    col_serial                 SERIAL                   NOT NULL,
    col_bigserial              BIGSERIAL                NOT NULL,
    col_boolean                BOOLEAN                  NOT NULL,
    col_boolean_null           BOOLEAN,
    col_smallint               SMALLINT                 NOT NULL,
    col_smallint_null          SMALLINT,
    col_int                    INTEGER                  NOT NULL,
    col_int_null               INTEGER,
    col_bigint                 BIGINT                   NOT NULL,
    col_bigint_null            BIGINT,
    col_numeric                NUMERIC                  NOT NULL,
    col_numeric_null           NUMERIC,
    col_real                   REAL                     NOT NULL,
    col_real_null              REAL,
    col_double                 DOUBLE PRECISION         NOT NULL,
    col_double_null            DOUBLE PRECISION,
    col_text                   TEXT                     NOT NULL,
    col_text_null              TEXT,
    col_char                   CHAR(1)                  NOT NULL,
    col_char_null              CHAR(1),
    col_date                   DATE                     NOT NULL,
    col_date_null              DATE,
    col_time_with              TIME WITH TIME ZONE      NOT NULL,
    col_time_with_null         TIME WITH TIME ZONE,
    col_time                   TIME                     NOT NULL,
    col_time_null              TIME,
    col_timestamp_with         TIMESTAMP WITH TIME ZONE NOT NULL,
    col_timestamp_with_null    TIMESTAMP WITH TIME ZONE,
    col_timestamp              TIMESTAMP                NOT NULL,
    col_timestamp_null         TIMESTAMP,
    col_uuid                   UUID                     NOT NULL,
    col_uuid_null              UUID,
    col_json                   JSONB                    NOT NULL,
    col_json_null              JSONB,
    col_array                  INTEGER[]                NOT NULL,
    col_array_null             INTEGER[],
    col_composite              type_composite           NOT NULL,
    col_composite_null         type_composite,
    col_composite_array        type_composite[]         NOT NULL,
    col_composite_array_null   type_composite[]
);
```

## 上記DDLの意図

列について、プロジェクトで使うかもしれない型を広めに試します。
JSON型、配列型、複合型（composite type）、複合型の配列も試します。
SQLiteよりも、だいぶ型の種類が多いですね。

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

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

```sql
INSERT INTO foo
(
    col_boolean,
    col_boolean_null,
    col_smallint,
    col_smallint_null,
    col_int,
    col_int_null,
    col_bigint,
    col_bigint_null,
    col_numeric,
    col_numeric_null,
    col_real,
    col_real_null,
    col_double,
    col_double_null,
    col_text,
    col_text_null,
    col_char,
    col_char_null,
    col_date,
    col_date_null,
    col_time_with,
    col_time_with_null,
    col_time,
    col_time_null,
    col_timestamp_with,
    col_timestamp_with_null,
    col_timestamp,
    col_timestamp_null,
    col_uuid,
    col_uuid_null,
    col_json,
    col_json_null,
    col_array,
    col_array_null,
    col_composite,
    col_composite_null,
    col_composite_array,
    col_composite_array_null
)
VALUES
(
    TRUE,
    NULL,
    12345,
    NULL,
    123456789,
    NULL,
    1234567890123456789,
    NULL,
    3.141592653589793238462643383279502884197169399375105820974944592307816406286208998628034825342117,
    NULL,
    3.141592653589793238462643383279502884197169399375105820974944592307816406286208998628034825342117,
    NULL,
    3.141592653589793238462643383279502884197169399375105820974944592307816406286208998628034825342117,
    NULL,
    'text_data',
    NULL,
    'c',
    NULL,
    '2023-01-01',
    NULL,
    '12:00:00',
    NULL,
    '12:00:00',
    NULL,
    '2023-12-31 23:59:59',
    NULL,
    '2023-12-31 23:59:59',
    NULL,
    GEN_RANDOM_UUID(),
    NULL,
    '{"a": 10, "b": "Json"}'::JSONB,
    NULL,
    ARRAY[10, NULL, 30]::INTEGER[],
    NULL,
    ROW(10, 'Composite')::type_composite,
    NULL,
    ARRAY[ROW(10, NULL), NULL, ROW(NULL, 'CompositeArray')]::type_composite[],
    NULL
),
(
    FALSE,
    FALSE,
    32767,
    32767,
    987654321,
    987654321,
    876543210987654321,
    876543210987654321,
    3.141592653589793238462643383279502884197169399375105820974944592307816406286208998628034825342117,
    3.141592653589793238462643383279502884197169399375105820974944592307816406286208998628034825342117,
    3.141592653589793238462643383279502884197169399375105820974944592307816406286208998628034825342117,
    3.141592653589793238462643383279502884197169399375105820974944592307816406286208998628034825342117,
    3.141592653589793238462643383279502884197169399375105820974944592307816406286208998628034825342117,
    3.141592653589793238462643383279502884197169399375105820974944592307816406286208998628034825342117,
    'text_data',
    'text_data',
    'c',
    'c',
    '2023-01-01',
    '2023-01-01',
    '12:00:00',
    '12:00:00',
    '12:00:00',
    '12:00:00',
    '2023-12-31 23:59:59',
    '2023-12-31 23:59:59',
    '2023-12-31 23:59:59',
    '2023-12-31 23:59:59',
    GEN_RANDOM_UUID(),
    GEN_RANDOM_UUID(),
    '{"a": 20, "b": null}'::JSONB,
    '{"a": null, "b": "Json"}'::JSONB,
    ARRAY[10, NULL, 30]::INTEGER[],
    ARRAY[10, NULL, 30]::INTEGER[],
    ROW(20, NULL)::type_composite,
    ROW(NULL, 'Composite')::type_composite,
    ARRAY[ROW(20, 'CompositeArray'), NULL]::type_composite[],
    ARRAY[NULL, ROW(30, 'CompositeArray')]::type_composite[]
);
```

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

```sql
SELECT
    *
FROM
    foo;
```

SELECT文の結果です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/327312/3ad4fbd5-978e-24de-09d3-dcaab0dd6d1d.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/327312/128f8ba1-7db9-0154-7e65-154bd0f66159.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/327312/7aa45e8f-4b8d-2bc1-c14b-40f9197fc8a3.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/327312/3dd84c1e-561b-d8a4-14a1-bc1b1aa3f3cd.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/327312/18c4c5b9-c593-30ef-c431-c25d99dedb74.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/327312/8b3bf8f9-ef3c-b8f7-6c26-3a0d7d7fe26c.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"
	"github.com/google/uuid"
	"net/http"
	"time"

	"github.com/labstack/echo/v4"
	"github.com/labstack/echo/v4/middleware"
	"github.com/uptrace/bun"
	"github.com/uptrace/bun/dialect/pgdialect"
	"github.com/uptrace/bun/driver/pgdriver"
	"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 *int64  `json:"a"`
			B *string `json:"b"`
		}

		compositeType struct {
			I *int64  `json:"i"`
			J *string `json:"j"`
		}

		resultSetType struct {
			ColSerial             int32             `json:"colSerial"`
			ColBigserial          int64             `json:"colBigserial"`
			ColBoolean            bool              `json:"colBoolean"`
			ColBooleanNull        *bool             `json:"colBooleanNull"`
			ColSmallint           int16             `json:"colSmallint"`
			ColSmallintNull       *int16            `json:"colSmallintNull"`
			ColInt                int32             `json:"colInt"`
			ColIntNull            *int32            `json:"colIntNull"`
			ColBigint             int64             `json:"colBigint"`
			ColBigintNull         *int64            `json:"colBigintNull"`
			ColNumeric            string            `json:"colNumeric"`
			ColNumericNull        *string           `json:"colNumericNull"`
			ColReal               float32           `json:"colReal"`
			ColRealNull           *float32          `json:"colRealNull"`
			ColDouble             float64           `json:"colDouble"`
			ColDoubleNull         *float64          `json:"colDoubleNull"`
			ColText               string            `json:"colText"`
			ColTextNull           *string           `json:"colTextNull"`
			ColChar               string            `json:"colChar"`
			ColCharNull           *string           `json:"colCharNull"`
			ColDate               time.Time         `json:"colDate"`
			ColDateNull           bun.NullTime      `json:"colDateNull"`
			ColTimeWith           time.Time         `json:"colTimeWith"`
			ColTimeWithNull       bun.NullTime      `json:"colTimeWithNull"`
			ColTime               time.Time         `json:"colTime"`
			ColTimeNull           bun.NullTime      `json:"colTimeNull"`
			ColTimestampWith      time.Time         `json:"colTimestampWith"`
			ColTimestampWithNull  bun.NullTime      `json:"colTimestampWithNull"`
			ColTimestamp          time.Time         `json:"colTimestamp"`
			ColTimestampNull      bun.NullTime      `json:"colTimestampNull"`
			ColUuid               uuid.UUID         `json:"colUuid"`
			ColUuidNull           *uuid.UUID        `json:"colUuidNull"`
			ColJson               jsonType          `json:"colJson"`
			ColJsonNull           *jsonType         `json:"colJsonNull"`
			ColArray              []int64           `bun:",array" json:"colArray"`
			ColArrayNull          *[]int64          `bun:",array" json:"colArrayNull"`
			ColArray2             []*int64          `json:"colArray2"`
			ColArrayNull2         *[]*int64         `json:"colArrayNull2"`
			ColComposite          compositeType     `json:"colComposite"`
			ColCompositeNull      *compositeType    `json:"colCompositeNull"`
			ColCompositeArray     []*compositeType  `json:"colCompositeArray"`
			ColCompositeArrayNull *[]*compositeType `json:"colCompositeArrayNull"`
		}
	)

	ctx := context.TODO()

	resultSetFoo := make([]resultSetType, 0)
	if err := db.NewSelect().
		Column("col_serial").
		Column("col_bigserial").
		Column("col_boolean").
		Column("col_boolean_null").
		Column("col_smallint").
		Column("col_smallint_null").
		Column("col_int").
		Column("col_int_null").
		Column("col_bigint").
		Column("col_bigint_null").
		Column("col_numeric").
		Column("col_numeric_null").
		Column("col_real").
		Column("col_real_null").
		Column("col_double").
		Column("col_double_null").
		Column("col_text").
		Column("col_text_null").
		Column("col_char").
		Column("col_char_null").
		Column("col_date").
		Column("col_date_null").
		Column("col_time_with").
		Column("col_time_with_null").
		Column("col_time").
		Column("col_time_null").
		Column("col_timestamp_with").
		Column("col_timestamp_with_null").
		Column("col_timestamp").
		Column("col_timestamp_null").
		Column("col_uuid").
		Column("col_uuid_null").
		Column("col_json").
		Column("col_json_null").
		Column("col_array").
		Column("col_array_null").
		ColumnExpr("TO_JSONB(col_array) AS col_array2").
		ColumnExpr("TO_JSONB(col_array_null) AS col_array_null2").
		ColumnExpr("TO_JSONB(col_composite) AS col_composite").
		ColumnExpr("TO_JSONB(col_composite_null) AS col_composite_null").
		ColumnExpr("TO_JSONB(col_composite_array) AS col_composite_array").
		ColumnExpr("TO_JSONB(col_composite_array_null) AS col_composite_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_array", "ARRAY[?, ?, ?]::INTEGER[]", 111, nil, 333).
		SetColumn("col_composite", "ROW(?, ?)::type_composite", 222, nil).
		SetColumn("col_composite_array", "ARRAY[?, ROW(?, ?)]::type_composite[]", nil, 111, "Hello").
		Where("col_serial = ?", 1).
		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_boolean", "?", false).
		Value("col_boolean_null", "?", nil).
		Value("col_smallint", "?", 9999).
		Value("col_smallint_null", "?", nil).
		Value("col_int", "?", 999999999).
		Value("col_int_null", "?", nil).
		Value("col_bigint", "?", 999999999999999999).
		Value("col_bigint_null", "?", nil).
		Value("col_numeric", "?",
			"2.718281828459045235360287471352662497757247093699959574966967627724076630353547594571382178").
		Value("col_numeric_null", "?", nil).
		Value("col_real", "?", 2.71828182845904523536028747135266).
		Value("col_real_null", "?", nil).
		Value("col_double", "?", 2.71828182845904523536028747135266).
		Value("col_double_null", "?", nil).
		Value("col_text", "?", "Hello").
		Value("col_text_null", "?", nil).
		Value("col_char", "?", "z").
		Value("col_char_null", "?", nil).
		Value("col_date", "?", "2023-12-22").
		Value("col_date_null", "?", nil).
		Value("col_time_with", "?", "13:00").
		Value("col_time_with_null", "?", nil).
		Value("col_time", "?", "13:00").
		Value("col_time_null", "?", nil).
		Value("col_timestamp_with", "?", "2023-12-22 13:00:00.000000").
		Value("col_timestamp_with_null", "?", nil).
		Value("col_timestamp", "?", "2023-12-22 13:00:00.000000").
		Value("col_timestamp_null", "?", nil).
		Value("col_uuid", "GEN_RANDOM_UUID()").
		Value("col_uuid_null", "?", nil).
		Value("col_json", "?", httpMessage{Message: "Hello"}).
		Value("col_json_null", "?", map[string]int{"key": 777}).
		Value("col_array", "ARRAY[?, ?, ?]::INTEGER[]", 111, nil, 333).
		Value("col_array_null", "?", nil).
		Value("col_composite", "ROW(?, ?)::type_composite", 222, nil).
		Value("col_composite_null", "?", nil).
		Value("col_composite_array", "ARRAY[?, ROW(?, ?)]::type_composite[]", nil, 111, "Hello").
		Value("col_composite_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_serial = (SELECT MAX(col_serial) FROM foo)").
		Exec(ctx); err != nil {

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

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

func main() {
	dsn := "postgres://postgres:secret@localhost:5432/postgres?sslmode=disable"
	sqldb := sql.OpenDB(pgdriver.NewConnector(pgdriver.WithDSN(dsn)))

	db = bun.NewDB(sqldb, pgdialect.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/google/uuid v1.5.0
	github.com/labstack/echo/v4 v4.11.4
	github.com/uptrace/bun v1.1.16
	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
)

require (
	github.com/fatih/color v1.15.0 // indirect
	github.com/golang-jwt/jwt v3.2.2+incompatible // indirect
	github.com/jinzhu/inflection v1.0.0 // indirect
	github.com/labstack/gommon v0.4.2 // indirect
	github.com/mattn/go-colorable v0.1.13 // indirect
	github.com/mattn/go-isatty v0.0.20 // indirect
	github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc // 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.17.0 // indirect
	golang.org/x/net v0.19.0 // indirect
	golang.org/x/sys v0.15.0 // indirect
	golang.org/x/text v0.14.0 // indirect
	golang.org/x/time v0.5.0 // indirect
	mellium.im/sasl v0.3.1 // indirect
)
```

# CRUD操作

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

## SELECT文

まずはSELECT文です。

### selectFooハンドラの中身

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

ソースコード中のColArrayとColArray2の違いは、後ほど説明します。

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

		compositeType struct {
			I *int64  `json:"i"`
			J *string `json:"j"`
		}

		resultSetType struct {
			ColSerial             int32             `json:"colSerial"`
			ColBigserial          int64             `json:"colBigserial"`
			ColBoolean            bool              `json:"colBoolean"`
			ColBooleanNull        *bool             `json:"colBooleanNull"`
			ColSmallint           int16             `json:"colSmallint"`
			ColSmallintNull       *int16            `json:"colSmallintNull"`
			ColInt                int32             `json:"colInt"`
			ColIntNull            *int32            `json:"colIntNull"`
			ColBigint             int64             `json:"colBigint"`
			ColBigintNull         *int64            `json:"colBigintNull"`
			ColNumeric            string            `json:"colNumeric"`
			ColNumericNull        *string           `json:"colNumericNull"`
			ColReal               float32           `json:"colReal"`
			ColRealNull           *float32          `json:"colRealNull"`
			ColDouble             float64           `json:"colDouble"`
			ColDoubleNull         *float64          `json:"colDoubleNull"`
			ColText               string            `json:"colText"`
			ColTextNull           *string           `json:"colTextNull"`
			ColChar               string            `json:"colChar"`
			ColCharNull           *string           `json:"colCharNull"`
			ColDate               time.Time         `json:"colDate"`
			ColDateNull           bun.NullTime      `json:"colDateNull"`
			ColTimeWith           time.Time         `json:"colTimeWith"`
			ColTimeWithNull       bun.NullTime      `json:"colTimeWithNull"`
			ColTime               time.Time         `json:"colTime"`
			ColTimeNull           bun.NullTime      `json:"colTimeNull"`
			ColTimestampWith      time.Time         `json:"colTimestampWith"`
			ColTimestampWithNull  bun.NullTime      `json:"colTimestampWithNull"`
			ColTimestamp          time.Time         `json:"colTimestamp"`
			ColTimestampNull      bun.NullTime      `json:"colTimestampNull"`
			ColUuid               uuid.UUID         `json:"colUuid"`
			ColUuidNull           *uuid.UUID        `json:"colUuidNull"`
			ColJson               jsonType          `json:"colJson"`
			ColJsonNull           *jsonType         `json:"colJsonNull"`
			ColArray              []int64           `bun:",array" json:"colArray"`
			ColArrayNull          *[]int64          `bun:",array" json:"colArrayNull"`
			ColArray2             []*int64          `json:"colArray2"`
			ColArrayNull2         *[]*int64         `json:"colArrayNull2"`
			ColComposite          compositeType     `json:"colComposite"`
			ColCompositeNull      *compositeType    `json:"colCompositeNull"`
			ColCompositeArray     []*compositeType  `json:"colCompositeArray"`
			ColCompositeArrayNull *[]*compositeType `json:"colCompositeArrayNull"`
		}
	)
```

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

続きのソースコードを見てみます。
複合型や複合型の配列については、試行錯誤の結果、[sqlcを試したときと同様](https://qiita.com/kanedaq/items/8b47443df2c42a15eaa9)に、TO_JSONB()を使ってJSON型を経由すれば、Go言語の構造体にマッピングできることがわかりました。
複合型についてsqlcを試したとき、[DBのスネークケースの列名から、JSONレスポンスのキャメルケースに変換するのが面倒](https://qiita.com/kanedaq/items/8b47443df2c42a15eaa9#%E5%AE%9F%E8%A3%853%E3%82%B9%E3%83%8D%E3%83%BC%E3%82%AF%E3%82%B1%E3%83%BC%E3%82%B9%E3%81%A0%E3%81%A3%E3%81%9Fjson%E3%81%AE%E3%82%AD%E3%83%BC%E3%82%92%E3%82%AD%E3%83%A3%E3%83%A1%E3%83%AB%E3%82%B1%E3%83%BC%E3%82%B9%E3%81%AB%E3%81%99%E3%82%8B)でしたので、本記事の複合型type_compositeはその変換が必要ないように列名（=i, j）を定義してあります。
こういう面倒は、避けられるなら最初から避けたいですね。

```go
	resultSetFoo := make([]resultSetType, 0)
	if err := db.NewSelect().
		Column("col_serial").
		Column("col_bigserial").
		Column("col_boolean").
		Column("col_boolean_null").
		Column("col_smallint").
		Column("col_smallint_null").
		Column("col_int").
		Column("col_int_null").
		Column("col_bigint").
		Column("col_bigint_null").
		Column("col_numeric").
		Column("col_numeric_null").
		Column("col_real").
		Column("col_real_null").
		Column("col_double").
		Column("col_double_null").
		Column("col_text").
		Column("col_text_null").
		Column("col_char").
		Column("col_char_null").
		Column("col_date").
		Column("col_date_null").
		Column("col_time_with").
		Column("col_time_with_null").
		Column("col_time").
		Column("col_time_null").
		Column("col_timestamp_with").
		Column("col_timestamp_with_null").
		Column("col_timestamp").
		Column("col_timestamp_null").
		Column("col_uuid").
		Column("col_uuid_null").
		Column("col_json").
		Column("col_json_null").
		Column("col_array").
		Column("col_array_null").
		ColumnExpr("TO_JSONB(col_array) AS col_array2").
		ColumnExpr("TO_JSONB(col_array_null) AS col_array_null2").
		ColumnExpr("TO_JSONB(col_composite) AS col_composite").
		ColumnExpr("TO_JSONB(col_composite_null) AS col_composite_null").
		ColumnExpr("TO_JSONB(col_composite_array) AS col_composite_array").
		ColumnExpr("TO_JSONB(col_composite_array_null) AS col_composite_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_serial",
    "col_bigserial",
    "col_boolean",
    "col_boolean_null",
    "col_smallint",
    "col_smallint_null",
    "col_int",
    "col_int_null",
    "col_bigint",
    "col_bigint_null",
    "col_numeric",
    "col_numeric_null",
    "col_real",
    "col_real_null",
    "col_double",
    "col_double_null",
    "col_text",
    "col_text_null",
    "col_char",
    "col_char_null",
    "col_date",
    "col_date_null",
    "col_time_with",
    "col_time_with_null",
    "col_time",
    "col_time_null",
    "col_timestamp_with",
    "col_timestamp_with_null",
    "col_timestamp",
    "col_timestamp_null",
    "col_uuid",
    "col_uuid_null",
    "col_json",
    "col_json_null",
    "col_array",
    "col_array_null",
    TO_JSONB(col_array) AS col_array2,
    TO_JSONB(col_array_null) AS col_array_null2,
    TO_JSONB(col_composite) AS col_composite,
    TO_JSONB(col_composite_null) AS col_composite_null,
    TO_JSONB(col_composite_array) AS col_composite_array,
    TO_JSONB(col_composite_array_null) AS col_composite_array_null
FROM
    "foo"
```

### selectFoo APIの実行結果

selectFoo APIを呼び出した結果です。
レスポンスは、ハンドラの最後の一行で結果表をそのままjsonにして返しているだけですが、おおむねきれいなレスポンスが生成できています。
ただし、このレスポンスにはおかしな部分もありますので、個別にみていきます。

```json
[
    {
        "colSerial": 1,
        "colBigserial": 1,
        "colBoolean": true,
        "colBooleanNull": null,
        "colSmallint": 12345,
        "colSmallintNull": null,
        "colInt": 123456789,
        "colIntNull": null,
        "colBigint": 1234567890123456789,
        "colBigintNull": null,
        "colNumeric": "3.141592653589793238462643383279502884197169399375105820974944592307816406286208998628034825342117",
        "colNumericNull": null,
        "colReal": 3.1415927,
        "colRealNull": null,
        "colDouble": 3.141592653589793,
        "colDoubleNull": null,
        "colText": "text_data",
        "colTextNull": null,
        "colChar": "c",
        "colCharNull": null,
        "colDate": "2023-01-01T00:00:00Z",
        "colDateNull": null,
        "colTimeWith": "0000-01-01T12:00:00Z",
        "colTimeWithNull": null,
        "colTime": "0000-01-01T12:00:00Z",
        "colTimeNull": null,
        "colTimestampWith": "2023-12-31T23:59:59Z",
        "colTimestampWithNull": null,
        "colTimestamp": "2023-12-31T23:59:59Z",
        "colTimestampNull": null,
        "colUuid": "40b4e894-e812-4fd6-b8c7-8beec353e078",
        "colUuidNull": null,
        "colJson": {
            "a": 10,
            "b": "Json"
        },
        "colJsonNull": null,
        "colArray": [
            10,
            0,
            30
        ],
        "colArrayNull": null,
        "colArray2": [
            10,
            null,
            30
        ],
        "colArrayNull2": null,
        "colComposite": {
            "i": 10,
            "j": "Composite"
        },
        "colCompositeNull": null,
        "colCompositeArray": [
            {
                "i": 10,
                "j": null
            },
            null,
            {
                "i": null,
                "j": "CompositeArray"
            }
        ],
        "colCompositeArrayNull": null
    },
    {
        "colSerial": 2,
        "colBigserial": 2,
        "colBoolean": false,
        "colBooleanNull": false,
        "colSmallint": 32767,
        "colSmallintNull": 32767,
        "colInt": 987654321,
        "colIntNull": 987654321,
        "colBigint": 876543210987654321,
        "colBigintNull": 876543210987654321,
        "colNumeric": "3.141592653589793238462643383279502884197169399375105820974944592307816406286208998628034825342117",
        "colNumericNull": "3.141592653589793238462643383279502884197169399375105820974944592307816406286208998628034825342117",
        "colReal": 3.1415927,
        "colRealNull": 3.1415927,
        "colDouble": 3.141592653589793,
        "colDoubleNull": 3.141592653589793,
        "colText": "text_data",
        "colTextNull": "text_data",
        "colChar": "c",
        "colCharNull": "c",
        "colDate": "2023-01-01T00:00:00Z",
        "colDateNull": "2023-01-01T00:00:00Z",
        "colTimeWith": "0000-01-01T12:00:00Z",
        "colTimeWithNull": "0000-01-01T12:00:00Z",
        "colTime": "0000-01-01T12:00:00Z",
        "colTimeNull": "0000-01-01T12:00:00Z",
        "colTimestampWith": "2023-12-31T23:59:59Z",
        "colTimestampWithNull": "2023-12-31T23:59:59Z",
        "colTimestamp": "2023-12-31T23:59:59Z",
        "colTimestampNull": "2023-12-31T23:59:59Z",
        "colUuid": "e35eae09-2654-44b3-be64-4a2a011f2dda",
        "colUuidNull": "636ce143-6b3c-4b71-8012-2adcc50d267a",
        "colJson": {
            "a": 20,
            "b": null
        },
        "colJsonNull": {
            "a": null,
            "b": "Json"
        },
        "colArray": [
            10,
            0,
            30
        ],
        "colArrayNull": [
            10,
            0,
            30
        ],
        "colArray2": [
            10,
            null,
            30
        ],
        "colArrayNull2": [
            10,
            null,
            30
        ],
        "colComposite": {
            "i": 20,
            "j": null
        },
        "colCompositeNull": {
            "i": null,
            "j": "Composite"
        },
        "colCompositeArray": [
            {
                "i": 20,
                "j": "CompositeArray"
            },
            null
        ],
        "colCompositeArrayNull": [
            null,
            {
                "i": 30,
                "j": "CompositeArray"
            }
        ]
    }
]
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/327312/0f446890-7d95-d8b0-20df-222327975a8c.png)

#### レスポンスの変な部分：タイムゾーンなしの日付・時間型の値にタイムゾーンが付いた

PostgreSQLのタイムゾーンなしの以下の型について、Go言語側でtime.Time型やbun.NullTime型にマッピングしたら、タイムゾーン付きになってしまった様子です。

- DATE
- TIME WITHOUT TIME ZONE
- TIMESTAMP WITHOUT TIME ZONE

上記の型は、Go言語側でstring型にマッピングした方が良さそうです。

#### レスポンスの変な部分：colArrayとcolArrayNullを見ると、NULLの配列要素を受け取れていない

最初にresultSetTypeのColArrayとColArrayNullを以下のように定義して、NULLの配列要素を受け取ろうとしましたが、

```go
			ColArray              []*int64          `bun:",array" json:"colArray"`
			ColArrayNull          *[]*int64         `bun:",array" json:"colArrayNull"`
```

以下のレスポンスが返ってきました。

```json
{
    "error": "sql: Scan error on column index 34, name \"col_array\": strconv.ParseInt: parsing \"\": invalid syntax"
}
```

そこで、resultSetTypeを以下のように修正したのが現行のソースコードになります。

```go
			ColArray              []int64           `bun:",array" json:"colArray"`
			ColArrayNull          *[]int64          `bun:",array" json:"colArrayNull"`
			ColArray2             []*int64          `json:"colArray2"`
			ColArrayNull2         *[]*int64         `json:"colArrayNull2"`
```

問い合わせ部分にも以下のようにColumnExprを追加しました。
動かしてレスポンスを見たら、colArray2とcolArrayNull2がNULLの配列要素を受け取れていることが確認できました。
TO_JSONB()を使って一度JSONを経由すれば受け取れるようです。

```go
		ColumnExpr("TO_JSONB(col_array) AS col_array2").
		ColumnExpr("TO_JSONB(col_array_null) AS col_array_null2").
```

配列にNULL要素が入らないケースではColArrayやColArrayNullのようにコーディングし、NULL要素が入るケースではColArray2やColArrayNull2のようにコーディングすれば良さそうです。

## UPDATE文

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

### updateFooハンドラの中身

ハンドラのクエリー部分を見ます。
col_serial列が1の行について、配列型、複合型、複合型の配列の列をそれぞれ更新する内容です。

```go
	if _, err := db.NewUpdate().
		Table("foo").
		SetColumn("col_array", "ARRAY[?, ?, ?]::INTEGER[]", 111, nil, 333).
		SetColumn("col_composite", "ROW(?, ?)::type_composite", 222, nil).
		SetColumn("col_composite_array", "ARRAY[?, ROW(?, ?)]::type_composite[]", nil, 111, "Hello").
		Where("col_serial = ?", 1).
		Exec(ctx); err != nil {

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

### Bunが生成したSQL

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

```sql
UPDATE "foo" SET
    col_array = ARRAY[111, NULL, 333]::INTEGER[],
    col_composite = ROW(222, NULL)::type_composite,
    col_composite_array = ARRAY[NULL, ROW(111, 'Hello')]::type_composite[]
WHERE
    (col_serial = 1)
```

### updateFoo APIの実行結果

updateFoo APIを呼び出した結果を見てみます。

```sql
SELECT
    col_array,
    col_composite,
    col_composite_array
FROM
    foo
WHERE
    col_serial = 1;
```

3つの列が意図した値に更新されました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/327312/191932ee-29a4-9eef-0c44-249349d0a7ac.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/327312/31a0c217-9be0-1d49-ef68-f6cecf4e0e4c.png)

### 再びBunとsqlcを比較：Bunは複合型の配列を割と簡単に更新できる

[本シリーズの1番目の記事](https://qiita.com/kanedaq/items/97ca682602b626d5d717#bun%E3%81%A8sqlc%E3%82%92%E6%AF%94%E8%BC%83bun%E3%81%AF%E5%8B%95%E7%9A%84sql%E3%82%92%E7%B5%84%E3%81%BF%E7%AB%8B%E3%81%A6%E3%82%84%E3%81%99%E3%81%9D%E3%81%86)でBunとsqlcを比較しましたが、本記事でも追加で比較します。

前回（6月～8月）のチーム開発中に、sqlcから複合型の配列の更新を試したときに、記事を2つ書きました。
- [チーム開発参加の記録【2023-06～2023-08】(4) sqlc + jackc/pgx/v5 からPostgreSQLの複合型の配列を更新してみた](https://qiita.com/kanedaq/items/43ff8c6146bbde2f2f0c)
- [チーム開発参加の記録【2023-06～2023-08】(6) PostgreSQLの複合型の配列の更新について、もう少し煮詰める](https://qiita.com/kanedaq/items/6c86d8dff79a5fa1dda3)

記事執筆時点で、sqlcは複合型をサポートしておらず、Goコードからでなくストアドプロシージャから複合型の配列を更新しました。
しかし本記事のGoコードで、Bunを使用したら複合型や複合型の配列を割と簡単に更新できました。
sqlcからBunに乗り換える理由が1つ増えたと思います。

<font color="Red">※ 追記（2023-12-24）</font>
以下にストアドプロシージャのメリットについて書きましたが、Bunでもネットワーク通信を減らす方法に気づきましたので、[こちらの記事](https://qiita.com/kanedaq/items/c446f937cee101e24c3e)に書きました。
<font color="Red">※ 追記終わり</font>

ちなみに、sqlcのときに使ったストアドプロシージャにもメリットがあります。

- ストアドプロシージャを使わない書き方

トランザクション内で複数のSQLを記述したら、アプリからSQLが発行されるたびにアプリとDBサーバー間で通信が発生します。

- ストアドプロシージャを使うと

ストアドプロシージャはDBサーバー内で実行されるので、一度ストアドプロシージャを呼び出せば、まとめて通信レスで実行され、パフォーマンスが上がるでしょう。

しかしながら、開発者体験はストアドプロシージャよりもGo言語の方が圧倒的に良いので、パフォーマンスがクリティカルでない部分をGo言語で書ける選択肢があるのは、うれしいことだと思います。

## INSERT文

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

### insertFooハンドラの中身

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

```go
	if _, err := db.NewInsert().
		Model((*dummyModel)(nil)).
		ModelTableExpr("foo").
		Value("col_boolean", "?", false).
		Value("col_boolean_null", "?", nil).
		Value("col_smallint", "?", 9999).
		Value("col_smallint_null", "?", nil).
		Value("col_int", "?", 999999999).
		Value("col_int_null", "?", nil).
		Value("col_bigint", "?", 999999999999999999).
		Value("col_bigint_null", "?", nil).
		Value("col_numeric", "?",
			"2.718281828459045235360287471352662497757247093699959574966967627724076630353547594571382178").
		Value("col_numeric_null", "?", nil).
		Value("col_real", "?", 2.71828182845904523536028747135266).
		Value("col_real_null", "?", nil).
		Value("col_double", "?", 2.71828182845904523536028747135266).
		Value("col_double_null", "?", nil).
		Value("col_text", "?", "Hello").
		Value("col_text_null", "?", nil).
		Value("col_char", "?", "z").
		Value("col_char_null", "?", nil).
		Value("col_date", "?", "2023-12-22").
		Value("col_date_null", "?", nil).
		Value("col_time_with", "?", "13:00").
		Value("col_time_with_null", "?", nil).
		Value("col_time", "?", "13:00").
		Value("col_time_null", "?", nil).
		Value("col_timestamp_with", "?", "2023-12-22 13:00:00.000000").
		Value("col_timestamp_with_null", "?", nil).
		Value("col_timestamp", "?", "2023-12-22 13:00:00.000000").
		Value("col_timestamp_null", "?", nil).
		Value("col_uuid", "GEN_RANDOM_UUID()").
		Value("col_uuid_null", "?", nil).
		Value("col_json", "?", httpMessage{Message: "Hello"}).
		Value("col_json_null", "?", map[string]int{"key": 777}).
		Value("col_array", "ARRAY[?, ?, ?]::INTEGER[]", 111, nil, 333).
		Value("col_array_null", "?", nil).
		Value("col_composite", "ROW(?, ?)::type_composite", 222, nil).
		Value("col_composite_null", "?", nil).
		Value("col_composite_array", "ARRAY[?, ROW(?, ?)]::type_composite[]", nil, 111, "Hello").
		Value("col_composite_array_null", "?", nil).
		Exec(ctx); err != nil {

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

Model周りの記述が、[1番目の記事](https://qiita.com/kanedaq/items/97ca682602b626d5d717#insertfoo%E3%83%8F%E3%83%B3%E3%83%89%E3%83%A9%E3%81%AE%E4%B8%AD%E8%BA%AB)と同様に変ですみません。

### Bunが生成したSQL

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

```sql
INSERT INTO foo (
    "col_boolean",
    "col_boolean_null",
    "col_smallint",
    "col_smallint_null",
    "col_int",
    "col_int_null",
    "col_bigint",
    "col_bigint_null",
    "col_numeric",
    "col_numeric_null",
    "col_real",
    "col_real_null",
    "col_double",
    "col_double_null",
    "col_text",
    "col_text_null",
    "col_char",
    "col_char_null",
    "col_date",
    "col_date_null",
    "col_time_with",
    "col_time_with_null",
    "col_time",
    "col_time_null",
    "col_timestamp_with",
    "col_timestamp_with_null",
    "col_timestamp",
    "col_timestamp_null",
    "col_uuid",
    "col_uuid_null",
    "col_json",
    "col_json_null",
    "col_array",
    "col_array_null",
    "col_composite",
    "col_composite_null",
    "col_composite_array",
    "col_composite_array_null"
)
VALUES (
    FALSE,
    NULL,
    9999,
    NULL,
    999999999,
    NULL,
    999999999999999999,
    NULL,
    '2.718281828459045235360287471352662497757247093699959574966967627724076630353547594571382178',
    NULL,
    2.718281828459045,
    NULL,
    2.718281828459045,
    NULL,
    'Hello',
    NULL,
    'z',
    NULL,
    '2023-12-22',
    NULL,
    '13:00',
    NULL,
    '13:00',
    NULL,
    '2023-12-22 13:00:00.000000',
    NULL,
    '2023-12-22 13:00:00.000000',
    NULL,
    GEN_RANDOM_UUID(),
    NULL,
    '{"message":"Hello"}',
    '{"key":777}',
    ARRAY[111, NULL, 333]::INTEGER[],
    NULL,
    ROW(222, NULL)::type_composite,
    NULL,
    ARRAY[NULL, ROW(111, 'Hello')]::type_composite[],
    NULL
)
```

### insertFoo APIの実行結果

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

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/327312/cd825979-2382-7a95-0b61-75acfed10b79.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/327312/69850fa7-7ad1-f081-5d51-390bbd8caf8d.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/327312/c9d68969-4645-07e2-7249-cb78c38195ed.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/327312/c939d057-e41b-080a-6540-17b63ceb9d4a.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/327312/d38de89c-95e1-e77e-957c-f809a54fb686.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/327312/664d6c53-5b90-9c14-0a8b-d7ff278beb14.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/327312/07a317b8-a3d0-88dd-b03c-ef768af5fa3a.png)

## DELETE文

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

### deleteFooハンドラの中身

ハンドラのクエリー部分を見ます。
col_serial列==最大値の行を削除する内容です。

```go
	if _, err := db.NewDelete().
		Table("foo").
		Where("col_serial = (SELECT MAX(col_serial) FROM foo)").
		Exec(ctx); err != nil {

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

### Bunが生成したSQL

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

```sql
DELETE FROM
    "foo"
WHERE
    (col_serial =
        (SELECT
            MAX(col_serial)
        FROM
            foo
        )
    )
```

### deleteFoo APIの実行結果

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

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/327312/7c22620c-5c42-0940-8848-b5f2db76609f.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/327312/0e0e61de-ab07-cb68-2343-409a6b4aa574.png)

# まとめ

BunをDBファーストで使ったところ、PostgreSQLでも問題なく動いてくれました。
また、PostgreSQLの以下の型をBunでうまく扱えたので、sqlcから乗り換える理由が増えました。

- 配列型
- 複合型
- 複合型の配列

# おまけ

本記事では、結果表のJSONの列をGo言語の構造体にマッピングしましたが、構造体に入れなくて良い場合は、json.RawMessage型にマッピングすれば良さげです。
https://bun.uptrace.dev/postgres/postgres-data-types.html#jsonb