2013年11月18日月曜日

Golang Cafe #4を開催しました。

Golang Cafe #4を開催しました。

今回のテーマは、「database/sqlパッケージを触る」ということで、
事前にPostgreSQL9.3.1をHomebrewでインストールしておきました。

リポジトリはGithubにあります。https://github.com/tyokoyama/golangcafe
ブランチCafe_#4を作成していますが、masterブランチにもマージしていますし、ディレクトリを変えているのでmasterからも参照できます。

PostgreSQLを使うので、ライブラリはPure Goのgithub.com/lib/pqを利用しました。
(他のデータベース、ライブラリを利用される方は、こちらを参照して下さい)

導入方法は、
go get github.com/lib/pq
で、インストールできます。

ここからは、lib/pqの前提で説明を続けます。

まず、クライアントの文字コードをUTF-8にしておく必要があります。(チェックロジックがpq/conn.goの113行目にあります。恐らく、新しいPostgreSQLならdefaultがUTF-8になっているので大丈夫だろうと思われます。

まず、pqのインポートを行って下さい。
import (
 _ "github.com/lib/pq"
 "database/sql"
)

import時に"_"をつけると、パッケージの読み込みは行うが、プログラムでは呼び出さない事を明示しています。今回は、database/sql/driverパッケージがinterfaceのかたまりで実装はpqにあります。したがって、database/sqlパッケージがdatabase/sql/driverのインターフェイスを呼び出すことで、pqで実装されたPostgreSQLへのクエリ実行のコードが呼び出される仕組みになっています。
要は、database/sqlがpqのドライバを呼び出すのでimportしておかないと、panicになると覚えておけば良いと思います。

データベースへの接続
db := sql.Open("postgres", "user=gdgchugoku dbname=sampledb sslmode=disable")
defer db.Close()

pqのドキュメント通りではないですが、PostgreSQL側のsslの設定を有効にしておかないとsslmode=verify-fullが動かないのと、省略時がsslmode=requiredらしいので、標準でsslを使った接続になるようです。したがって、サンプルで試す時はsslmode=disableにしてsslを使わないように指定しましょう。

Queryの実行
公式のサンプルだと、
row := db.QueryRow("select * from character where cost=?", param)
などと、記述できるようですが、
pqを利用する場合は、
row := db.QueryRow("select * from character where cost=$1", param)
というように、?ではなく、$1などという指定しか受け付けないようです。
http://godoc.org/github.com/lib/pqを見る限りでも、?のサンプルが見当たらないので、サポートしていないのかもしれません。コードを書く時は、Driver側のドキュメントを見た方がいいかもしれません。
ちなみに、RowもRowsもClose()が実装されていますが、自動的にクローズされるということなので、明示的に呼び出す必要はありません。
RowとRowsの違いですが、Rowの方は先頭1行のみで、全行取得はRowsを利用します。(中身の実装はRowもRowsを含んでいるので、同じものです。便利機能というところでしょうか?)

カーソルの制御
db.Query()の戻り値の*Rows.Next()を呼び出し、カーソルを次の行に移動させます。Golangの(pqの?)カーソルは最初の読み出しでもNextを呼び出さないと、行のデータが取得できません。
サンプル通りですが、以下の様なコードになります。
 if rows, err := db.Query("select * from character"); err == nil {
  if names, err := rows.Columns(); err != nil {
   t.Errorf("row.Columns error %v", err)
  } else {
   for name := range names {
    t.Logf("name = %s", name)
   }
  }

  for rows.Next() {
   // 行の中身を見る。
   var no int
   var name string
   var cost float64
   var typeId int
   var attack int
   var lead int
   var scheme string
   var morale int

   if err := rows.Scan(&no, &name, &cost, &typeId, &attack, &lead, &scheme, &morale); err != nil {
    t.Errorf("Scan Error %v", err)
   }
   t.Logf("Rows[%d, %s, %f, %d, %d, %d, %s, %d]", no, name, cost, typeId, attack, lead, scheme, morale)
  }
 } else if err == sql.ErrNoRows {
  t.Logf("Query NoRows")
 } else {
  t.Errorf("Query error %v", err)
 }

*Rows.Columns()は列の名前を取得するメソッドですが、PostgreSQLの仕様なのか、0,1,2・・・という数字(列番号)しか返してきませんでした。select * from ...のように全列取得だからかと列指定もしてみましたが、同様に0,1なので、他言語で列名が取得できるのは、何か別の工夫があるのか、pqの実装がショボイのかはわかりませんでした。(Driverの開発ノウハウが必要かも?)

更新系のクエリを実行する場合ですが、参照系と同様に、*DB.Query()を使えば実行できます。また、*DB.Exec()もあるので、そちらを利用してもいいかもしれません。ただ、pqの実装が甘いようで、Exec()だと、更新クエリにより影響を受けた行数はResult.RowsAffected()で取得できますが、最後に追加したIDはAPIとしては、Result.LastInsertId()が用意されていますが、pqではサポートしていません。(ドキュメントにも明記されています)

  if row, err := db.Query(fmt.Sprintf("insert into character values(%d, '羽柴秀吉', 2.5, 1, 8, 6, '撹乱貫通射撃', 4) RETURNING no", no)); err != nil {
   t.Errorf("Insert Query Error %v", err)
  } else {
   if names, err := row.Columns(); err != nil {
    t.Errorf("row.Columns error %v", err)
   } else {
    for name := range names {
     t.Logf("name = %s", name)
    }
   }

   row.Next()
   if names, err := row.Columns(); err != nil {
    t.Errorf("row.Columns error %v", err)
   } else {
    for name := range names {
     t.Logf("name = %s", name)
    }
   }

   if err := row.Scan(&no); err != nil {
    t.Errorf("Scan after insert Error %v", err)
   }
   t.Logf("no = %d", no)
  }

LastInsertId()がサポートされていないので、現状で更新したIDを取得するためには、PostgreSQLのクエリの機能であるRETURNINGをつける必要があります。これは、更新、削除の時も利用できますが、UPDATEの時に指定すると更新された行全てが返ってくるようになっています。(試していないけど、DELETEの時も複数行返ってくると思われる)

と、ここまでがトランザクションを使わずCRUDを実現する方法でした。
量が多いので、別の記事にしようと思っていますが、ひとまず、Golang Cafe #4のまとめとしてはこの辺で終わります。

#4で議論になったこと。
Q. GolangにORM無いの?
紹介がありました。
http://jmoiron.net/blog/golang-orms/