[インデックス 18007] ファイルの概要
このコミットは、Go言語の標準ライブラリである database/sql
パッケージにおける QueryRow.Scan
メソッドのエラーハンドリングの改善に関するものです。具体的には、ドライバの Next()
メソッドや Close()
メソッドから返されるエラーが適切にチェックされず、データベースからの実際のエラーが隠蔽される可能性があった問題(Issue #6651)を修正しています。
コミット
commit 1f20ab1116ab6cb0b77e22ffba3de9919e9def50
Author: Marko Tiikkaja <marko@joh.to>
Date: Mon Dec 16 12:48:35 2013 -0800
database/sql: Check errors in QueryRow.Scan
The previous coding did not correctly check for errors from the driver's
Next() or Close(), which could mask genuine errors from the database, as
witnessed in issue #6651.
Even after this change errors from Close() will be ignored if the query
returned no rows (as Rows.Next will have closed the handle already), but it
is a lot easier for the drivers to guard against that.
Fixes #6651.
R=golang-dev, bradfitz
CC=golang-dev
https://golang.org/cl/41590043
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/1f20ab1116ab6cb0b77e22ffba3de9919e9def50
元コミット内容
このコミットの元の内容は、database/sql
パッケージの QueryRow.Scan
メソッドにおいて、ドライバの実装が返す Next()
や Close()
からのエラーが適切にチェックされていなかった点を修正することです。これにより、データベースからの実際のエラーがユーザーに伝わらず、問題の特定が困難になるという Issue #6651
で報告された挙動が改善されます。
変更後も、クエリが結果行を返さなかった場合(Rows.Next
が既にハンドルを閉じているため)、Close()
からのエラーは無視されますが、ドライバ側でこのケースをより容易にガードできるようになります。
変更の背景
Go言語の database/sql
パッケージは、データベース操作のための汎用的なインターフェースを提供します。QueryRow
は単一の結果行を期待するクエリを実行するために使用され、その結果は Scan
メソッドによってGoの変数にマッピングされます。
Issue #6651
は、QueryRow().Scan()
を使用した際に、データベースドライバが Rows.Next()
や Rows.Close()
メソッド内でエラーを返しても、それが Scan
メソッドの呼び出し元に伝播しないという問題点を指摘していました。これは、例えばデータベース接続が切断された場合や、クエリ実行中に内部的なエラーが発生した場合に、アプリケーションがそのエラーを検知できず、誤った動作を続ける可能性があることを意味します。
この問題は、QueryRow
が内部的に Rows
オブジェクトを扱い、その Next()
や Close()
メソッドを呼び出す際に、これらのメソッドが返すエラーを適切にチェックしていなかったために発生していました。結果として、データベースからの重要なエラーシグナルが失われ、デバッグやエラーハンドリングが困難になっていました。このコミットは、このエラーの隠蔽を防ぎ、より堅牢なエラーハンドリングを実現することを目的としています。
前提知識の解説
このコミットを理解するためには、Go言語の database/sql
パッケージの基本的な概念と、データベースドライバのインターフェースに関する知識が必要です。
-
database/sql
パッケージ:- Goの標準ライブラリで、SQLデータベースとの対話のための汎用的なインターフェースを提供します。特定のデータベースシステムに依存しない抽象化レイヤーを提供し、ドライバを介して様々なデータベース(PostgreSQL, MySQL, SQLiteなど)に接続できます。
DB
オブジェクト: データベースへの接続プールを表します。QueryRow
メソッド: 単一の結果行を返すことが期待されるクエリを実行するために使用されます。例えば、SELECT name FROM users WHERE id = 1
のようなクエリに適しています。Row
オブジェクト:QueryRow
の結果を表すオブジェクトです。単一の結果行を保持します。Scan
メソッド:Row
オブジェクトのメソッドで、クエリ結果の列をGoの変数にスキャン(マッピング)します。Rows
オブジェクト:DB.Query
メソッドなど、複数の結果行を返すクエリの結果を表すオブジェクトです。結果セットをイテレートするために使用されます。Rows.Next()
メソッド:Rows
オブジェクトのメソッドで、結果セットの次の行に進みます。次の行が存在し、エラーなく準備できた場合はtrue
を返し、それ以外の場合はfalse
を返します。Rows.Close()
メソッド:Rows
オブジェクトのメソッドで、結果セットを閉じ、関連するリソースを解放します。これは非常に重要で、リソースリークを防ぐために必ず呼び出す必要があります。通常はdefer rows.Close()
のように使用されます。Rows.Err()
メソッド:Rows
オブジェクトのメソッドで、Next()
のイテレーション中に発生したエラーを返します。
-
driver
パッケージ:database/sql
パッケージの低レベルなインターフェースを定義しており、各データベースドライバはこのインターフェースを実装します。driver.Rows
インターフェース: データベースドライバが実装する結果セットのインターフェースです。Next()
やClose()
などのメソッドを含みます。
このコミットの核心は、database/sql
パッケージが driver
パッケージによって提供される Next()
や Close()
メソッドからのエラーをどのように扱うか、という点にあります。以前の実装では、これらのドライバレベルのエラーが QueryRow.Scan
の呼び出し元に適切に伝わっていなかったため、問題が発生していました。
技術的詳細
このコミットの技術的な詳細は、database/sql
パッケージの Row
型の Scan
メソッドと、Rows
型の Next
メソッドの内部的なエラーハンドリングの改善にあります。
変更前は、Row.Scan
メソッドが内部的に r.rows.Next()
を呼び出した際に、Next()
が false
を返した場合、それが「次の行がない」ためなのか、「エラーが発生した」ためなのかを区別していませんでした。単に ErrNoRows
を返すか、r.rows.Scan
で発生したエラーのみをチェックしていました。同様に、r.rows.Close()
が呼び出された際も、その戻り値のエラーが無視される可能性がありました。
このコミットでは、以下の2つの主要な変更が導入されています。
-
Row.Scan
におけるRows.Err()
のチェック:Row.Scan
メソッド内でr.rows.Next()
がfalse
を返した場合、単にErrNoRows
を返すのではなく、まずr.rows.Err()
をチェックするように変更されました。これにより、Next()
がfalse
を返した理由が、実際にエラーが発生したためであるならば、そのエラーがScan
メソッドの呼び出し元に適切に伝播されるようになります。これは、Rows.Next()
のドキュメントが「成功した場合はtrue
を返し、次の結果行がない場合、または準備中にエラーが発生した場合はfalse
を返す。両者の区別にはErr
を参照する必要がある」と明記している挙動に合致させるものです。 -
Row.Scan
におけるRows.Close()
のエラーチェック:Row.Scan
メソッドの最後に、r.rows.Scan(dest...)
が成功した後、r.rows.Close()
を呼び出し、その戻り値のエラーもチェックするように変更されました。これにより、結果セットを閉じる際にドライバ側で発生したエラーも捕捉し、Scan
メソッドの呼び出し元に伝播させることが可能になります。ただし、コミットメッセージにもあるように、クエリが結果行を返さなかった場合(Rows.Next
が既にハンドルを閉じているため)、Close()
からのエラーは無視される可能性があります。これは、QueryRow
の性質上、単一の結果行を期待するため、行が処理された後にRows
オブジェクトが自動的に閉じられることが期待されるためです。しかし、ドライバ側でこのケースをより適切に処理できるようになります。
これらの変更により、QueryRow.Scan
を使用するアプリケーションは、データベースドライバから報告されるより広範なエラーを捕捉できるようになり、エラーハンドリングの堅牢性が向上します。
コアとなるコードの変更箇所
このコミットにおけるコアとなるコードの変更は、主に src/pkg/database/sql/sql.go
ファイルの Row
型の Scan
メソッドに集中しています。
--- a/src/pkg/database/sql/sql.go
+++ b/src/pkg/database/sql/sql.go
@@ -1625,12 +1627,19 @@ func (r *Row) Scan(dest ...interface{}) error {
}
if !r.rows.Next() {
+ if err := r.rows.Err(); err != nil {
+ return err
+ }
return ErrNoRows
}
err := r.rows.Scan(dest...)
if err != nil {
return err
}
+ // Make sure the query can be processed to completion with no errors.
+ if err := r.rows.Close(); err != nil {
+ return err
+ }
return nil
}
また、テストのために src/pkg/database/sql/fakedb_test.go
にフックが追加され、src/pkg/database/sql/sql_test.go
に新しいテストケース TestIssue6651
が追加されています。
fakedb_test.go
の変更:
--- a/src/pkg/database/sql/fakedb_test.go
+++ b/src/pkg/database/sql/fakedb_test.go
@@ -686,7 +686,13 @@ func (rc *rowsCursor) Columns() []string {
return rc.cols
}
+var rowsCursorNextHook func(dest []driver.Value) error
+
func (rc *rowsCursor) Next(dest []driver.Value) error {
+ if rowsCursorNextHook != nil {
+ return rowsCursorNextHook(dest)
+ }
+
if rc.closed {
return errors.New("fakedb: cursor is closed")
}
この rowsCursorNextHook
は、テスト中に Next()
メソッドが特定のエラーを返すようにシミュレートするために使用されます。
コアとなるコードの解説
Row.Scan
メソッドの変更は、QueryRow
が単一の結果行を処理する際の堅牢性を高めるためのものです。
-
if !r.rows.Next()
ブロック内の変更:- 変更前は、
r.rows.Next()
がfalse
を返した場合、無条件にErrNoRows
を返していました。 - 変更後は、
r.rows.Next()
がfalse
を返した直後にif err := r.rows.Err(); err != nil { return err }
が追加されました。これは、Rows.Next()
がfalse
を返す理由が、単に次の行がないためではなく、イテレーション中にエラーが発生したためである可能性を考慮しています。もしエラーがあれば、そのエラーが即座にScan
メソッドの呼び出し元に返されます。これにより、データベースドライバがNext()
の実装内で発生させたエラー(例: ネットワークエラー、不正なデータ形式など)が適切に捕捉されるようになります。
- 変更前は、
-
r.rows.Close()
のエラーチェックの追加:err := r.rows.Scan(dest...)
が成功した後、if err := r.rows.Close(); err != nil { return err }
が追加されました。- これは、
QueryRow
が単一の結果行を処理した後、内部的に使用しているRows
オブジェクトを閉じる際に、ドライバのClose()
メソッドがエラーを返す可能性を考慮したものです。例えば、データベース接続が既に切断されている場合や、リソース解放中に問題が発生した場合に、Close()
がエラーを返すことがあります。この変更により、そのようなエラーもScan
メソッドの呼び出し元に伝播されるようになります。コミットメッセージにあるように、クエリが結果行を返さなかった場合はRows.Next
が既にハンドルを閉じているため、Close()
からのエラーは無視される可能性がありますが、これはQueryRow
の設計上の特性と、ドライバ側での対応の容易さを考慮したものです。
これらの変更により、QueryRow.Scan
は、結果行の取得中だけでなく、結果セットのクリーンアップ中にも発生しうるエラーをより包括的に捕捉し、アプリケーションに報告できるようになりました。
新しいテストケース TestIssue6651
は、これらの変更が意図通りに機能することを確認するために追加されました。
- 最初の部分では、
rowsCursorNextHook
を設定してNext()
がエラーを返すようにし、QueryRow().Scan()
がそのエラーを正しく捕捉することを確認しています。 - 2番目の部分では、
rowsCloseHook
を設定してClose()
がエラーを返すようにし、QueryRow().Scan()
がそのエラーを正しく捕捉することを確認しています。
関連リンク
- Go Issue #6651: https://github.com/golang/go/issues/6651
- Go CL 41590043: https://golang.org/cl/41590043
参考にした情報源リンク
- Go
database/sql
パッケージのドキュメント: https://pkg.go.dev/database/sql - Go
database/sql/driver
パッケージのドキュメント: https://pkg.go.dev/database/sql/driver - A Guide to the Go
database/sql
Package: https://go.dev/doc/database/ - Go
database/sql
tutorial: https://go.dev/doc/tutorial/database-access - Go
Rows.Next()
documentation: https://pkg.go.dev/database/sql#Rows.Next - Go
Rows.Err()
documentation: https://pkg.go.dev/database/sql#Rows.Err - Go
Rows.Close()
documentation: https://pkg.go.dev/database/sql#Rows.Close - Go
Row.Scan()
documentation: https://pkg.go.dev/database/sql#Row.Scan - Go
QueryRow()
documentation: https://pkg.go.dev/database/sql#DB.QueryRow