#はじめに
本記事はGo言語でのDBアクセス(select/insert/updateとトランザクション制御)に関するメモです。DBMSとしてpostgreSQL、ライブラリは標準のdatabase/sqlを使っています。
##database/sql
database/sqlは、Go言語のデータベースアクセス用の標準パッケージです。
database/sqlでは、アプリケーションに対して、DBを操作するためのインターフェースを提供しています。
下図に示したとおり、DBへの実際の操作を行うのはdbDrvierで、アプリケーションからドライバを直接扱うことはしません。
##database/sqlを使ったDB接続
database/sqlでDB接続するには、database/sqlとDBドライバをimportして、sql.Openを実行します。
sqlOpenの第1引数はドライバ名称で、今回はpostgreSQLを使うので「postgres」を設定します。第2引数は接続文字列です。こちらは使用するDBMSや環境に合わせて設定してださい。
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) | 複数行取得 |
QueryRow | func (db *DB) QueryRow(query string, args ...interface{}) *Row | 単一行取得。複数行返却された場合はエラー |
Exec | func (db *DB) Exec(query string, args ...interface{}) (Result, error) | insert/update |
他にもQueryContextのように~Contextと付いているメソッドがあります。こちらのメソッドはクエリーにcontextを設定でき、タイムアウトなんかを指定できるようです。
今回は、Query,QueryRow,Execを使って、以下のように定義したユーザ情報を格納するテーブルt_userを操作してみます。
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に合わせて構造体を定義しています。
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を直接記述します。
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をつけることでプレースホルダーを指定することもできます。
rows, err := db.Query("select * from t_comments where id=$1", 1)
上記の場合は、id(PrimaryKey)を指定して検索しているので、取得結果が1行になります。したがってQueryRowを使って書き換えることもできます。
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の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に変換しています。
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言語に記載があります。是非参考にしてみてください。
トランザクション
database/sqlでトランザクション制御をするには、Beginでトランザクションを開始し、RollbackまたはCommitでトランザクションを終了します。
tx, err := db.Begin()
if err != nil {
log.Fatal(err)
}
// tx.Rollback() or tx.Commit()
既に作成したselect/insert/updateに関するメソッドもトランザクション制御の対象とするために、以下のように、sql.DBだった第1引数をsql.Txに置き換えています。
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するか否かの判定が必要になるので、もっとスマートな実装が必要だと思います。その辺については別に記事を執筆しようかと思います。
参考文献
この記事は以下の情報を参考にして執筆しました。