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

[インデックス 11745] ファイルの概要

コミット

commit aca4a6c933c34f136408653c30595a9471372d5e
Author: Andrew Balholm <andybalholm@gmail.com>
Date:   Fri Feb 10 09:19:22 2012 +1100

    database/sql: support ErrSkip in Tx.Exec
    
    If the database driver supports the Execer interface but returns
    ErrSkip, calling Exec on a transaction was returning the error instead
    of using the slow path.
    
    R=golang-dev, bradfitz
    CC=golang-dev
    https://golang.org/cl/5654044

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/aca4a6c933c34f136408653c30595a9471372d5e

元コミット内容

このコミットは、Go言語の database/sql パッケージにおいて、トランザクションの Exec メソッドが driver.Execer インターフェースをサポートするデータベースドライバから driver.ErrSkip が返された場合に、エラーを返してしまう問題を修正するものです。本来 driver.ErrSkip は、その操作をドライバが直接処理できないことを示し、database/sql パッケージがフォールバックとして「遅いパス(slow path)」、つまりプリペアドステートメントを使用するべきであることを意味します。しかし、修正前は ErrSkip が返されると、Exec メソッドは単にエラーとして処理してしまっていました。

変更の背景

Go言語の database/sql パッケージは、データベース操作のための汎用的なインターフェースを提供します。このパッケージは、具体的なデータベースドライバ(例: MySQL, PostgreSQL, SQLiteなど)と連携して動作します。ドライバは database/sql/driver パッケージで定義されたインターフェースを実装することで、database/sql パッケージから利用可能になります。

driver.Execer インターフェースは、ドライバがSQLクエリを直接実行できる場合に実装されます。これにより、database/sql パッケージはプリペアドステートメントを介さずに、より効率的にクエリを実行できます。しかし、ドライバが特定のクエリを直接実行できない場合や、何らかの理由で Execer インターフェース経由での実行をスキップしたい場合に driver.ErrSkip という特別なエラーを返すことができます。

このコミットが行われる前の Tx.Exec メソッドの実装では、driver.Execer インターフェースを介してクエリを実行しようとした際に、ドライバが driver.ErrSkip を返すと、それを通常の実行時エラーとして扱ってしまい、呼び出し元にエラーを伝播させていました。これは ErrSkip の本来の意図(database/sql パッケージにフォールバック処理を促す)に反していました。結果として、ドライバが Execer をサポートしていても、特定のシナリオで ErrSkip を返すと、database/sql パッケージの「遅いパス」(プリペアドステートメントを使用するパス)が利用されず、不必要なエラーが発生していました。

この問題は、ドライバが Execer インターフェースを実装しているにもかかわらず、特定のクエリに対して ErrSkip を返すようなケースで顕在化しました。例えば、ドライバが特定のSQL方言や機能に特化した最適化された Exec 実装を持っているが、汎用的なクエリや複雑なクエリに対しては ErrSkip を返して database/sql パッケージのプリペアドステートメントによる処理に任せたい、といったシナリオが考えられます。

前提知識の解説

Go言語の database/sql パッケージ

database/sql パッケージは、GoプログラムからSQLデータベースにアクセスするための標準ライブラリです。このパッケージは、データベースドライバとアプリケーションコードの間の抽象化レイヤーを提供します。これにより、アプリケーションは特定のデータベースシステムに依存することなく、汎用的なAPIを使用してデータベース操作を行うことができます。

主要な概念:

  • DB: データベースへの接続プールを表します。
  • Tx: データベーストランザクションを表します。トランザクション内で複数の操作をアトミックに実行するために使用されます。
  • Stmt: プリペアドステートメントを表します。同じクエリを複数回実行する場合にパフォーマンスを向上させます。
  • Result: Exec メソッドの実行結果(影響を受けた行数、最後に挿入されたIDなど)を表します。

database/sql/driver パッケージ

database/sql/driver パッケージは、database/sql パッケージがデータベースと通信するために必要なインターフェースを定義しています。データベースドライバは、これらのインターフェースを実装することで、database/sql パッケージと統合されます。

