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