Go言語でDBアクセス(database/sql)

#はじめに
本記事はGo言語でのDBアクセス(select/insert/updateとトランザクション制御)に関するメモです。DBMSとしてpostgreSQL、ライブラリは標準のdatabase/sqlを使っています。

##database/sql 
[database/sql](https://golang.org/pkg/database/sql/)は、Go言語のデータベースアクセス用の標準パッケージです。
database/sqlでは、アプリケーションに対して、DBを操作するためのインターフェースを提供しています。
下図に示したとおり、DBへの実際の操作を行うのはdbDrvierで、アプリケーションからドライバを直接扱うことはしません。

![](http://yuml.me/diagram/class/[Application]->[database/sql]->[dbDriver]->[DataBase])

##database/sqlを使ったDB接続
database/sqlでDB接続するには、database/sqlとDBドライバをimportして、sql.Openを実行します。
sqlOpenの第1引数はドライバ名称で、今回はpostgreSQLを使うので「postgres」を設定します。第2引数は接続文字列です。こちらは使用するDBMSや環境に合わせて設定してださい。


```go:main.go
package main

import (
	"database/sql"
	"log"

	_ "github.com/lib/pq"
)

func setupDB(dbDriver string, dsn string) (*sql.DB, error) {
	db, err := sql.Open(dbDriver, dsn)
	if err != nil {
		return nil, err
	}
	return db, err
}

func main() {
	dbDriver := "postgres"
	dsn := "host=127.0.0.1 port=5432 user=user password=password dbname=dbname sslmode=disable"
	db, err := setupDB(dbDriver, dsn)
	defer db.Close()

	if err != nil {
		log.Fatal(err)
	}
}

```

##database/sqlでクエリー発行
クエリーを発行するメソッドにはQuery,QueryRow,Execなどがあります。概要は下表のとおりです。

|メソッド名|定義|用途|
|:---|:---|:--|
|Query|[func (db *DB) Query(query string, args ...interface{}) (*Rows, error)](https://golang.org/pkg/database/sql/#DB.Query)|複数行取得|
|QueryRow|[func (db *DB) QueryRow(query string, args ...interface{}) *Row](https://golang.org/pkg/database/sql/#DB.QueryRow)|単一行取得。複数行返却された場合はエラー|
|Exec|[func (db *DB) Exec(query string, args ...interface{}) (Result, error)](https://golang.org/pkg/database/sql/#DB.Exec)|insert/update|

他にもQueryContextのように～Contextと付いているメソッドがあります。こちらのメソッドはクエリーにcontextを設定でき、タイムアウトなんかを指定できるようです。

今回は、Query,QueryRow,Execを使って、以下のように定義したユーザ情報を格納するテーブルt_userを操作してみます。

```sql:t_user.ddl
CREATE SEQUENCE public.t_user_id_seq
    INCREMENT 1
    START 1
    MINVALUE 1
    MAXVALUE 9223372036854775807
    CACHE 1;

ALTER SEQUENCE public.t_user_id_seq
    OWNER TO postgres;

CREATE TABLE public.t_user
(
    id bigint NOT NULL DEFAULT nextval('t_user_id_seq'::regclass),
    name character varying(30) COLLATE pg_catalog."default" unique NOT NULL,
    profile character varying(600) COLLATE pg_catalog."default" ,
    created timestamp with time zone NOT NULL,
    updated timestamp with time zone NOT NULL,
    CONSTRAINT t_user_pkey PRIMARY KEY (id)
)

TABLESPACE pg_default;

ALTER TABLE public.t_user
    OWNER to postgres;
```

テーブルの各項目の役割は次のとおりです。

|項目名|物理名|用途|
|:--|:--|:--|
|ユーザID |id|ユーザを一意に特定するためのキー。insert時に自動で採番されるようにしています。|
|ユーザ名 |name|ユーザ名。他のユーザ名との重複を許可しません。|
|プロファイル |profile|ユーザが任意に設定できるユーザの情報。未設定(null)を許容しています。|
|登録日時 |created|レコードを登録した日時。|
|更新日時 |updated|レコードを更新した日時。|

次にクエリーの結果を格納する構造体Userを以下のように定義します。構造体の定義は、テーブルかselectした結果を設定するのに都合の良いように定義にしておきます。
今回はテーブルt_userに合わせて構造体を定義しています。

```go
type User struct {
	ID      int64
	Name    string
	profile sql.NullString
	Created time.Time
	Updated time.Time
}
```
profileフィールドの型がstringではなく、sql.NullStringを指定しているのは、profileにnullが設定される可能性があるためです。
database/sqlでテーブルをselectして結果を取得するにはQuery系のメソッドで得た結果をScanしますが、profileをstringで定義していた場合、nullが入っているとScan時にエラーとなります。ここではprofileの型にsql.NullStringを指定することで、Scan時に発生するエラーを回避しています。

### database/sql Query

テーブルから最大10行取得するサンプルです。QueryメソッドのパラメータにSQLを直接記述します。

```go
rows, err := db.Query("select * from t_user limit 10")
defer rows.Close()

if err != nil {
	log.Fatal(err)
}
for rows.Next() {
	u := &User{}
	if err := rows.Scan(&u.ID, &u.Name, &u.Profile, &u.Created, &u.Updated); err != nil {
		log.Fatal(err)
	}
	fmt.Println(u)
}
```

以下のように$1をつけることでプレースホルダーを指定することもできます。

```go
rows, err := db.Query("select * from t_comments where id=$1", 1)
```

上記の場合は、id(PrimaryKey)を指定して検索しているので、取得結果が1行になります。したがってQueryRowを使って書き換えることもできます。

```go
func selectUser(db *sql.DB, id int64) (*User, error) {
	u := &User{}
	if err := db.QueryRow("select * from t_user where id=$1", id).Scan(&u.ID, &u.Name, &u.profile, &u.Created, &u.Updated); err != nil {
		log.Fatal(err)
	}
	return u, nil
}
```

selectUserの実行結果を表示したものは以下のとおりです。

```
&{1 taka23kz { false} 2020-03-28 23:59:18.898586 +0900 JST 2020-03-28 23:59:18.898586 +0900 JST} <nil>
```
profileの取得内容が{ false}のようになっているのは、[sql.NullString](https://golang.org/pkg/database/sql/#NullString)のString部分とValid部分が表示されているためです。

>type NullString struct {
>	String string
>	Valid  bool // Valid is true if String is not NULL
>}

### database/sql Exec
update/insertにはExecメソッドを使っています。
updateUser、insertUserのprofileパラメータがsql.NullStringではなくて、stringとしているのは、単にメソッドの使い勝手を優先してのことです。
Execメソッドにprofileパラメータを渡す際には、newNullStringメソッドでstringからsql.NullStringに変換しています。

```go
func updateUser(db *sql.DB, id int64, profile string) error {
	if _, err := db.Exec("update t_user set profile = $1, updated = $2 where id=$3", profile, time.Now(), id); err != nil {
		log.Fatal(err)
	}
	return nil
}

func insertUser(db *sql.DB, name string, profile string) error {
	if _, err := db.Exec("insert into t_user (name, profile, created, updated) values($1, $2, $3, $4)", name, profile, time.Now(), time.Now()); err != nil {
		log.Fatal(err)
	}
	return nil
}

func newNullString(s string) sql.NullString {
	if len(s) == 0 {
		return sql.NullString{}
	}
	return sql.NullString{
		String: s,
		Valid:  true,
	}
}
```

テーブルから値をScanするということについては、これで問題ありませんが、Userの内容をJSONとして出力する場合にはこのままでは使えません。この問題についての対応方法については、脇道に逸れるため割愛しますが、参考文献の[みんなのGo言語](https://gihyo.jp/book/2019/978-4-297-10727-7)に記載があります。是非参考にしてみてください。

### トランザクション
database/sqlでトランザクション制御をするには、[Begin](https://golang.org/pkg/database/sql/#DB.Begin)でトランザクションを開始し、[Rollback](https://golang.org/pkg/database/sql/#Tx.Rollback)または[Commit](https://golang.org/pkg/database/sql/#Tx.Commit)でトランザクションを終了します。

```go
tx, err := db.Begin()
if err != nil {
	log.Fatal(err)
}
// tx.Rollback() or tx.Commit()
```

既に作成したselect/insert/updateに関するメソッドもトランザクション制御の対象とするために、以下のように、sql.DBだった第1引数をsql.Txに置き換えています。

```go
func selectUser(tx *sql.Tx, id int64) (*User, error) {
	u := &User{}
	if err := tx.QueryRow("select * from t_user where id=$1", id).Scan(&u.ID, &u.Name, &u.profile, &u.Created, &u.Updated); err != nil {
		log.Fatal(err)
	}
	return u, nil
}

func updateUser(tx *sql.Tx, id int64, profile string) error {
	if _, err := tx.Exec("update t_user set profile = $1, updated = $2 where id=$3", newNullString(profile), time.Now(), id); err != nil {
		log.Fatal(err)
	}
	return nil
}

func insertUser(tx *sql.Tx, name string, profile string) error {
	if _, err := tx.Exec("insert into t_user (name, profile, created, updated) values($1, $2, $3, $4)", name, newNullString(profile), time.Now(), time.Now()); err != nil {
		log.Fatal(err)
	}
	return nil
}
```
ただ、このままの実装では、個々のクエリーの成否を毎度判定して、Rollbackするか否かの判定が必要になるので、もっとスマートな実装が必要だと思います。その辺については別に記事を執筆しようかと思います。

## 参考文献

この記事は以下の情報を参考にして執筆しました。

- [Package sql](https://golang.org/pkg/database/sql/)
- [みんなのGo言語 改訂2版 ISBN978-4-297-10727-7](https://gihyo.jp/book/2019/978-4-297-10727-7)
- [Stack overflow. Golang Insert NULL into sql instead of empty string](https://stackoverflow.com/questions/40266633/golang-insert-null-into-sql-instead-of-empty-string)