GolangでPostgreSQL(その1)
はじめに
先日、Golangでsqlxを使ったデータベースアクセスの記事を書きました。
今回からは、各種データベースに接続してテストしてみようと思います。
まずは、前回の記事でも使用した 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 []byteAll other types are returned directly from the backend as []byte values in text format.
アプリケーション
テスト用のテーブルに対して入出力を行うアプリケーションを実装します。歯抜けの状態から徐々に埋めていけるように、Nullable 型で入出力します。
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)
	}
}
おわりに
それぞれのマニュアルや仕様に従って実装されていました。ただし、通貨単位やタイムゾーンについては、データベースの設定にも依存するようなので、出力の期待値は上記とは異なる場合があるかと思います。
日本語については、別途、テストしたいと思います。
Discussion