LoginSignup
1
0

チーム開発参加の記録【2023-06~2023-08】(2) sqlc + jackc/pgx/v5(v5.4.0)を使ってみた

Last updated at Posted at 2023-06-16

あるオンラインサロンでチーム開発に参加しています。
私はチーム03のバックエンド側メンバーに加わりました。
チーム03のバックエンドは、Go+Gin+sqlcを使うことになりました。
チーム開発に参加しながら、私の学習の軌跡を記事にしていきます。

本シリーズのリンク

※ 本記事のソースコードは主に学習・検証目的で書いたものであり、プロダクトにそのまま使用できる品質を目指していません。

本記事で行うこと

チーム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種類を試します。

ディレクトリ構成&ファイル一覧


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

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

db/query/query.sql

-- name: ListFoo :many
SELECT
    *
FROM
    foo;

実装(1):sql_package=database/sql

sqlcのsql_packageを「database/sql」(デフォルト)にしました。

実装(1)のsqlc.yaml

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
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
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
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
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
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のレスポンスは以下のようになりました。

image.png


[
  {
    "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つの設定が追加されていることです。

sqlc.yaml

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
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
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
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
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
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のレスポンスは以下のようになりました。

image.png


[
  {
    "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のレスポンスを設計したいと思います。

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0