主要なインターフェース:

  • Driver: データベースドライバのルートインターフェース。Open メソッドを持ち、データベースへの接続を確立します。
  • Conn: データベースへの単一の接続を表します。Prepare, Close, Begin などのメソッドを持ちます。
  • Execer: Conn インターフェースを実装する型が、SQLクエリを直接実行できる場合に実装するオプションのインターフェースです。このインターフェースを実装することで、database/sql パッケージはプリペアドステートメントを介さずに Exec 操作を実行できます。
    type Execer interface {
        Exec(query string, args []Value) (Result, error)
    }
    
  • driver.ErrSkip: これは database/sql/driver パッケージで定義されている特別なエラー変数です。ドライバが特定の操作(例: ExecQuery)を直接処理できない、または処理したくない場合に返されます。database/sql パッケージは ErrSkip を受け取ると、その操作をドライバに任せるのではなく、自身でフォールバック処理(通常はプリペアドステートメントを使用する「遅いパス」)を実行します。

トランザクション (Tx)

database/sql パッケージにおけるトランザクションは、一連のデータベース操作を単一の論理的な作業単位としてグループ化するために使用されます。トランザクション内のすべての操作は成功するか、すべて失敗するかのいずれかです(ACID特性の原子性)。Tx オブジェクトは DB.Begin() メソッドによって取得され、Commit() または Rollback() メソッドで終了します。

Tx.Exec メソッドは、トランザクション内でSQLのINSERT, UPDATE, DELETEなどのDML(Data Manipulation Language)ステートメントを実行するために使用されます。

技術的詳細

このコミットの核心は、database/sql パッケージ内の Tx.Exec メソッドにおける driver.Execer インターフェースの利用ロジックの修正です。

修正前のコードは以下のようになっていました。

	if execer, ok := ci.(driver.Execer); ok {
		resi, err := execer.Exec(query, args)
		if err != nil { // ここが問題
			return nil, err
		}
		return result{resi}, nil
	}

このコードでは、ci (これは driver.Conn インターフェースを実装するオブジェクト) が driver.Execer インターフェースも実装している場合、その Exec メソッドを呼び出します。そして、execer.Exec から返された errnil でない場合、即座にそのエラーを返していました。

問題は、driver.ExecerExec メソッドが driver.ErrSkip を返す可能性がある点です。driver.ErrSkip は、ドライバがその操作を直接処理できないことを示す特別なエラーであり、database/sql パッケージがフォールバック処理(プリペアドステートメントを使用する「遅いパス」)を行うべきであることを意味します。しかし、上記のコードでは ErrSkip も通常の実行時エラーと同様に扱われ、呼び出し元に伝播してしまっていました。

修正後のコードは以下のようになります。

	if execer, ok := ci.(driver.Execer); ok {
		resi, err := execer.Exec(query, args)
		if err == nil { // エラーがnilの場合のみ成功とみなす
			return result{resi}, nil
		}
		if err != driver.ErrSkip { // ErrSkipでない場合のみエラーを返す
			return nil, err
		}
		// ErrSkipの場合は、このifブロックを抜けて「遅いパス」に進む
	}

この修正により、ロジックは以下のように変更されました。

  1. execer.Exec(query, args) を呼び出し、結果とエラーを取得します。
  2. err == nil の場合、つまりドライバがクエリを正常に実行できた場合は、その結果を返して処理を終了します。
  3. err != nil の場合、次に err != driver.ErrSkip をチェックします。
    • もし errdriver.ErrSkip でない(つまり、真のエラーである)場合、そのエラーを呼び出し元に返します。
    • もし errdriver.ErrSkip である場合、この if ブロックを抜けます。これにより、database/sql パッケージは driver.Execer を介した高速パスをスキップし、その後のコードブロックで定義されている「遅いパス」(通常は ci.Prepare(query) を呼び出してプリペアドステートメントを作成し、それから実行するパス)に進むことになります。

この変更により、driver.ErrSkip が正しく解釈され、database/sql パッケージが意図したフォールバックメカニズムを利用できるようになりました。これにより、ドライバが Execer を実装していても、特定のクエリに対して柔軟に処理を委譲できるようになり、不必要なエラーの発生を防ぎます。

