Golang Cafe #4 まとめ データベースアクセス(PostgreSQL編)

2013/11/17に開催された「Golang Cafe #4」についてのまとめです。
Go言語でも他の言語と同じようにデータベースへのアクセスを行うことができます。
今回はPostgreSQLを用いてGo言語での操作を試してみました。

事前準備

PostgreSQLのインストール

私の環境はWindows7 64ビット版です。こちらから64ビット版をダウンロードし、インストールします。windows版ですのでインストール中に初期化もされ、pgAdminもインストールされるのですぐに利用できます。

PostgreSQL用ドライバのインストール

GOPATH直下にて go get コマンドを実行します。(go getコマンドを使用するためにはMercurialが必要です)

$ go get github.com/lib/pq

詳しいドキュメントはpq - GoDocにあります。また現在利用できるドライバの一覧はSQLDriversに記載されています。

検証プログラム

今回も+TakashiYokoyama氏がたたき台を準備してくれ、それを元に検証を進めていきましたが、以下の内容は私自身が復習も兼ねてスクラッチで書いた検証コードを元にまとめたものになります。
DDLソースコードhttps://github.com/taknb2nch/godbtest/tree/master/postgresqlに置いてあります。
実行は以下の手順で行います。

  1. PostgreSQLサーバーにgodbtestデータベースを作成します。(ユーザ、パスワードはともにpostgresとします、文字コードはutf8)
  2. 必要に応じて01.connect_test.goのgetConnection関数の接続文字列を修正します。
  3. プロジェクト内のcreate_table.sqlをgodbtestに対して実行します。
  4. プロジェクト内のsample_data.sqlをgodbtestに対して実行します。
  5. プロジェクトディレクトリにて go test -v > result.log を実行します。(リダイレクトしているのは出力された内容を後で確認しやすくするためです)
  6. 再度実行するには4と5を繰り返します。

今回のサンプルデータは疑似個人情報データ生成サービスを利用しています。

はじめに

Go言語でのデータベースアクセスでは高度な機能は用意されておらず、JavaC#(ResultSetやDbDataReader)と同じような形になります。Rails系にみられるActiveRecordなことはできません。

基本的な手順は他の言語と同じく以下のようになります。

  1. データベース接続
  2. トランザクション開始(必要に応じて)
  3. クエリー実行
  4. 結果の取得
  5. トランザクションコミットまたはロールバック(必要に応じて)
  6. データベース切断


今回検証するテーブル定義は以下のとおりで、

create table table1 (
  id serial not null
  , display_name character varying(100)
  , sex character(1) not null
  , birthday timestamp
  , age integer
  , married boolean
  , rate real
  , salary numeric(10,0)
  , constraint table1_PKC primary key (id)
) ;

このテーブルの1レコードに対応した下記のような構造体を定義してプログラム中で使用しています。

type Record struct {
    id int
    displayName sql.NullString
    sex string
    birthday pq.NullTime
    age sql.NullInt64
    married sql.NullBool
    rate sql.NullFloat64
    salary sql.NullInt64
}

idとsex以外はnull許容型となっています。
go言語のデフォルトではTimeのNull許容型は用意されていないので、各ドライバレベルで定義されているものを使用します。


以降、接続・切断、参照系、更新系、トランザクションとまとめていきたいと思います。

データベースへの接続と切断

database/sqlパケージ以下は基本的な型、値、処理、インターフェースが定義されており事前準備でインストールしたドライバがインターフェースを実装している形となります。
データベースへの接続と切断を行うには以下のようにします。(※ソースコードは必要な部分のみを掲載しています)

// 01.connect_test.go
package godbtest

import (
    _ "github.com/lib/pq"
    "database/sql"
    "testing"
)

func Testデータベースへの接続(t *testing.T) {
	
    db, err := sql.Open("postgres", "user=postgres1 password=postgres host=localhost dbname=godbtest sslmode=disable")
    // db, err := sql.Open("postgres", "postgres://postgres:postgres@localhost/godbtest?sslmode=disable")

    if err != nil {
        t.Fatalf("データベースの接続に失敗しました。: %v", err)
    }

    // データベースに接続するため、実際は不要
    err = db.Ping()

    if err != nil {
        t.Fatalf("データベースの接続に失敗しました。2: %v", err)
    }

    defer db.Close()

}

