Golangでsqlxを使う
はじめに
Golangでリレーショナル・データベースにアクセスするなら、database/sql
パッケージのお世話になるのである。例えば、こんな感じで、Insert
Select
Delete
ができる。
package main
import (
"database/sql"
"log"
"os"
_ "github.com/lib/pq"
)
// Customer は、顧客
type Customer struct {
CustomerID int
Name string
Address string
}
// CustomerKey は、顧客のキー
type CustomerKey struct {
CustomerID int
}
func main() {
dsn := os.Getenv("DSN")
db, err := sql.Open("postgres", dsn)
if err != nil {
log.Printf("sql.Open error %s", err)
}
key := CustomerKey{1}
src := Customer{key.CustomerID, "Shohei Otani", "Los Angeles Angels"}
_, err = db.Exec(`
INSERT INTO CUSTOMER (
CUSTOMER_ID, NAME, ADDRESS
) VALUES (
$1, $2, $3)`,
src.CustomerID,
src.Name,
src.Address,
)
if err != nil {
log.Printf("db.Exec error %s", err)
}
dst := Customer{}
err = db.QueryRow(`
SELECT
CUSTOMER_ID, NAME, ADDRESS
FROM CUSTOMER
WHERE CUSTOMER_ID = $1`,
key.CustomerID,
).Scan(
&dst.CustomerID,
&dst.Name,
&dst.Address,
)
if err != nil {
log.Printf("db.QueryRow error %s", err)
}
log.Printf("\nsrc = %#v\ndst = %#v\n", src, dst)
_, err = db.Exec(`
DELETE FROM CUSTOMER
WHERE CUSTOMER_ID = $1`,
key.CustomerID,
)
if err != nil {
log.Printf("db.Exec error %s", err)
}
}
これはこれで良いのだけれども、テーブルのカラムは、意味的にも概念的にもライフサイクル的にも共通する部分があるから同じテーブルに配置されているのであって、それを扱うプログラム側でもひと塊として扱いたいものである。上の場合は、Golangの構造体にしている。
これらのカラムの値をSQLを介して、データベースと入出力するのであるが、database/sql
の場合、上のように、Exec
の引数もScan
の引数も項目毎に指定する必要がある。こういう場合もあるのだろうから、こういう指定もできるようにすべきなのだろうが、構造体をマルっと指定できるようにもできないものだろうか。
また、今回は、Postgresを使ったので、SQL文のプレースホルダーに $1
$2
$3
などを使っているのであるが、これをMySQLだったら ?
にしてね。とか、Oracleだったら:col
にしてね。というのも、う~ん。という感じである。
sqlx
sqlx というモジュールを使わせて頂くと、これらの残念なところが解消されるのである。しかも、標準ライブラリの純粋な拡張になっているのも素晴らしい。
標準ライブラリの拡張とは
sqlx では、database/sql
の DB
Conn
Tx
Stmt
を以下のように拡張している。
type DB struct {
*sql.DB
driverName string
unsafe bool
Mapper *reflectx.Mapper
}
type Conn struct {
*sql.Conn
driverName string
unsafe bool
Mapper *reflectx.Mapper
}
type Tx struct {
*sql.Tx
driverName string
unsafe bool
Mapper *reflectx.Mapper
}
type Stmt struct {
*sql.Stmt
unsafe bool
Mapper *reflectx.Mapper
}
つまり、sqlx.DB
は、sql.DB
を埋め込んでいるため、上書きでメソッドを再定義していない限り、sql.DB
で定義されたメソッドが動作する。Conn
Tx
Stmt
も同じである。
他のフィールド driverName
は、プレースホルダーの方言に対応するために DB
からそれぞれに引き継がれる。また、unsafe
とMapper
は、構造体タグをキャッシュする仕組みのために使用される。reflextx
は、sqlx に含まれるパッケージで、これはこれで興味深いパッケージであるが、今回の記事では説明しない。
標準ライブラリの拡張であるから、置き換えること自体は、非常に簡単である。
package main
import (
"log"
"os"
"github.com/jmoiron/sqlx"
_ "github.com/lib/pq"
)
// Customer は、顧客
type Customer struct {
CustomerID int
Name string
Address string
}
// CustomerKey は、顧客のキー
type CustomerKey struct {
CustomerID int
}
func main() {
dsn := os.Getenv("DSN")
db, err := sqlx.Open("postgres", dsn)
if err != nil {
log.Printf("sql.Open error %s", err)
}
key := CustomerKey{1}
src := Customer{key.CustomerID, "Shohei Otani", "Los Angeles Angels"}
_, err = db.Exec(`
INSERT INTO CUSTOMER (
CUSTOMER_ID, NAME, ADDRESS
) VALUES (
$1, $2, $3)`,
src.CustomerID,
src.Name,
src.Address,
)
if err != nil {
log.Printf("db.Exec error %s", err)
}
dst := Customer{}
err = db.QueryRow(`
SELECT
CUSTOMER_ID, NAME, ADDRESS
FROM CUSTOMER
WHERE CUSTOMER_ID = $1`,
key.CustomerID,
).Scan(
&dst.CustomerID,
&dst.Name,
&dst.Address,
)
if err != nil {
log.Printf("db.QueryRow error %s", err)
}
log.Printf("\nsrc = %#v\ndst = %#v\n", src, dst)
_, err = db.Exec(`
DELETE FROM CUSTOMER
WHERE CUSTOMER_ID = $1`,
key.CustomerID,
)
if err != nil {
log.Printf("db.Exec error %s", err)
}
}
間違い探しの解答は、以下の2か所である。
4d3
< "database/sql"
7a7
> "github.com/jmoiron/sqlx"
25c25
< db, err := sql.Open("postgres", dsn)
---
> db, err := sqlx.Open("postgres", dsn)
このように sqlx を使用しても、標準ライブラリを使用して作成されたソースコードの書き換えは必須ではない。(型名や関数名などは sql
から sqlx
にパッケージ名の置換が必要ではある)
Exec と Scan
さて、最初に提起した課題は、以下の3点だった。
- Exec などで入力として構造体を指定したい。
- Scan などで出力として構造体を指定したい。
- プレースホルダーの方言を意識したくない。
sqlx では、NamedExec
と StructScan
を使用して、以下のように書ける。
package main
import (
"log"
"os"
"github.com/jmoiron/sqlx"
_ "github.com/lib/pq"
)
// Customer は、顧客
type Customer struct {
CustomerID int `db:"customer_id"`
Name string `db:"name"`
Address string `db:"address"`
}
// CustomerKey は、顧客のキー
type CustomerKey struct {
CustomerID int `db:"customer_id"`
}
func main() {
dsn := os.Getenv("DSN")
db, err := sqlx.Open("postgres", dsn)
if err != nil {
log.Printf("sql.Open error %s", err)
}
key := CustomerKey{1}
src := Customer{key.CustomerID, "Shohei Otani", "Los Angeles Angels"}
_, err = db.NamedExec(`
INSERT INTO CUSTOMER (
CUSTOMER_ID, NAME, ADDRESS
) VALUES (
:customer_id, :name, :address)`,
src,
)
if err != nil {
log.Printf("db.Exec error %s", err)
}
dst := Customer{}
err = db.QueryRowx(`
SELECT
CUSTOMER_ID, NAME, ADDRESS
FROM CUSTOMER
WHERE CUSTOMER_ID = $1`,
key.CustomerID,
).StructScan(
&dst,
)
if err != nil {
log.Printf("db.QueryRow error %s", err)
}
log.Printf("\nsrc = %#v\ndst = %#v\n", src, dst)
_, err = db.NamedExec(`
DELETE FROM CUSTOMER
WHERE CUSTOMER_ID = :customer_id`,
key,
)
if err != nil {
log.Printf("db.Exec error %s", err)
}
}
先程との違いは、NamedExec
StructScan
での構造体の使用に加えて、構造体に db
タグを使用している箇所とSQL文中のプレースホルダーである。
13,15c13,15
< CustomerID int
< Name string
< Address string
---
> CustomerID int `db:"customer_id"`
> Name string `db:"name"`
> Address string `db:"address"`
20c20
< CustomerID int
---
> CustomerID int `db:"customer_id"`
33c33
< _, err = db.Exec(`
---
> _, err = db.NamedExec(`
37,40c37,38
< $1, $2, $3)`,
< src.CustomerID,
< src.Name,
< src.Address,
---
> :customer_id, :name, :address)`,
> src,
47c45
< err = db.QueryRow(`
---
> err = db.QueryRowx(`
53,56c51,52
< ).Scan(
< &dst.CustomerID,
< &dst.Name,
< &dst.Address,
---
> ).StructScan(
> &dst,
63c59
< _, err = db.Exec(`
---
> _, err = db.NamedExec(`
65,66c61,62
< WHERE CUSTOMER_ID = $1`,
< key.CustomerID,
---
> WHERE CUSTOMER_ID = :customer_id`,
> key,
最後のピース
まぁ、これで良いかなと思ったのだが、QueryRowx
のプレースホルダーが残っている。NamedQuery
はあるのだが NamedQueryRow
はない…
今回は、BindNamed
を使用してオリジナルとほぼ同じ動きになるようにした。
package main
import (
"log"
"os"
"github.com/jmoiron/sqlx"
_ "github.com/lib/pq"
)
// Customer は、顧客
type Customer struct {
CustomerID int `db:"customer_id"`
Name string `db:"name"`
Address string `db:"address"`
}
// CustomerKey は、顧客のキー
type CustomerKey struct {
CustomerID int `db:"customer_id"`
}
func main() {
dsn := os.Getenv("DSN")
db, err := sqlx.Open("postgres", dsn)
if err != nil {
log.Printf("sql.Open error %s", err)
}
key := CustomerKey{1}
src := Customer{key.CustomerID, "Shohei Otani", "Los Angeles Angels"}
_, err = db.NamedExec(`
INSERT INTO CUSTOMER (
CUSTOMER_ID, NAME, ADDRESS
) VALUES (
:customer_id, :name, :address)`,
src,
)
if err != nil {
log.Printf("db.Exec error %s", err)
}
dst := Customer{}
query, args, err := db.BindNamed(`
SELECT
CUSTOMER_ID, NAME, ADDRESS
FROM CUSTOMER
WHERE CUSTOMER_ID = :customer_id`,
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)
}
log.Printf("\nsrc = %#v\ndst = %#v\n", src, dst)
_, err = db.NamedExec(`
DELETE FROM CUSTOMER
WHERE CUSTOMER_ID = :customer_id`,
key,
)
if err != nil {
log.Printf("db.Exec error %s", err)
}
}
違いは、以下のとおりである。
45c45
< err = db.QueryRowx(`
---
> query, args, err := db.BindNamed(`
49,50c49,56
< WHERE CUSTOMER_ID = $1`,
< key.CustomerID,
---
> WHERE CUSTOMER_ID = :customer_id`,
> key,
> )
> if err != nil {
> log.Printf("db.BindNamed error %s", err)
> }
> err = db.QueryRowx(query,
> args...,
おわりに
sqlx は、非常に簡潔に問題を解決してくれた。また、sqlx 自体も非常にコンパクトに書かれていて、中身を読むことで勉強になった。
今回のソースコードは、記事中のものがほぼ全てであるが、以下にもある。
Discussion