🐹

Golangでsqlxを使う

2023/05/08に公開

はじめに

Golangでリレーショナル・データベースにアクセスするなら、database/sqlパッケージのお世話になるのである。例えば、こんな感じで、Insert Select Delete ができる。

sql/main.go
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 というモジュールを使わせて頂くと、これらの残念なところが解消されるのである。しかも、標準ライブラリの純粋な拡張になっているのも素晴らしい。

https://github.com/jmoiron/sqlx

標準ライブラリの拡張とは

sqlx では、database/sqlDB Conn Tx Stmt を以下のように拡張している。

github.com/jmoiron/sqlx/blob/master/sqlx.go(抜粋)
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 からそれぞれに引き継がれる。また、unsafeMapperは、構造体タグをキャッシュする仕組みのために使用される。reflextx は、sqlx に含まれるパッケージで、これはこれで興味深いパッケージであるが、今回の記事では説明しない。

標準ライブラリの拡張であるから、置き換えること自体は、非常に簡単である。

sqlx1/main.go
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か所である。

diff sql/main.go sqlx1/main.go
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 では、NamedExecStructScan を使用して、以下のように書ける。

sqlx2/main.go
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文中のプレースホルダーである。

diff sqlx1/main.go sqlx2/main.go
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 を使用してオリジナルとほぼ同じ動きになるようにした。

sqlx3/main.go
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)
	}
}

違いは、以下のとおりである。

diff sqlx2/main.go sqlx3/main.go
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 自体も非常にコンパクトに書かれていて、中身を読むことで勉強になった。

今回のソースコードは、記事中のものがほぼ全てであるが、以下にもある。

https://github.com/take0a/go-sqlx-sample

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

Discussion