import文で先頭が_始まっているものがありますが、これは「インポートはするけど直接は使用しない」というものです。
ソースコード中ではdatabase/sqlパッケージを使用するのですが、実際にはgithub.com/lib/pqのコードが実行されるため必要ということらしいです。

sql.Open("ドライバ名", "接続文字列")

ドライバ名はgithub.com/lib/pq側のinit関数で定義されています。
接続文字列はパラメータ=値(スペース区切り)またはURLの形で指定できます。(詳細はpq - GoDocを参照)
sql.Open()を実行しただけではデータベースには接続されず、初めてクエリを実行するタイミングで接続されるようです。上記サンプルではdb.Ping()を読んだ時点で接続されます。sql.SetMaxIdleConns()でコネクションプール数を設定できるようですが、うまい検証コードが思いつきませんでした。

SELECT

複数件検索

複数件のレコードを取得するにはsql.Open()にて取得したDB型のQueryメソッドを使用します。
引数にクエリー文字列と可変長のパラメータをとり、Rows型とerror型を返します。

// 02.select_test.go
query := "select id, display_name, sex, birthday, age, married, rate, salary from table1 where id>=$1"

rows, err := db.Query(query, key)

if err != nil {
    t.Fatalf("クエリーの実行に失敗しました。: %v", err)
}

for rows.Next() {

    var r Record

    if err = rows.Scan(&r.id, &r.displayName, &r.sex, &r.birthday, &r.age, &r.married, &r.rate, &r.salary); err != nil {
        t.Errorf("値の取得に失敗しました。: %v", err)
    }

    t.Logf("%d, %v, %v, %v, %v, %v, %v, %v", 
        r.id, r.displayName, r.sex, r.birthday, r.age, r.married, r.rate, r.salary)
}

rows.Close()

rowsは初期状態でカーソルが先頭レコードの前にあるので注意が必要です。rows.Next()でカーソルを進めていきながら処理をします。
各行の値を取得するにはrows.Scan()を使用します。
引数には値を受け取りたい変数のアドレスを渡します。今回は構造体を使用していますが、各メンバそれぞれのアドレスを渡す必要があります。
クエリーで指定したカラムの数と結果を受け取る変数の数が異なるとエラーになります。
他言語と同じようにクエリーでパラメータを使用する場合は$を使用します。

1件検索

1件のレコードを取得するにはDB型のQueryRowメソッドを使用します。内部的にはRowsの最初の1件を返しているようです。
Queryメソッドとは異なり、引数にクエリー文字列と可変長のパラメータをとり、Row型を返します。

// 02.select_test.go
query := "select id, display_name, sex, birthday, age, married, rate, salary from table1 where id=$1"

var r Record

err := db.QueryRow(query, key).Scan(&r.id, &r.displayName, &r.sex, &r.birthday, &r.age, &r.married, &r.rate, &r.salary)

switch {
case err == sql.ErrNoRows : 
    t.Logf("対象のレコードは存在しません。id=%d : %v", key, err)
case err != nil :
    t.Fatalf("値の取得に失敗しました。: %v", err)
default :
    t.Logf("%d, %v, %v, %v, %v, %v, %v, %v", 
        r.id, r.displayName, r.sex, r.birthday, r.age, r.married, r.rate, r.salary)
}

クエリーの実行に失敗したかどうかはrow.Scan()の戻り値で判定する必要があるようです。またレコードが存在しない場合も同じ戻り値で判定できるようです。nilが返ってくるわけではないようです。

結果セットのカラム名の取得

Queryメソッドの戻り値のrowsのColumnsメソッドで結果セットに存在するカラム名を取得できるようです。

// 02.select_test.go
rows, err := db.Query(query, 1)

if err != nil {
    t.Fatalf("クエリーの実行に失敗しました。: %v", err)
}

names, err := rows.Columns()

