あるオンラインサロンでチーム開発に参加しています。
私はチーム03のバックエンド側メンバーに加わりました。
チーム03のバックエンドは、Go+Gin+sqlcを使うことになりました。
チーム開発に参加しながら、私の学習の軌跡を記事にしていきます。
本シリーズのリンク
- チーム開発参加の記録【2023-06~2023-08】(1) Go+Ginで画像をダウンロード/アップロードするAPIを作る
- チーム開発参加の記録【2023-06~2023-08】(2) sqlc + jackc/pgx/v5(v5.4.0)を使ってみた
- チーム開発参加の記録【2023-06~2023-08】(3) sqlc + jackc/pgx/v5(v5.4.1)からPostgreSQLの複合型の配列を使ってみた
- チーム開発参加の記録【2023-06~2023-08】(4) sqlc + jackc/pgx/v5 からPostgreSQLの複合型の配列を更新してみた
- チーム開発参加の記録【2023-06~2023-08】(5) gocronでスケジュール処理し、定期的にバッチジョブを起動してみた
- チーム開発参加の記録【2023-06~2023-08】(6) PostgreSQLの複合型の配列の更新について、もう少し煮詰める
※ 本記事のソースコードは主に学習・検証目的で書いたものであり、プロダクトにそのまま使用できる品質を目指していません。
本記事で行うこと
チーム03のバックエンド側では、APIを作りこむ前に、まずダミーデータを返すAPIとドキュメントを作って、フロントエンド側になるべく早くAPIを提供することになりました。
ただ、APIを設計する前に確認したいことがあり、先にsqlcを使ってみることにしました。
何を確認したかったかは、本記事の最後に書きます。
データベースの準備
データベースはPostgreSQLを使用します。
あらかじめ以下のSQLを実行して、fooテーブルをCREATEし、データを2件INSERTしました。
TIME型と、NOT NULL制約付きでないREAL型は、期待した結果が得られなかったのですが、チーム03ではこれらの型の使用予定がないため、本記事では検証対象外とします。
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_varchar VARCHAR NOT NULL,
col_varchar_null VARCHAR,
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
);
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_double,
col_double_null,
col_varchar,
col_varchar_null,
col_char,
col_char_null,
col_date,
col_date_null,
col_timestamp_with,
col_timestamp_with_null,
col_timestamp,
col_timestamp_null
)
VALUES
(
TRUE,
NULL,
12345,
NULL,
123456789,
NULL,
1234567890123456789,
NULL,
3.141592653589793238462643383279502884197169399375105820974944592307816406286208998628034825342117,
NULL,
3.141592653589793238462643383279502884197169399375105820974944592307816406286208998628034825342117,
3.141592653589793238462643383279502884197169399375105820974944592307816406286208998628034825342117,
NULL,
'varchar_data',
NULL,
'c',
NULL,
'2023-01-10',
NULL,
'2023-01-10 12:00:00',
NULL,
'2023-01-10 12:00:00',
NULL
),
(
FALSE,
FALSE,
32767,
32767,
987654321,
987654321,
876543210987654321,
876543210987654321,
3.141592653589793238462643383279502884197169399375105820974944592307816406286208998628034825342117,
3.141592653589793238462643383279502884197169399375105820974944592307816406286208998628034825342117,
3.141592653589793238462643383279502884197169399375105820974944592307816406286208998628034825342117,
3.141592653589793238462643383279502884197169399375105820974944592307816406286208998628034825342117,
3.141592653589793238462643383279502884197169399375105820974944592307816406286208998628034825342117,
'varchar_data',
'varchar_data',
'c',
'c',
'2023-01-10',
'2023-01-10',
'2023-01-10 12:00:00',
'2023-01-10 12:00:00',
'2023-01-10 12:00:00',
'2023-01-10 12:00:00'
);
2通りの実装
それでは、Go+Gin+sqlcを使用して、fooテーブルの全データを返すAPIを実装します。
sqlcの公式ドキュメントによれば、sqlcのsql_packageに指定できるのは以下の3種類です。
- pgx/v4
- pgx/v5
- database/sql(デフォルト)
本記事では、以下の2種類を試します。
- 実装(1):database/sql
- 実装(2):pgx/v5
- pgx/v5のバージョンは、2023-06-14にリリースされたv5.4.0です。
https://github.com/jackc/pgx/blob/master/CHANGELOG.md
- pgx/v5のバージョンは、2023-06-14にリリースされたv5.4.0です。
ディレクトリ構成&ファイル一覧
Project Root
├── db/
│ ├── query/
│ │ ├── query.sql
│ │ └── schema.sql
│ └── sqlc/
│ ├── db.go
│ ├── models.go
│ └── query.sql.go
├── app.go
├── go.mod
└── sqlc.yaml
db/sqlc/配下のファイルは、sqlcのコードジェネレーターによって生成されるファイルで、人が以下の3ファイルを用意して「sqlc generate」コマンドを実行することにより生成されます。
- db/query/schema.sql
- db/query/query.sql
- sqlc.yaml
実装(1)と実装(2)で共通するソースコードを準備
2つのSQLファイルが共通する内容となります。
共通コード:db/query/schema.sql
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_varchar VARCHAR NOT NULL,
col_varchar_null VARCHAR,
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
);
共通コード:db/query/query.sql
-- name: ListFoo :many
SELECT
*
FROM
foo;
実装(1):sql_package=database/sql
sqlcのsql_packageを「database/sql」(デフォルト)にしました。
実装(1)のsqlc.yaml
version: "2"
sql:
- engine: "postgresql"
queries: "./db/query/query.sql"
schema: "./db/query/schema.sql"
gen:
go:
out: "./db/sqlc"
package: "db"
実装(1)のdb/sqlc/*.go(ジェネレーターで生成)
ここまでで、以下の3ファイルが用意できていますので、
- db/query/schema.sql
- db/query/query.sql
- sqlc.yaml
Project Rootで以下を実行します。
sqlc generate
すると、db/sqlc/配下に以下の3つのgoファイルが生成されました。
- db/sqlc/db.go
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.18.0
package db
import (
"context"
"database/sql"
)
type DBTX interface {
ExecContext(context.Context, string, ...interface{}) (sql.Result, error)
PrepareContext(context.Context, string) (*sql.Stmt, error)
QueryContext(context.Context, string, ...interface{}) (*sql.Rows, error)
QueryRowContext(context.Context, string, ...interface{}) *sql.Row
}
func New(db DBTX) *Queries {
return &Queries{db: db}
}
type Queries struct {
db DBTX
}
func (q *Queries) WithTx(tx *sql.Tx) *Queries {
return &Queries{
db: tx,
}
}
- db/sqlc/models.go
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.18.0
package db
import (
"database/sql"
"time"
)
type Foo struct {
ColSerial int32
ColBigserial int64
ColBoolean bool
ColBooleanNull sql.NullBool
ColSmallint int16
ColSmallintNull sql.NullInt16
ColInt int32
ColIntNull sql.NullInt32
ColBigint int64
ColBigintNull sql.NullInt64
ColNumeric string
ColNumericNull sql.NullString
ColReal float32
ColDouble float64
ColDoubleNull sql.NullFloat64
ColVarchar string
ColVarcharNull sql.NullString
ColChar string
ColCharNull sql.NullString
ColDate time.Time
ColDateNull sql.NullTime
ColTimestampWith time.Time
ColTimestampWithNull sql.NullTime
ColTimestamp time.Time
ColTimestampNull sql.NullTime
}
- db/sqlc/query.sql.go
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.18.0
// source: query.sql
package db
import (
"context"
)
const listFoo = `-- name: ListFoo :many
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_double, col_double_null, col_varchar, col_varchar_null, col_char, col_char_null, col_date, col_date_null, col_timestamp_with, col_timestamp_with_null, col_timestamp, col_timestamp_null
FROM
foo
`
func (q *Queries) ListFoo(ctx context.Context) ([]Foo, error) {
rows, err := q.db.QueryContext(ctx, listFoo)
if err != nil {
return nil, err
}
defer rows.Close()
var items []Foo
for rows.Next() {
var i Foo
if err := rows.Scan(
&i.ColSerial,
&i.ColBigserial,
&i.ColBoolean,
&i.ColBooleanNull,
&i.ColSmallint,
&i.ColSmallintNull,
&i.ColInt,
&i.ColIntNull,
&i.ColBigint,
&i.ColBigintNull,
&i.ColNumeric,
&i.ColNumericNull,
&i.ColReal,
&i.ColDouble,
&i.ColDoubleNull,
&i.ColVarchar,
&i.ColVarcharNull,
&i.ColChar,
&i.ColCharNull,
&i.ColDate,
&i.ColDateNull,
&i.ColTimestampWith,
&i.ColTimestampWithNull,
&i.ColTimestamp,
&i.ColTimestampNull,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
実装(1)のapp.goとgo.mod
app.goでAPIを実装します。
- app.go
package main
import (
"context"
"database/sql"
db "exercise/db/sqlc"
"net/http"
"os"
"github.com/gin-gonic/gin"
_ "github.com/lib/pq"
)
type httpError struct {
Error string `json:"error"`
}
var database *sql.DB
func listFoo(c *gin.Context) {
q := db.New(database)
list, err := q.ListFoo(context.Background())
if err != nil {
c.JSON(http.StatusInternalServerError, httpError{Error: err.Error()})
return
}
c.JSON(http.StatusOK, list)
}
func main() {
connString := "user=postgres password=secret host=localhost port=5432 dbname=your_database sslmode=disable"
var err error
database, err = sql.Open("postgres", connString)
if err != nil {
print(err.Error())
os.Exit(1)
}
defer database.Close()
router := gin.Default()
router.GET("/foo", listFoo)
router.Run("0.0.0.0:8080")
}
- go.mod
module exercise
go 1.20
require (
github.com/gin-gonic/gin v1.9.1
github.com/lib/pq v1.10.9
)
require (
github.com/kr/pretty v0.3.0 // indirect
github.com/rogpeppe/go-internal v1.10.0 // indirect
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
)
require (
github.com/bytedance/sonic v1.9.1 // indirect
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect
github.com/gabriel-vasile/mimetype v1.4.2 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.14.0 // indirect
github.com/goccy/go-json v0.10.2 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/cpuid/v2 v2.2.4 // indirect
github.com/leodido/go-urn v1.2.4 // indirect
github.com/mattn/go-isatty v0.0.19 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/pelletier/go-toml/v2 v2.0.8 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.11 // indirect
golang.org/x/arch v0.3.0 // indirect
golang.org/x/crypto v0.10.0 // indirect
golang.org/x/net v0.10.0 // indirect
golang.org/x/sys v0.9.0 // indirect
golang.org/x/text v0.10.0 // indirect
google.golang.org/protobuf v1.30.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
実装(1)の実行結果
APIのレスポンスは以下のようになりました。
[
{
"ColSerial": 1,
"ColBigserial": 1,
"ColBoolean": true,
"ColBooleanNull": {
"Bool": false,
"Valid": false
},
"ColSmallint": 12345,
"ColSmallintNull": {
"Int16": 0,
"Valid": false
},
"ColInt": 123456789,
"ColIntNull": {
"Int32": 0,
"Valid": false
},
"ColBigint": 1234567890123456789,
"ColBigintNull": {
"Int64": 0,
"Valid": false
},
"ColNumeric": "3.141592653589793238462643383279502884197169399375105820974944592307816406286208998628034825342117",
"ColNumericNull": {
"String": "",
"Valid": false
},
"ColReal": 3.1415927,
"ColDouble": 3.141592653589793,
"ColDoubleNull": {
"Float64": 0,
"Valid": false
},
"ColVarchar": "varchar_data",
"ColVarcharNull": {
"String": "",
"Valid": false
},
"ColChar": "c",
"ColCharNull": {
"String": "",
"Valid": false
},
"ColDate": "2023-01-10T00:00:00Z",
"ColDateNull": {
"Time": "0001-01-01T00:00:00Z",
"Valid": false
},
"ColTimestampWith": "2023-01-10T12:00:00Z",
"ColTimestampWithNull": {
"Time": "0001-01-01T00:00:00Z",
"Valid": false
},
"ColTimestamp": "2023-01-10T12:00:00Z",
"ColTimestampNull": {
"Time": "0001-01-01T00:00:00Z",
"Valid": false
}
},
{
"ColSerial": 2,
"ColBigserial": 2,
"ColBoolean": false,
"ColBooleanNull": {
"Bool": false,
"Valid": true
},
"ColSmallint": 32767,
"ColSmallintNull": {
"Int16": 32767,
"Valid": true
},
"ColInt": 987654321,
"ColIntNull": {
"Int32": 987654321,
"Valid": true
},
"ColBigint": 876543210987654321,
"ColBigintNull": {
"Int64": 876543210987654321,
"Valid": true
},
"ColNumeric": "3.141592653589793238462643383279502884197169399375105820974944592307816406286208998628034825342117",
"ColNumericNull": {
"String": "3.141592653589793238462643383279502884197169399375105820974944592307816406286208998628034825342117",
"Valid": true
},
"ColReal": 3.1415927,
"ColDouble": 3.141592653589793,
"ColDoubleNull": {
"Float64": 3.141592653589793,
"Valid": true
},
"ColVarchar": "varchar_data",
"ColVarcharNull": {
"String": "varchar_data",
"Valid": true
},
"ColChar": "c",
"ColCharNull": {
"String": "c",
"Valid": true
},
"ColDate": "2023-01-10T00:00:00Z",
"ColDateNull": {
"Time": "2023-01-10T00:00:00Z",
"Valid": true
},
"ColTimestampWith": "2023-01-10T12:00:00Z",
"ColTimestampWithNull": {
"Time": "2023-01-10T12:00:00Z",
"Valid": true
},
"ColTimestamp": "2023-01-10T12:00:00Z",
"ColTimestampNull": {
"Time": "2023-01-10T12:00:00Z",
"Valid": true
}
}
]
このレスポンスのjsonには、気になる点が3つあります。
- NOT NULL制約付きでない型の値が不必要に複雑
- キーがパスカルケースになっているので、キャメルケースにしたい
- TIMEZONE付きでない日時型の値の末尾にもZが入っているのは何?
試行錯誤の結果、前者2つは実装(2)で改善できました。
実装(2):sql_package=pgx/v5
実装(1)のAPIのレスポンスについて、
- NOT NULL制約付きでない項目の値をシンプルな形にしたい
- この課題に対応するために、sqlcのsql_packageを「pgx/v5」にしました。
- キーをキャメルケースにしたい
- この課題に対応するために、sqlcのemit_json_tagsとjson_tags_case_styleを設定しました。
実装(2)のsqlc.yaml
実装(1)との違いは、sql_package、emit_json_tags、json_tags_case_styleの3つの設定が追加されていることです。
version: "2"
sql:
- engine: "postgresql"
queries: "./db/query/query.sql"
schema: "./db/query/schema.sql"
gen:
go:
out: "./db/sqlc"
package: "db"
sql_package: "pgx/v5"
emit_json_tags: true
json_tags_case_style: "camel"
実装(2)のdb/sqlc/*.go(ジェネレーターで生成)
ここまでで、以下の3ファイルが用意できていますので、
- db/query/schema.sql
- db/query/query.sql
- sqlc.yaml
Project Rootで以下を実行します。
sqlc generate
すると、db/sqlc/配下に以下の3つのgoファイルが生成されました。
- db/sqlc/db.go
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.18.0
package db
import (
"context"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgconn"
)
type DBTX interface {
Exec(context.Context, string, ...interface{}) (pgconn.CommandTag, error)
Query(context.Context, string, ...interface{}) (pgx.Rows, error)
QueryRow(context.Context, string, ...interface{}) pgx.Row
}
func New(db DBTX) *Queries {
return &Queries{db: db}
}
type Queries struct {
db DBTX
}
func (q *Queries) WithTx(tx pgx.Tx) *Queries {
return &Queries{
db: tx,
}
}
- db/sqlc/models.go
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.18.0
package db
import (
"github.com/jackc/pgx/v5/pgtype"
)
type Foo struct {
ColSerial int32 `json:"colSerial"`
ColBigserial int64 `json:"colBigserial"`
ColBoolean bool `json:"colBoolean"`
ColBooleanNull pgtype.Bool `json:"colBooleanNull"`
ColSmallint int16 `json:"colSmallint"`
ColSmallintNull pgtype.Int2 `json:"colSmallintNull"`
ColInt int32 `json:"colInt"`
ColIntNull pgtype.Int4 `json:"colIntNull"`
ColBigint int64 `json:"colBigint"`
ColBigintNull pgtype.Int8 `json:"colBigintNull"`
ColNumeric pgtype.Numeric `json:"colNumeric"`
ColNumericNull pgtype.Numeric `json:"colNumericNull"`
ColReal float32 `json:"colReal"`
ColDouble float64 `json:"colDouble"`
ColDoubleNull pgtype.Float8 `json:"colDoubleNull"`
ColVarchar string `json:"colVarchar"`
ColVarcharNull pgtype.Text `json:"colVarcharNull"`
ColChar string `json:"colChar"`
ColCharNull pgtype.Text `json:"colCharNull"`
ColDate pgtype.Date `json:"colDate"`
ColDateNull pgtype.Date `json:"colDateNull"`
ColTimestampWith pgtype.Timestamptz `json:"colTimestampWith"`
ColTimestampWithNull pgtype.Timestamptz `json:"colTimestampWithNull"`
ColTimestamp pgtype.Timestamp `json:"colTimestamp"`
ColTimestampNull pgtype.Timestamp `json:"colTimestampNull"`
}
- db/sqlc/query.sql.go
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.18.0
// source: query.sql
package db
import (
"context"
)
const listFoo = `-- name: ListFoo :many
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_double, col_double_null, col_varchar, col_varchar_null, col_char, col_char_null, col_date, col_date_null, col_timestamp_with, col_timestamp_with_null, col_timestamp, col_timestamp_null
FROM
foo
`
func (q *Queries) ListFoo(ctx context.Context) ([]Foo, error) {
rows, err := q.db.Query(ctx, listFoo)
if err != nil {
return nil, err
}
defer rows.Close()
var items []Foo
for rows.Next() {
var i Foo
if err := rows.Scan(
&i.ColSerial,
&i.ColBigserial,
&i.ColBoolean,
&i.ColBooleanNull,
&i.ColSmallint,
&i.ColSmallintNull,
&i.ColInt,
&i.ColIntNull,
&i.ColBigint,
&i.ColBigintNull,
&i.ColNumeric,
&i.ColNumericNull,
&i.ColReal,
&i.ColDouble,
&i.ColDoubleNull,
&i.ColVarchar,
&i.ColVarcharNull,
&i.ColChar,
&i.ColCharNull,
&i.ColDate,
&i.ColDateNull,
&i.ColTimestampWith,
&i.ColTimestampWithNull,
&i.ColTimestamp,
&i.ColTimestampNull,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
実装(2)のapp.goとgo.mod
app.goでAPIを実装します。
- app.go
package main
import (
"context"
db "exercise/db/sqlc"
"net/http"
"os"
"github.com/gin-gonic/gin"
"github.com/jackc/pgx/v5/pgxpool"
)
type httpError struct {
Error string `json:"error"`
}
var pool *pgxpool.Pool
func listFoo(c *gin.Context) {
q := db.New(pool)
list, err := q.ListFoo(context.Background())
if err != nil {
c.JSON(http.StatusInternalServerError, httpError{Error: err.Error()})
return
}
c.JSON(http.StatusOK, list)
}
func main() {
connString := "user=postgres password=secret host=localhost port=5432 dbname=your_database sslmode=disable"
var err error
pool, err = pgxpool.New(context.Background(), connString)
if err != nil {
print(err.Error())
os.Exit(1)
}
defer pool.Close()
router := gin.Default()
router.GET("/foo", listFoo)
router.Run("0.0.0.0:8080")
}
- go.mod
module exercise
go 1.20
require github.com/gin-gonic/gin v1.9.1
require (
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
github.com/jackc/puddle/v2 v2.2.0 // indirect
github.com/kr/text v0.2.0 // indirect
github.com/rogpeppe/go-internal v1.10.0 // indirect
golang.org/x/sync v0.1.0 // indirect
)
require (
github.com/bytedance/sonic v1.9.1 // indirect
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect
github.com/gabriel-vasile/mimetype v1.4.2 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.14.0 // indirect
github.com/goccy/go-json v0.10.2 // indirect
github.com/jackc/pgx/v5 v5.4.0
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/cpuid/v2 v2.2.4 // indirect
github.com/leodido/go-urn v1.2.4 // indirect
github.com/mattn/go-isatty v0.0.19 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/pelletier/go-toml/v2 v2.0.8 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.11 // indirect
golang.org/x/arch v0.3.0 // indirect
golang.org/x/crypto v0.10.0 // indirect
golang.org/x/net v0.10.0 // indirect
golang.org/x/sys v0.9.0 // indirect
golang.org/x/text v0.10.0 // indirect
google.golang.org/protobuf v1.30.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
実装(2)の実行結果
APIのレスポンスは以下のようになりました。
[
{
"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,
"colDouble": 3.141592653589793,
"colDoubleNull": null,
"colVarchar": "varchar_data",
"colVarcharNull": null,
"colChar": "c",
"colCharNull": null,
"colDate": "2023-01-10",
"colDateNull": null,
"colTimestampWith": "2023-01-10T21:00:00+09:00",
"colTimestampWithNull": null,
"colTimestamp": "2023-01-10T12:00:00Z",
"colTimestampNull": 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,
"colDouble": 3.141592653589793,
"colDoubleNull": 3.141592653589793,
"colVarchar": "varchar_data",
"colVarcharNull": "varchar_data",
"colChar": "c",
"colCharNull": "c",
"colDate": "2023-01-10",
"colDateNull": "2023-01-10",
"colTimestampWith": "2023-01-10T21:00:00+09:00",
"colTimestampWithNull": "2023-01-10T21:00:00+09:00",
"colTimestamp": "2023-01-10T12:00:00Z",
"colTimestampNull": "2023-01-10T12:00:00Z"
}
]
実装(1)のレスポンスからの改善点は、
- NOT NULL制約付きでない型の値がシンプルで扱いやすくなった
- jsonのキーがキャメルケースになった
また実装(1)との、その他の違いとして、
- NUMERIC型の値が文字列でなくなった
- API利用側にとって、文字列の方が扱いやすいのか扱いにくいのか?
- 今回のプロジェクトではNUMERIC型を使う予定がないので、気にしないことにします。
- DATE型の値の末尾にZが付かなくなった
- DATE型は改善されたわけですが、TIMESTAMP WITHOUT TIMEZONE型の値には、相変わらず末尾にZが付いてしまっているので、今後のために記憶にとどめておきます。
まとめ
APIを設計する前に、以下2点を確認したくて、先にsqlcを使ってみました。
- PostgreSQLのデータ型がGo言語側でどの型にマッピングされるか知りたかった
- クエリーの戻り値をそのままjsonに変換して、APIのレスポンスにしたらどうなるか確認したかった
型マッピングはsqlcのジェネレーターが生成したコードを見ればわかりますし、実装(2)のAPIレスポンスはjsonのキーがキャメルケースになり、値も扱いやすくなりました。
これらを前提にAPIのレスポンスを設計したいと思います。