🐘

GolangでPostgreSQL(その1)

2023/05/22に公開

はじめに

先日、Golangでsqlxを使ったデータベースアクセスの記事を書きました。

https://zenn.dev/robon/articles/ff2419b7f5a76c

今回からは、各種データベースに接続してテストしてみようと思います。
まずは、前回の記事でも使用した PostgreSQL から評価しました。

PostgreSQL

データベース

Amazon RDS 上に構築した PostgreSQL サーバーを使用します。

設定
エンジンバージョン 13.7
インスタンスクラス db.t3.micro
vCPU 2
RAM 1GB
ストレージ 20GiB

クライアント

Amazon EC2 の Amazon Linux 2 から接続します。

ツール

psql を使用しますので、Amazon Linux 2 の場合は、以下のようにインストールします。

$ sudo yum install postgresql

テスト用のテーブル

データ型の一覧

しばらくリレーショナルデータベースを触っていなかったのですが、何十年も前から標準化すると言いつつ、相変わらず方言がいっぱい😅です。マニュアル から抜粋します。

名称 別名 説明
bigint int8 8バイト符号付き整数
bigserial serial8 自動増分8バイト整数
bit [ (n) ] 固定長ビット列
bit varying [ (n) ] varbit 可変長ビット列
boolean bool 論理値(真/偽)
box 平面上の矩形
bytea バイナリデータ("バイトの配列(byte array)")
character varying [ (n) ] varchar [ (n) ] 可変長文字列
character [ (n) ] char [ (n) ] 固定長文字列
cidr IPv4もしくはIPv6ネットワークアドレス
circle 平面上の円
date 暦の日付(年月日)
double precision float8 倍精度浮動小数点(8バイト)
inet IPv4もしくはIPv6ホストアドレス
integer int, int4 4バイト符号付き整数
interval [ fields ] [ (p) ] 時間間隔
json JSONデータ
line 平面上の無限直線
lseg 平面上の線分
macaddr MAC(メディアアクセスコントロール)アドレス
money 貨幣金額
numeric [ (p, s) ] decimal [ (p, s) ] 精度の選択可能な高精度数値
path 平面上の幾何学的経路
point 平面上の幾何学的点
polygon 平面上の閉じた幾何学的経路
real float4 単精度浮動小数点(4バイト)
smallint int2 2バイト符号付き整数
smallserial serial2 自動増分2バイト整数
serial serial4 自動増分4バイト整数
text 可変長文字列
time [ (p) ] [ without time zone ] 時刻(時間帯なし)
time [ (p) ] with time zone timetz 時間帯付き時刻
timestamp [ (p) ] [ without time zone ] 日付と時刻(時間帯なし)
timestamp [ (p) ] with time zone timestamptz 時間帯付き日付と時刻
tsquery テキスト検索問い合わせ
tsvector テキスト検索文書
txid_snapshot ユーザレベルのトランザクションIDスナップショット
uuid 汎用一意識別子
xml XMLデータ

テスト用のテーブル

そのまま CREATE TABLE文にします。

CREATE TABLE datatypes (
    col01 bigint,
    col02 bigserial,
    col03 bit(1),
    col04 varbit(1),
    col05 boolean,
    col06 box,
    col07 bytea,
    col08 varchar(10),
    col09 character(10),
    col10 cidr,
    col11 circle,
    col12 date,
    col13 float8,
    col14 inet,
    col15 integer,
    col16 interval,
    col17 json,
    col18 line,
    col19 lseg,
    col20 macaddr,
    col21 money,
    col22 numeric(10,4),
    col23 path,
    col24 point,
    col25 polygon,
    col26 real,
    col27 smallint,
    col28 smallserial,
    col29 serial,
    col30 text,
    col31 time,
    col32 timetz,
    col33 timestamp,
    col34 timestamptz,
    col35 tsquery,
    col36 tsvector,
    col37 txid_snapshot,
    col38 uuid,
    col39 xml,
    CONSTRAINT pk_datatypes PRIMARY KEY(col01)
);

Golang

処理系

$ go version
go version go1.18.6 linux/amd64

ライブラリ

github.com/lib/pq の v1.10.9 を使用します。データ型については、以下ように記載されています。

Parameters pass through driver.DefaultParameterConverter before they are handled by this package. When the binary_parameters connection option is enabled, []byte values are sent directly to the backend as data in binary format.

This package returns the following types for values from the PostgreSQL backend:

・integer types smallint, integer, and bigint are returned as int64
・floating-point types real and double precision are returned as float64
・character types char, varchar, and text are returned as string
・temporal types date, time, timetz, timestamp, and timestamptz are returned as time.Time
・the boolean type is returned as bool
・the bytea type is returned as []byte

