Go database/sql チュートリアル 04 - 結果セットの取得

"Retrieving Result Sets" from database/sql tutorial

Go database/sqlチュートリアルの結果セットの取得の日本語意訳になります。


結果セットの取得

データストアから結果を取得するにはいくつか決まった方法があります。

  • 行セットを返却するクエリを実行する。
  • 繰り返し使用するステートメントを準備し、複数回実行し、破棄する。
  • 繰り返し使用しない、1回限りのステートメントを実行する。
  • 単一の行を返却するクエリを実行する。この特殊なケースには簡単な方法があります。

Goのdatabase/sqlの関数名は重要です、Queryを含む関数名は、データベースに問い合わせをするように設計されており、空であっても行セットを返却するようになっています。行を返却しないステートメントではQueryを使うべきではありません。Exec()を使うべきです。

データベースからのデータ取得

データベースへクエリを投げ、結果を操作する方法の例を見てみましょう。
usersテーブルへidが1のユーザーを問い合わせ、ユーザーのidnameを出力するようにします。rows.Scan()を使い、一回に一行ずつ結果を変数に割り当てるようにします。

var (
	id int
	name string
)
rows, err := db.Query("SELECT id, name FROM users WHERE id = ?", 1)
if err != nil {
	log.Fatal(err)
}
defer rows.Close()
for rows.Next() {
	err := rows.Scan(&id, &name)
	if err != nil {
		log.Fatal(err)
	}
	log.Println(id, name)
}
err = rows.Err()
if err != nil {
	log.Fatal(err)
}

上記コードでは以下のようなことが起こっています。

  • db.Query()を使い、データベースへクエリを送信しています。普段通り、エラーチェックをしています。
  • rows.Close() を遅延実行しています。これはとても重要です。
  • rows.Next()を使い、行セットに対して繰り返し処理をしています。
  • 各行において、rows.Scan()でカラムを変数へ読み込んでいます。
  • 各行への繰り返し処理が終わると、エラーをチェックしています。

これがGo言語でデータベースのデータを取得する唯一の方法です。例えば行をマップとして取得することはできません。なぜなら全てが強く型付けされているからです。
この例のように、正しい型の変数を作成しそのポインタを渡す必要があります。

この内、いくつかの部分は間違いやすく、悪い結果をもたらします。

  • for rows.Next()ループの終了後、必ずエラーチェックをしてください。ループ中でのエラー発生を把握する必要があります。全ての行が処理されるまでループが継続するはずだと決めつけないでください。
  • 2つ目に、(rowsで表現される)オープン中の結果セットがある限り、実際のコネクションはビジー状態になっていて、他のクエリで使うことはできません。つまりコネクションプール中でそのコネクションは利用不可能です。もしrows.Next()で全ての行を繰り返し処理すると、最終的には最後の行が読み込まれ、rows.Next()内部でEOFエラーが発生し、rows.Close()を呼び出します。しかし、もしも早期リターンといったような理由でループを抜けてしまうと、rowsはクローズされませんし、コネクションもオープンのままになってしまいます。(ただし、rows.Next()がエラーによってfalseが返却される時には、自動的にクローズされます。)こうなるとリソースが簡単に使い尽くされてしまいます。
  • rows.Close()は既にクローズされている場合には何もしない安全な操作なので、何回呼び出しても大丈夫です。ただし実行時パニックを避けるために、エラーチェックを最初に行い、エラーがない場合のみrows.Close()を呼び出すようにしてください。
  • たとえループ終了時に明示的にrows.Close()を呼び出していたとしても、defer rows.Close()をするようにしてください。これは悪いアイデアではありません。
  • ループ内ではdeferはしないでください。その関数が終了するまでdefer文は実行されないため、長時間実行される関数では使うべきではありません。そうしてしまうと、徐々にメモリが蓄積されていくでしょう。もしもループ内で繰り返しクエリを実行し、結果セットを取得している場合は、deferを使わずに毎回結果の利用が終わった際に明示的にrows.Close()を呼び出すようにしてください。


Scan()の動作について

行データを繰り返し処理し、その値を変数へ格納する時、Goは裏側でデータの型変換をしてくれます。これはその変数の型に基いて行われます。このことを知っていると、コードをクリーンに保ち、繰り返しの作業を避けることができるでしょう。

例えば、VARCHAR(45)のような文字列カラムが定義されたテーブルからいくつかの行を取得するとします。しかし、そのテーブルには常に数値があることを知りました。もしScan()string型へのポインタを渡すと、Goはバイト列をそのstring型へとコピーしてくれるでしょう。その後はstrconv.ParseInt()などで、値を数値に変換することができます。intへの変換エラーと同様に、SQL操作のエラーもチェックする必要があります。これは面倒で乱雑です。

もしくは、Scan()int型のポインタを渡すことができます。Goはそれを検出してstrconv.ParseInt()を呼び出してくれます。変換エラーがある場合は、Scan()の戻り値としてエラーが返却されます。あなたのコードは短くきれいになりました。database/sqlを使うときはこれが推奨される方法です。

クエリのプリペア

一般的に、何度も使うクエリは常にプリペアにするべきです。
クエリのプリペアの結果がプリペアドステートメントになり、これはステートメント実行時に指定するパラメータのプレースホルダ(バインド値)を持つことができます。これは文字列の結合を行うよりも、様々な理由(例えばSQLインジェクション対策)ではるかに優れています。

MySQLでのプレースホルダは?で、PostgreSQLでは$NでNには数値が入ります。SQLiteではどちらも使えます。オラクルでは:param1のようなコロンで始まる命名になっています。この例ではMySQLを使っているので?を使っていきます。

stmt, err := db.Prepare("SELECT id, name FROM users WHERE id = ?")
if err != nil {
	log.Fatal(err)
}
defer stmt.Close()
rows, err := stmt.Query(1)
if err != nil {
	log.Fatal(err)
}
defer rows.Close()
for rows.Next() {
	// ...
}
if err = rows.Err(); err != nil {
	log.Fatal(err)
}

db.Query()の裏側で、実際にはクエリのプリペアをし、クエリを実行し、そしてプリペアドステートメントをクローズしています。それはデータベースを3往復しています。もしあなたが注意深い人間でないならば、アプリケーションとデータベースとのやりとりを3倍にしてしまうでしょう!いくつかのドライバは特定のケースでこれを避けることができますが、全てのドライバがそうするわけではありません。詳細はプリペアドステートメントを参照してください。

単一行クエリ

クエリが多くとも1行しか返却しない場合、冗長な定型コードをいくらか省略することができます。

var name string
err = db.QueryRow("SELECT name FROM users WHERE id = ?", 1).Scan(&name)
if err != nil {
	log.Fatal(err)
}
fmt.Println(name)

クエリのエラーはScan()が呼び出されるまで延期され、Scan()によって返却されます。また、プリペアドステートメントからQueryRow()を呼び出すこともできます。

stmt, err := db.Prepare("SELECT name FROM users WHERE id = ?")
if err != nil {
	log.Fatal(err)
}
var name string
err = stmt.QueryRow(1).Scan(&name)
if err != nil {
	log.Fatal(err)
}
fmt.Println(name)

前章: データベースへのアクセス
次章: 更新とトランザクション

 
comments powered by Disqus