【Go/PostgreSQL】selectしてany型で受け取りたい!
DBからのデータの受け取り方
Go言語で database/sql
github.com/lib/pq
パッケージを使用してPostgreSQLからテーブルをselectするとき、通常はテーブルのスキーマに合わせた構造体型を定義しておくことになります。
しかし、ユーザから入力された任意のクエリを実行させたい場合など、予め取得するテーブルのスキーマが決まっていない場合は、構造体型の代わりに[]any型を使用することになります。
// DB接続
db, err := sql.Open("postgres", "host=localhost, port=5432 ...")
if err != nil {
return nil, err
}
defer db.Close()
// クエリ実行
cursor, err := db.Query("select ...")
if err != nil {
return nil, err
}
defer cursor.Close()
// 値を取得
rows := make([][]any, 0)
for cursor.Next() {
// 1レコード分のデータを []any 型で受け取る
row := make([]any, len(cols))
prow := make([]any, len(cols))
for i := range row {
prow[i] = &row[i]
}
err = cursor.Scan(prow...)
if err != nil {
return nil, err
}
rows = append(rows, row)
}
if err := cursor.Err(); err != nil {
return nil, err
}
PostgreSQLからGoへの型変換の行われ方
各カラムの値はany型の変数に格納されることになるのですが、データベースとGoで扱える型が異なるため、その対応関係を知っておく必要があります。
また、多くの場合、自分で扱いやすい型への変換処理を用意することになると思います。変換処理なんて用意しなくても、それなりにうまくGoの型が設定されるのでは?と思えるのですが、そうは問屋が卸しません。例えば、PostgreSQLのnumeric(10,4)
型はGoでは[]byte
型で受け取ることになります。数値型なのになぜスライス型になるのかというと、PostgreSQLの配列リテラルの表記法の文字列としてデータを受け取ることになるためです。具体的には、1.234
は []byte("1.2340")
という文字列をバイトコードで表現した値になります。データベースが扱う可変長のデータをGoで受け取るためには仕方ないことだと思いますが、このままでは扱いづらくて仕方がないため、やはり変換処理が必要になってきます。
PostgreSQLとGoの型の対応
PostgreSQLの各データ型がGoではどのような型に対応するのかを表にまとめました。
公式のデータ型一覧に加えて、範囲型と配列型を記載しています。
Postgresのデータ型 | DatabaseTypeName() | 変数の型 | 変数の値(例) |
---|---|---|---|
bigint (別名) int8 |
"INT8" | int64 |
1234 |
bigserial (別名) serial8 |
"INT8" | int64 |
1234 |
bit [(n)] | "BIT" |
[]byte ※文字列 |
"00110101" |
bit varying [(n)] (別名) varbit |
"VARBIT" |
[]byte ※文字列 |
"00110101" |
boolean (別名) bool |
"BOOL" | bool |
true |
box | "BOX" |
[]byte ※文字列 |
"(10,20),(0,0)" |
bytea | "BYTEA" | []byte |
{1, 2, 14, 15} |
character varying [(n)] (別名) varchar [(n)] |
"VARCHAR" | string |
"abcdefghij" |
character [(n)] (別名) char [(n)] |
"BPCHAR" |
[]byte ※文字列 |
"abcdefghij" |
cidr | "CIDR" |
[]byte ※文字列 |
"192.168.10.0/24" |
circle | "CIRCLE" |
[]byte ※文字列 |
"<(10,20),30>" |
date | "DATE" | time.Time |
2006-01-02 00:00:00 +0000 +0000 |
double precision (別名) floag8 |
"FLOAT8" | float64 |
1.234 |
inet | "INET" |
[]byte ※文字列 |
"192.168.10.1" |
integer (別名) int (別名) int4 |
"INT4" | int64 |
1234 |
interval [fields] [(p)] | "INTERVAL" |
[]byte ※文字列 |
"01:00:00" |
json | "JSON" |
[]byte ※文字列 |
"{\"value\": 10}" |
line | "LINE" |
[]byte ※文字列 |
"{10,20,30}" |
lseg | "LSEG" |
[]byte ※文字列 |
"[(10,20),(30,40)]" |
macaddr | "MACADDR" |
[]byte ※文字列 |
"aa:bb:cc:dd:ee:ff" |
money | "MONEY" |
[]byte ※文字列 |
"$1,234.00" |
numeric [(p, s)] (別名) decimal [(p, s)] |
"NUMERIC" |
[]byte ※文字列 |
"1.2340" |
path | "PATH" |
[]byte ※文字列 |
"[(10,20),(30,40),(50,60)]" |
point | "POINT" |
[]byte ※文字列 |
"(10,20)" |
polygon | "POLYGON" |
[]byte ※文字列 |
"((0,0),(10,20),(20,10))" |
real (別名) float4 |
"FLOAT4" | float64 |
1.234 ※ Inf やNaN も取得可能 |
smalint (別名) int2 |
"INT2" | int64 |
1234 |
smallserial (別名) serial2 |
"INT2" | int64 |
1234 |
serial (別名) serial4 |
"INT4" | int64 |
1234 |
text | "TEXT" | string |
"abcdefghij" |
time [(p)] [without time zone] | "TIME" | time.Time |
0000-01-01 15:04:05 +0000 UTC |
time [(p)] with time zone (別名) timetz |
"TIMETZ" | time.Time |
0000-01-01 15:04:05 +0700 +0700 |
timestamp [(p)] [without time zone] | "TIMESTAMP" | time.Time |
2006-01-02 15:04:05 +0000 UTC |
timestamp [(p)] with time zone (別名) timestamptz |
"TIMESTAMPTZ" | time.Time |
2006-01-02 15:04:05 +0700 +0700 |
tsquery | "TSQUERY" |
[]byte ※文字列 |
"'abc'" |
tsvector | "TSVECTOR" |
[]byte ※文字列 |
"'abc' 'def'" |
txid_snapshot | "TXID_SNAPSHOT" |
[]byte ※文字列 |
"10:20:10,14,15" |
uuid | "UUID" |
[]byte ※文字列 |
"a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11" |
xml | "XML" |
[]byte ※文字列 |
"<foo>bar</foo>" |
範囲型 (例: int4range) |
"INT4RANGE" |
[]byte ※文字列 |
"[10,20)" |
配列型 (例: integer[]) |
"_INT4" ※要素の型名の先頭に _ が付く |
[]byte ※文字列 |
"{10,20}" |
この表を見ると、前述のnumeric型の例だけではなく、予想以上に多くの型が[]byte
型の文字列表現になることが分かると思います。それだと元の型の判別が付かず適切なデータ変換ができませんので、PostgreSQLでどのようなデータ型であったかの情報が必要です。クエリの実行結果のカーソルオブジェクトに対して ColumnTypes()
を呼び出してカラムの型情報を持つスライスを取得し、更にそれぞれのカラムの型情報に対して DatabaseTypeName()
を呼び出すことで、PostgreSQL上での型名称を知ることができます。
次のコードは、取得したテーブルのデータを変換する部分です。
※ついでに、後で扱いやすいように []map[string]any
型に整形しています。
// (冒頭のサンプルコードのようにデータを取得した後)
...
// カラム情報を取得
cols, _ := cursor.Columns()
colTypes, _ := cursor.ColumnTypes()
// 取得したデータを出力用データに変換
output := make([]map[string]any, 0, len(rows))
for _, row := range rows {
record := map[string]any{}
for i, v := range row {
// 適切なデータに変換
record[cols[i]] = convert(v, colTypes[i].DatabaseTypeName())
}
output = append(output, record)
}
...
// anyの値を変換する関数
// ※ numeric型をfloat64に変換する部分のみのサンプル
func convert(v any, dbTypeName string) any {
switch vv := v.(type) {
case []byte:
t := string(vv)
switch dbTypeName {
case "NUMERIC":
f, err := strconv.ParseFloat(t, 64)
if err != nil {
return 0.0
}
return f
case "XXXX":
...
}
return t
case XXX:
...
default:
return vv
}
}
あとは、扱う必要があるデータ型に対応するように convert 関数の switch 部分を書き足していきましょう。
Discussion