All other types are returned directly from the backend as []byte values in text format.

https://pkg.go.dev/github.com/lib/pq#hdr-Data_Types

アプリケーション

テスト用のテーブルに対して入出力を行うアプリケーションを実装します。歯抜けの状態から徐々に埋めていけるように、Nullable 型で入出力します。

main.go
package main

import (
	"database/sql"
	"log"
	"math"
	"os"
	"reflect"
	"time"

	"github.com/google/go-cmp/cmp"
	"github.com/jmoiron/sqlx"
	_ "github.com/lib/pq"
)

// Datatypes は、データ型テストテーブル
type Datatypes struct {
	Col01 sql.NullInt64   `db:"col01"` // bigint
	Col02 sql.NullString  `db:"col02"` // bigserial
	Col03 sql.NullString  `db:"col03"` // bit(1)
	Col04 sql.NullString  `db:"col04"` // varbit(1)
	Col05 sql.NullBool    `db:"col05"` // boolean
	Col06 sql.NullString  `db:"col06"` // box
	Col07 []byte          `db:"col07"` // bytea
	Col08 sql.NullString  `db:"col08"` // varchar(10)
	Col09 sql.NullString  `db:"col09"` // character(10)
	Col10 sql.NullString  `db:"col10"` // cidr
	Col11 sql.NullString  `db:"col11"` // circle
	Col12 sql.NullTime    `db:"col12"` // date
	Col13 sql.NullFloat64 `db:"col13"` // float8
	Col14 sql.NullString  `db:"col14"` // inet
	Col15 sql.NullInt64   `db:"col15"` // integer
	Col16 sql.NullString  `db:"col16"` // interval
	Col17 sql.NullString  `db:"col17"` // json
	Col18 sql.NullString  `db:"col18"` // line
	Col19 sql.NullString  `db:"col19"` // lseg
	Col20 sql.NullString  `db:"col20"` // macaddr
	Col21 sql.NullString  `db:"col21"` // money
	Col22 sql.NullString  `db:"col22"` // numeric(10,4)
	Col23 sql.NullString  `db:"col23"` // path
	Col24 sql.NullString  `db:"col24"` // point
	Col25 sql.NullString  `db:"col25"` // polygon
	Col26 sql.NullFloat64 `db:"col26"` // real
	Col27 sql.NullInt64   `db:"col27"` // smallint
	Col28 sql.NullString  `db:"col28"` // smallserial
	Col29 sql.NullString  `db:"col29"` // serial
	Col30 sql.NullString  `db:"col30"` // text
	Col31 sql.NullTime    `db:"col31"` // time
	Col32 sql.NullTime    `db:"col32"` // timetz
	Col33 sql.NullTime    `db:"col33"` // timestamp
	Col34 sql.NullTime    `db:"col34"` // timestamptz
	Col35 sql.NullString  `db:"col35"` // tsquery
	Col36 sql.NullString  `db:"col36"` // tsvector
	Col37 sql.NullString  `db:"col37"` // txid_snapshot
	Col38 sql.NullString  `db:"col38"` // uuid
	Col39 sql.NullString  `db:"col39"` // xml
}

// Key は、データ型テストテーブルのキー
type Key struct {
	Col01 int64 `db:"col01"`
}

