Fork me on GitHub

リザルトセットの取得


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

  1. 行を返すクエリを発行します。

  2. 繰り返し用いるステートメントを準備し、複数回の実行と破棄をします。

  3. 繰り返し使用するための準備をせずに、一度だけのステートメントを実行します。

  4. 単一の行を返すクエリを発行します。この場合はショートカットがあります。

Goの database/sql の関数名は重要です。関数名に Query という文字を含んでいれば、データベースにクエリを発行し、(空行も許す)行の集合を返すために設計されているとわかります。行を返さないステートメントの場合、 Query 関数を使うべきではなく Exec() を用いるべきです。

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

データベースにクエリを発行して、結果を取得する方法の例を見てみましょう。user テーブルから id が 1 であるユーザを取得し、idname を表示します。rows.Scan() を使用して 1 度に 1 行ずつ、変数に結果を割り当てます。

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)
}

上記のコードで行われていることは次のとおりです。

  1. db.Query() を用いてデータベースにクエリを発行します。そしてエラーをチェックします。

  2. defer rows.Close() とします。これはとても重要です。

  3. rows.Next() を用いて行を繰り返し処理します。

  4. rows.Scan() を用いて、それぞれの行のカラムを結果を変数に読み込みます。

  5. 行の反復処理が完了したら、エラーをチェックします。

これは Go で行う唯一の方法です。たとえば行を連想配列として取得することはできません。なぜならすべてが強く型付けされているためです。次に示すように、適切な型の変数を宣言し、ポインタとして渡す必要があります。

上記の処理は間違えやすく、予期せぬ結果を招く可能性があります。

  • for rows.Next() のループの最後に必ずエラーをチェックする必要があります。ループ中にエラーが発生した場合、エラーについて知る必要があります。すべての行を処理するまで、ループが繰り返されるわけではありません。

  • 次にリザルトセット(上記の例では rows 変数に該当)が開いている限り、コネクションはビジーで、他のクエリには使用できません。つまりコネクションプールとしては使用できないということです。rows.Next() ですべての行が反復処理され、最後の行を読み込むと rows.Next() で内部的に EOF エラーが発生し、 rows.Close() が呼び出されます。ただし、なんらかの理由で早期リターンなどといったループの途中で終了するとリザルトセットはCloseされず、コネクションはOpenしたまま残ります。 rows.Next() がエラーのためにfalseを返す場合は自動的に閉じられます。容易にリソースの枯渇を導きます。

  • rows.Close() はすでにリザルトセットが閉じられていても無害な操作になるため、何度も呼び出すことができます。ただし、実行時のpanicを避けるために、エラーをチェックし、エラーがないときのみ rows.Close() を呼び出すことに注意してください。1

  • ループの最後で明示的に rows.Close() を呼び出す場合でも常に defer rows.Close() を実装すべきです。これは悪い考えではありません。

  • ループの中で defer を呼ばないでください。defer ステートメントは関数が終了するまで実行されません。そのため長時間実行される関数では使うべきではありません。使用すると少しずつメモリ使用量が増えます。繰り返しクエリを発行し、ループ内で結果を処理する場合は、それぞれの結果を処理したときに、明示的に rows.Close() を呼び出し defer を使うべきではありません。

Scan() の動作方法

行を反復処理し、結果を変数にスキャンすると、Go は内部でデータ型の変換を行います。これは結果を格納する変数の型に基づいています。これに注意することで、コードをきれいにし、繰り返しの作業を避けることができます。

例えば、VARCHAR(45) などの文字列のカラムとして定義されたテーブルからいくつかの行をフェッチするとします。ただしテーブルには常に数値を含んでいるとします。文字列にポインタを渡すと、Goはバイトを文字列にコピーします。これで strconv.ParseInt() などを用いて値を数値に変換することができます。SQL操作のエラーをチェックする必要があり、また整数の解析エラーをチェックする必要があります。これは面倒で退屈です。

その代わりに、単に Scan() を数値へのポインタに渡すことができます。Goは数値であることを検出して strconv.ParseInt() を呼び出します。変換時にエラーが発生した場合、Scan() を呼び出すとエラーが返されます。コードはすっきりと小さくなりました。これは database/sql の推奨される使い方です。

クエリの準備

一般に、複数回クエリを発行するためにクエリを予め準備する必要があります。クエリを準備した結果はプリペアードステートメントとなります。このステートメントはプレースホルダ(別名バインド値)としてステートメントの実行時に指定するパラメータを含めることができます。これはあらゆる理由(例えばSQLインジェクショ攻撃を回避するなど)で、文字列を結合するよりもはるかに優れています。

MySQLではパラメータのプレースホルダは ? です。PostgreSQLでは $N (Nは数値)です。SQLiteではどちらでもOKです。Oracleのプレースホルダの場合は :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倍になります。いくつかのドライバーは特定の場合に回避することができますが、すべてのドライバーが回避できるわけではありません。詳細は prepared statements を参照してください。

単一の行のクエリ

クエリが高々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() が呼ばれるまで遅延され、呼び出されると返ってきます。プリペアステートメントとして QueryRow() を呼ぶこともできます。

stmt, err := db.Prepare("select name from users where id = ?")
if err != nil {
    log.Fatal(err)
}
defer stmt.Close()
var name string
err = stmt.QueryRow(1).Scan(name)
if err != nil {
    log.Fatal(err)
}
fmt.Println(name)
1

rows がnilの場合に defer rows.Close() を呼び出すとnilポインターによるpanicが起こり、関数が終了します。