コアとなるコードの変更箇所

変更は src/pkg/database/sql/sql.go ファイルの Tx.Exec メソッド内で行われています。

--- a/src/pkg/database/sql/sql.go
+++ b/src/pkg/database/sql/sql.go
@@ -523,10 +523,12 @@ func (tx *Tx) Exec(query string, args ...interface{}) (Result, error) {
 
 	if execer, ok := ci.(driver.Execer); ok {
 		resi, err := execer.Exec(query, args)
-		if err != nil {
+		if err == nil {
+			return result{resi}, nil
+		}
+		if err != driver.ErrSkip {
 			return nil, err
 		}
-		return result{resi}, nil
 	}
 
 	sti, err := ci.Prepare(query)

コアとなるコードの解説

変更されたのは、Tx.Exec メソッドの冒頭部分、具体的には driver.Execer インターフェースの型アサーションと、その後のエラーハンドリングロジックです。

  • if execer, ok := ci.(driver.Execer); ok { ... }:

    • これは型アサーションです。cidriver.Conn インターフェースを実装するオブジェクトですが、ここではそれが driver.Execer インターフェースも実装しているかどうかをチェックしています。
    • oktrue の場合、cidriver.Execer インターフェースを実装しており、そのインスタンスが execer 変数に格納されます。
    • このブロック内では、ドライバが Execer をサポートしている場合の高速パスのロジックが記述されています。
  • resi, err := execer.Exec(query, args):

    • driver.Execer インターフェースの Exec メソッドを呼び出し、SQLクエリと引数をドライバに直接渡して実行を試みます。
    • resi には実行結果(driver.Result)、err にはエラーが返されます。
  • if err == nil { return result{resi}, nil } (追加):

    • execer.Exec がエラーなく成功した場合、その結果を database/sql パッケージの Result 型にラップして即座に返します。これは、ドライバがクエリを正常に処理できた場合の最も効率的なパスです。
  • if err != driver.ErrSkip { return nil, err } (変更):

    • 元のコードでは if err != nil { return nil, err } でした。
    • この変更により、errnil でない場合に、それが driver.ErrSkip であるかどうかを明示的にチェックするようになりました。
    • もし errdriver.ErrSkip でない場合(つまり、真の実行時エラーである場合)、そのエラーを呼び出し元に返します。
    • 重要な点: もし errdriver.ErrSkip であった場合、この if ブロックは実行されません。これにより、コードの実行フローは if execer, ok := ci.(driver.Execer); ok { ... } ブロックの外に進みます。
  • ブロックを抜けた後の処理:

    • if execer, ok := ci.(driver.Execer); ok { ... } ブロックを抜けた場合、それは以下のいずれかの状況を意味します。
      1. cidriver.Execer インターフェースを実装していなかった。
      2. cidriver.Execer を実装しており、execer.Exec を呼び出したが、driver.ErrSkip が返された。
    • どちらの場合も、database/sql パッケージは「遅いパス」に進みます。この「遅いパス」は、ci.Prepare(query) を呼び出してプリペアドステートメントを作成し、そのプリペアドステートメントの Exec メソッドを呼び出すことでクエリを実行します。これにより、ドライバが直接処理できないクエリに対しても、database/sql パッケージがフォールバックとして機能し、適切な方法でクエリを実行できるようになります。

この修正は、database/sql パッケージがドライバの能力をより適切に利用し、driver.ErrSkip のセマンティクスを正しく尊重するための重要な改善です。

関連リンク

参考にした情報源リンク

  • Go言語の公式ドキュメント (database/sql および database/sql/driver パッケージ)
  • Go言語のコミット履歴 (GitHub)
  • Go言語のコードレビューシステム (Gerrit) の関連CL (Change-list): https://golang.org/cl/5654044 (コミットメッセージに記載されているリンク)
  • Go言語の database/sql パッケージに関する一般的な解説記事やブログポスト (Web検索を通じて得られた情報)
  • driver.ErrSkip の挙動に関するGoコミュニティでの議論 (Web検索を通じて得られた情報)