Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

[インデックス 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 パッケージの基本的な概念と、データベースドライバのインターフェースに関する知識が必要です。

  1. 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() のイテレーション中に発生したエラーを返します。
  2. 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つの主要な変更が導入されています。

  1. Row.Scan における Rows.Err() のチェック: Row.Scan メソッド内で r.rows.Next()false を返した場合、単に ErrNoRows を返すのではなく、まず r.rows.Err() をチェックするように変更されました。これにより、Next()false を返した理由が、実際にエラーが発生したためであるならば、そのエラーが Scan メソッドの呼び出し元に適切に伝播されるようになります。これは、Rows.Next() のドキュメントが「成功した場合は true を返し、次の結果行がない場合、または準備中にエラーが発生した場合は false を返す。両者の区別には Err を参照する必要がある」と明記している挙動に合致させるものです。

  2. 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 が単一の結果行を処理する際の堅牢性を高めるためのものです。

  1. 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() の実装内で発生させたエラー(例: ネットワークエラー、不正なデータ形式など)が適切に捕捉されるようになります。
  2. 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() がそのエラーを正しく捕捉することを確認しています。

関連リンク

参考にした情報源リンク