if err != nil {
    t.Fatalf("カラム名の取得に失敗しました。: %v", err)
}

for name := range names {
    t.Logf("%v", name)
}

ただ現時点ではカラム名のインディックスが取得出来るだけでカラム名は取得できないようです。

INSERT

1件追加

レコードを追加するにはDB型のExecメソッドを使用します。引数にクエリー文字列とパラメータをとり、Result型とerror型を返します。

// 03.insert_test.go
query := "insert into table1 (display_name, sex, birthday, age, married, rate, salary) "
query += "values ($1, $2, $3, $4, $5, $6, $7) returning id"

var r = createRecord()
result, err := db.Exec(query, r.displayName, r.sex, r.birthday, r.age, r.married, r.rate, r.salary)

if err != nil {
    t.Fatalf("クエリーの実行に失敗しました。: %v", err)
}

if c, err := result.LastInsertId(); err != nil {
    t.Logf("LastInsertIdを取得できません。: %v", err)
} else {
    t.Logf("LastInsertId: %v", c)
}

if c, err := result.RowsAffected(); err != nil {
    t.Errorf("RowsAffectedを取得できません。: %v", err)
} else {
    t.Logf("RowsAffected: %v", c)
}

基本的に更新系の処理を行うにはExecメソッドでよいと思うのですが、PostgreSQLでInsertの場合は注意が必要です。SequenceやAUTO_INCREMENTを使用したInsertの場合、result.LastInsertId()でInsertした値(SequenceやAUTO_INCREMENTの結果)を取得できるのですが、PostgreSQLでは現段階ではサポートされておらず取得できません。
そこで代替手段として以下のようにします。

// 03.insert_test.go
query := "insert into table1 (display_name, sex, birthday, age, married, rate, salary) "
query += "values ($1, $2, $3, $4, $5, $6, $7) returning id"

var r = createRecord()
var newId int

err := db.QueryRow(query, r.displayName, r.sex, r.birthday, r.age, r.married, r.rate, r.salary).Scan(&newId)

switch {
case err == sql.ErrNoRows : 
    // TODO:
    t.Logf("対象のレコードは存在しません。: %v", err)
case err != nil :
    t.Fatalf("値の取得に失敗しました。: %v", err)
default :
    t.Logf("登録ID=%d", newId)
}

クエリーの最後にreturning句でInsert後の値で返したいものを指定します。QueryRowメソッドでクエリーを発行し取得した結果セットの中にInsert後の値が格納されているのでそれを取得するという方法になります。
その他の更新系(UpdateやDelete)でも同様な方法で取得できると思いますが、事前に分かっていることが多いと思うのでExecメソッドで十分かもしれません。

PreparedStatementを使用した複数件の追加

複数件のレコードを追加する場合はDB型のPrepareメソッドを使用します。(通常のInsertを複数回実行しても構いません)
引数にクエリー文字列をとり戻り値にStmt型とerror型を返します。
Stmt型のQueryRow(またはExec)メソッドをクエリーのパラメータを引数によぶことでクエリーを発行できます。

// 03.insert_test.go
stmt, err := db.Prepare(query)

if err != nil {
    t.Fatalf("Prepareに失敗しました。: ", err)
}

for i := 0; i < 5; i++ {

    var r = createRecord()
    row := stmt.QueryRow(r.displayName, r.sex, r.birthday, r.age, r.married, r.rate, r.salary)
    //result, err := stmt.Exec(r.displayName, r.sex, r.birthday, r.age, r.married, r.rate, r.salary)

    // 省略
}

stmt.Close()

UPDATE

1件または複数件の更新

基本的にはInsertの場合と同じです。ただsequence等の処理結果を取得する必要が無いことが多いと思うのでExecメソッドで十分だと思います。

// 04.update_test.go
query := "update table1 set display_name=$1, sex=$2, birthday=$3, age=$4, married=$5, rate=$6, salary=$7 "
query += "where id=$8 returning id"

result, err := db.Exec(query, nil, 0, nil, nil, nil, nil, nil, id)

