Go database/sqlチュートリアルの結果セットの取得の日本語意訳になります。
結果セットの取得
データストアから結果を取得するにはいくつか決まった方法があります。
- 行セットを返却するクエリを実行する。
- 繰り返し使用するステートメントを準備し、複数回実行し、破棄する。
- 繰り返し使用しない、1回限りのステートメントを実行する。
- 単一の行を返却するクエリを実行する。この特殊なケースには簡単な方法があります。
Goのdatabase/sql
の関数名は重要です、Query
を含む関数名は、データベースに問い合わせをするように設計されており、空であっても行セットを返却するようになっています。行を返却しないステートメントではQuery
を使うべきではありません。Exec()
を使うべきです。
データベースからのデータ取得
データベースへクエリを投げ、結果を操作する方法の例を見てみましょう。
users
テーブルへid
が1のユーザーを問い合わせ、ユーザーのid
とname
を出力するようにします。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)
前章: データベースへのアクセス
次章: 更新とトランザクション