本シリーズのリンク
- チーム開発参加の記録【2023-10~2024-03】(1) Go言語用ORM「Bun」をDBファーストで使う試み(SQLite使用)
- チーム開発参加の記録【2023-10~2024-03】(2) Go言語からTursoを使ってみた
- チーム開発参加の記録【2023-10~2024-03】(3) slogを使ってリクエスト・レスポンス情報をログ出力してみた
- チーム開発参加の記録【2023-10~2024-03】(4) Go言語用ORM「Bun」をDBファーストで使う試み(PostgreSQL使用)
- チーム開発参加の記録【2023-10~2024-03】(5) Go言語用ORM「Bun」でトランザクション、UPSERT、JOINを使ってみた
- チーム開発参加の記録【2023-10~2024-03】(6) Go言語用ORM「Bun」で複数のクエリーをまとめてDBサーバーで実行
- チーム開発参加の記録【2023-10~2024-03】(7) slogでErrorレベルのログをSlackに飛ばしてみた
本記事で行うこと
本シリーズの1番目の記事で、ORMのBunからSQLiteを使用しました。
そして2番目の記事で、BunからTursoを使ってみて、動くことを確認しました。
しかし、2番目の記事で使用したlibSQLのGo言語用クライアントライブラリは、記事執筆時点でまだwip(開発途中)だそうです。
チームメンバーから、wipのライブラリを使うのは不安という声が上がりましたので(当然ですね)、使用するデータベースをチームで再検討しました。
その結果、SQLiteとPostgreSQLのどちらにも対応できるように開発を行い、どっちを使うかは後日動かしてみて判断することにしました。
本記事では、1番目の記事でBunからSQLiteに試したのと同様の作業を、PostgreSQLにも試すことにします。
テーブルを作る
それでは作業に移ります。
1番目の記事と同様にDBファーストで行きますので、まずDDLを書きます。
DBファーストで行く理由は1番目の記事に書きましたので、そちらをお読みいただければと思います。
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しました。
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文を発行してみます
SELECT
*
FROM
foo;
SELECT文の結果です。
EchoとBunを使って、fooテーブルにCRUD操作するWeb APIサーバーを作ってみる
それでは、Go言語からBunを使っていきます。
最初に、今回書いたソースコード全体を載せてしまいます。
ディレクトリ構造とファイル一覧
Project Root
├── app.go
└── go.mod
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
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の違いは、後ほど説明します。
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を試したときと同様に、TO_JSONB()を使ってJSON型を経由すれば、Go言語の構造体にマッピングできることがわかりました。
複合型についてsqlcを試したとき、DBのスネークケースの列名から、JSONレスポンスのキャメルケースに変換するのが面倒でしたので、本記事の複合型type_compositeはその変換が必要ないように列名(=i, j)を定義してあります。
こういう面倒は、避けられるなら最初から避けたいですね。
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を、整形して見やすくしました。
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にして返しているだけですが、おおむねきれいなレスポンスが生成できています。
ただし、このレスポンスにはおかしな部分もありますので、個別にみていきます。
[
{
"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"
}
]
}
]
レスポンスの変な部分:タイムゾーンなしの日付・時間型の値にタイムゾーンが付いた
PostgreSQLのタイムゾーンなしの以下の型について、Go言語側でtime.Time型やbun.NullTime型にマッピングしたら、タイムゾーン付きになってしまった様子です。
- DATE
- TIME WITHOUT TIME ZONE
- TIMESTAMP WITHOUT TIME ZONE
上記の型は、Go言語側でstring型にマッピングした方が良さそうです。
レスポンスの変な部分:colArrayとcolArrayNullを見ると、NULLの配列要素を受け取れていない
最初にresultSetTypeのColArrayとColArrayNullを以下のように定義して、NULLの配列要素を受け取ろうとしましたが、
ColArray []*int64 `bun:",array" json:"colArray"`
ColArrayNull *[]*int64 `bun:",array" json:"colArrayNull"`
以下のレスポンスが返ってきました。
{
"error": "sql: Scan error on column index 34, name \"col_array\": strconv.ParseInt: parsing \"\": invalid syntax"
}
そこで、resultSetTypeを以下のように修正したのが現行のソースコードになります。
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を経由すれば受け取れるようです。
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の行について、配列型、複合型、複合型の配列の列をそれぞれ更新する内容です。
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を、整形して見やすくしました。
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を呼び出した結果を見てみます。
SELECT
col_array,
col_composite,
col_composite_array
FROM
foo
WHERE
col_serial = 1;
3つの列が意図した値に更新されました。
再びBunとsqlcを比較:Bunは複合型の配列を割と簡単に更新できる
本シリーズの1番目の記事でBunとsqlcを比較しましたが、本記事でも追加で比較します。
前回(6月~8月)のチーム開発中に、sqlcから複合型の配列の更新を試したときに、記事を2つ書きました。
- チーム開発参加の記録【2023-06~2023-08】(4) sqlc + jackc/pgx/v5 からPostgreSQLの複合型の配列を更新してみた
- チーム開発参加の記録【2023-06~2023-08】(6) PostgreSQLの複合型の配列の更新について、もう少し煮詰める
記事執筆時点で、sqlcは複合型をサポートしておらず、Goコードからでなくストアドプロシージャから複合型の配列を更新しました。
しかし本記事のGoコードで、Bunを使用したら複合型や複合型の配列を割と簡単に更新できました。
sqlcからBunに乗り換える理由が1つ増えたと思います。
※ 追記(2023-12-24)
以下にストアドプロシージャのメリットについて書きましたが、Bunでもネットワーク通信を減らす方法に気づきましたので、こちらの記事に書きました。
※ 追記終わり
ちなみに、sqlcのときに使ったストアドプロシージャにもメリットがあります。
- ストアドプロシージャを使わない書き方
トランザクション内で複数のSQLを記述したら、アプリからSQLが発行されるたびにアプリとDBサーバー間で通信が発生します。
- ストアドプロシージャを使うと
ストアドプロシージャはDBサーバー内で実行されるので、一度ストアドプロシージャを呼び出せば、まとめて通信レスで実行され、パフォーマンスが上がるでしょう。
しかしながら、開発者体験はストアドプロシージャよりもGo言語の方が圧倒的に良いので、パフォーマンスがクリティカルでない部分をGo言語で書ける選択肢があるのは、うれしいことだと思います。
INSERT文
次はINSERT文を見ていきます。
insertFooハンドラの中身
ハンドラのクエリー部分を見ます。
NULLを入れたい列にnilを設定しています。
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番目の記事と同様に変ですみません。
Bunが生成したSQL
上記コードからBunが生成した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されたことを確認できました。
DELETE文
次はDELETE文を見ていきます。
deleteFooハンドラの中身
ハンドラのクエリー部分を見ます。
col_serial列==最大値の行を削除する内容です。
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を、整形して見やすくしました。
DELETE FROM
"foo"
WHERE
(col_serial =
(SELECT
MAX(col_serial)
FROM
foo
)
)
deleteFoo APIの実行結果
deleteFoo APIを呼び出した結果です。
意図した行(3行目)が正常に削除されました。
まとめ
BunをDBファーストで使ったところ、PostgreSQLでも問題なく動いてくれました。
また、PostgreSQLの以下の型をBunでうまく扱えたので、sqlcから乗り換える理由が増えました。
- 配列型
- 複合型
- 複合型の配列
おまけ
本記事では、結果表のJSONの列をGo言語の構造体にマッピングしましたが、構造体に入れなくて良い場合は、json.RawMessage型にマッピングすれば良さげです。
https://bun.uptrace.dev/postgres/postgres-data-types.html#jsonb