func main() {
	dsn := os.Getenv("DSN")
	db, err := sqlx.Open("postgres", dsn)
	if err != nil {
		log.Printf("sql.Open error %s", err)
	}

	JST, _ := time.LoadLocation("Asia/Tokyo")
	key := Key{1}
	src := Datatypes{
		Col01: sql.NullInt64{Int64: 1, Valid: true},
		Col02: sql.NullString{String: "1", Valid: true}, // bigserial not null
		Col03: sql.NullString{String: "0", Valid: true},
		Col04: sql.NullString{String: "1", Valid: true},
		Col05: sql.NullBool{Bool: true, Valid: true},
		Col06: sql.NullString{String: "(1,1),(0,0)", Valid: true},
		Col07: []byte{1, 2, 3}, // select 結果は nil でなく empty
		Col08: sql.NullString{String: "varchar", Valid: true},
		Col09: sql.NullString{String: "character ", Valid: true}, // space padding
		Col10: sql.NullString{String: "192.168.1.0/24", Valid: true},
		Col11: sql.NullString{String: "<(0,0),1>", Valid: true},
		Col12: sql.NullTime{Time: time.Date(2001, 2, 3, 0, 0, 0, 0, time.UTC), Valid: true},
		Col13: sql.NullFloat64{Float64: 1.5, Valid: true},
		Col14: sql.NullString{String: "192.168.1.1", Valid: true},
		Col15: sql.NullInt64{Int64: math.MaxInt32, Valid: true},
		Col16: sql.NullString{String: "1 year", Valid: true},
		Col17: sql.NullString{String: `{"key":1}`, Valid: true},
		Col18: sql.NullString{String: "{1,2,3}", Valid: true},
		Col19: sql.NullString{String: "[(1,1),(0,0)]", Valid: true},
		Col20: sql.NullString{String: "08:00:2b:01:02:03", Valid: true},
		Col21: sql.NullString{String: "$123.45", Valid: true},
		Col22: sql.NullString{String: "123456.1234", Valid: true},
		Col23: sql.NullString{String: "[(1,1),(0,0)]", Valid: true},
		Col24: sql.NullString{String: "(1,2)", Valid: true},
		Col25: sql.NullString{String: "((1,2),(0,0))", Valid: true},
		Col26: sql.NullFloat64{Float64: 1.25, Valid: true},
		Col27: sql.NullInt64{Int64: math.MinInt16, Valid: true},
		Col28: sql.NullString{String: "2", Valid: true}, // smallserial not null
		Col29: sql.NullString{String: "3", Valid: true}, // serial not null
		Col30: sql.NullString{String: "text", Valid: true},
		Col31: sql.NullTime{Time: time.Date(0, 1, 1, 1, 2, 3, 4000000, time.UTC), Valid: true},
		Col32: sql.NullTime{Time: time.Date(0, 1, 1, 1, 2, 3, 4000000, JST), Valid: true},
		Col33: sql.NullTime{Time: time.Date(2001, 2, 3, 4, 5, 6, 7000000, time.UTC), Valid: true},
		Col34: sql.NullTime{Time: time.Date(2001, 2, 3, 4, 5, 6, 7000000, JST), Valid: true},
		Col35: sql.NullString{String: "'super':*", Valid: true},
		Col36: sql.NullString{String: "'Fat' 'Rats' 'The'", Valid: true},
		Col37: sql.NullString{String: "10:20:10,14,15", Valid: true},
		Col38: sql.NullString{String: "a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11", Valid: true},
		Col39: sql.NullString{String: "<xml /><root></root>", Valid: true},
	}

	_, err = db.NamedExec(`
		INSERT INTO datatypes (
			col01, col02, col03, col04, col05, col06, col07, col08, col09, col10,
			col11, col12, col13, col14, col15, col16, col17, col18, col19, col20,
			col21, col22, col23, col24, col25, col26, col27, col28, col29, col30,
			col31, col32, col33, col34, col35, col36, col37, col38, col39
		) VALUES (
			:col01, :col02, :col03, :col04, :col05, :col06, :col07, :col08, :col09, :col10,
			:col11, :col12, :col13, :col14, :col15, :col16, :col17, :col18, :col19, :col20,
			:col21, :col22, :col23, :col24, :col25, :col26, :col27, :col28, :col29, :col30,
			:col31, :col32, :col33, :col34, :col35, :col36, :col37, :col38, :col39
		)`,
		src,
	)
	if err != nil {
		log.Printf("db.Exec error %s", err)
	}

	dst := Datatypes{}
	query, args, err := db.BindNamed(`
		SELECT 
			col01, col02, col03, col04, col05, col06, col07, col08, col09, col10,
			col11, col12, col13, col14, col15, col16, col17, col18, col19, col20,
			col21, col22, col23, col24, col25, col26, col27, col28, col29, col30,
			col31, col32, col33, col34, col35, col36, col37, col38, col39
		FROM datatypes 
		WHERE col01 = :col01`,
		key,
	)
	if err != nil {
		log.Printf("db.BindNamed error %s", err)
	}
	err = db.QueryRowx(query,
		args...,
	).StructScan(
		&dst,
	)
	if err != nil {
		log.Printf("db.QueryRow error %s", err)
	}
	if !reflect.DeepEqual(src, dst) {
		// log.Printf("\nsrc = %#v\ndst = %#v\n", src, dst)
		diff := cmp.Diff(src, dst)
		if len(diff) > 0 {
			log.Print(diff)
		}
	}

	_, err = db.NamedExec(`
		DELETE FROM datatypes
		WHERE col01 = :col01`,
		key,
	)
	if err != nil {
		log.Printf("db.Exec error %s", err)
	}
}

おわりに

それぞれのマニュアルや仕様に従って実装されていました。ただし、通貨単位やタイムゾーンについては、データベースの設定にも依存するようなので、出力の期待値は上記とは異なる場合があるかと思います。

日本語については、別途、テストしたいと思います。

GitHubで編集を提案
株式会社ROBONの技術ブログ

Discussion