LoginSignup
17
10

More than 3 years have passed since last update.

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

Last updated at Posted at 2020-03-29

はじめに

本記事は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や環境に合わせて設定してださい。

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) 複数行取得
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を操作してみます。

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に合わせて構造体を定義しています。

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するか否かの判定が必要になるので、もっとスマートな実装が必要だと思います。その辺については別に記事を執筆しようかと思います。

参考文献

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

17
10
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
17
10