if err != nil {
    t.Fatalf("クエリーの実行に失敗しました。: %v", err)
}

if c, err := result.LastInsertId(); err != nil {
    t.Logf("LastInsertIdを取得できません。: %v", err)
} else {
    t.Logf("LastInsertId: %v", c)
}

if c, err := result.RowsAffected(); err != nil {
    t.Errorf("RowsAffectedを取得できません。: %v", err)
} else {
    t.Logf("RowsAffected: %v", c)
}

result.RowsAffected()の値を参照することで更新件数を取得することができます。更新対象のレコードが存在しなかった、場合はエラーにはならず0件になります。更新対象が複数件の場合はその件数が返ります。

PreparedStatementを使用した複数件の更新

PreparedStatementを使用する場合もInsertの場合と同じです。
1件または複数件の更新を行う場合と同じくExecメソッドを使用する以外に違いはないのでコードだけ載せておきます。

// 04.update_test.go
stmt, err := db.Prepare(query)

if err != nil {
    t.Fatalf("Prepareに失敗しました。: ", err)
}

for i := 0; i < 5; i++ {

    result, err := stmt.Exec(nil, 0, nil, nil, nil, nil, nil, id - i)

    // 省略
}

stmt.Close()

DELETE

1件または複数件の削除

クエリーの内容が異なるだけでUpdateの場合と同じなのでコードだけ載せておきます。

// 05.delete_test.go
query := "delete from table1 where id=$1"

result, err := db.Exec(query, id)

if err != nil {
    t.Fatalf("クエリーの実行に失敗しました。: %v", err)
}

if c, err := result.LastInsertId(); err != nil {
    t.Logf("LastInsertIdを取得できません。: %v", err)
} else {
    t.Logf("LastInsertId: %v", c)
}

if c, err := result.RowsAffected(); err != nil {
    t.Errorf("RowsAffectedを取得できません。: %v", err)
} else {
    t.Logf("RowsAffected: %v", c)
}
PreparedStatementを使用した複数件の削除

クエリーの内容が異なるだけでUpdateの場合と同じなのでコードだけ載せておきます。

// 05.delete_test.go
stmt, err := db.Prepare(query)

if err != nil {
    t.Fatalf("Prepareに失敗しました。: ", err)
}

for i := 0; i < 5; i++ {

    result, err := stmt.Exec(id - i)

    // 省略

}

stmt.Close()

トランザクション

go言語でもトランザクションを使用したデータベースアクセスを行うことができます。
トランザクションを使用するにはDB型のBeginメソッドを使用します。引数は不要でTx型とerror型を返します。

// 06.transaction_test.go
tx, err := db.Begin()

if err != nil {
    t.Fatalf("トランザクションの取得に失敗しました。: %v", err)
}

query := "insert into table1 (display_name, sex, birthday, age, married, rate, salary) "
query += "values ($1, $2, $3, $4, $5, $6, $7) returning id"

var r = createRecord()
var newId int

err := tx.QueryRow(query, r.displayName, r.sex, r.birthday, r.age, r.married, r.rate, r.salary).Scan(&newId)

// 本来ならerrの内容を確認してcommitまたはrollbackを決める必要がある
err = tx.Commit()
// err = tx.Rollback()

if err != nil {
    t.Fatalf("トランザクションのコミットに失敗しました。: %v", err)
} else {
    t.Logf("トランザクションをコミットしました。")
}

トランザクションを使用しない場合は、DB型のQuery、QueryRow、Exec、あるいはPrepareメソッドをよんでいましたが、トランザクションを使用する場合はdb.Begin()にて取得したTx型に対して同様のメソッドをよぶだけの違いです。
そして処理の最後にtx.Commit()またはtx.Rollback()をよびます。

補足

本文中ではソースコードは一部分を抜粋して掲載しているので実際の検証コードとテスト結果を照らしあわせてみるほうが分かりやすいかもしれません。


今回はデータベースにPostgreSQLを使用しましたが、後ほど検証してみたところMySQLを使用したほうがわかりやすかったかもしれません。
MySQLを使用した場合の違い等は別エントリーでまとめたいと思います。