[インデックス 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パッケージで定義されている特別なエラー変数です。ドライバが特定の操作(例:ExecやQuery)を直接処理できない、または処理したくない場合に返されます。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 から返された err が nil でない場合、即座にそのエラーを返していました。
問題は、driver.Execer の Exec メソッドが 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ブロックを抜けて「遅いパス」に進む
}
この修正により、ロジックは以下のように変更されました。
execer.Exec(query, args)を呼び出し、結果とエラーを取得します。err == nilの場合、つまりドライバがクエリを正常に実行できた場合は、その結果を返して処理を終了します。err != nilの場合、次にerr != driver.ErrSkipをチェックします。- もし
errがdriver.ErrSkipでない(つまり、真のエラーである)場合、そのエラーを呼び出し元に返します。 - もし
errがdriver.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 { ... }:- これは型アサーションです。
ciはdriver.Connインターフェースを実装するオブジェクトですが、ここではそれがdriver.Execerインターフェースも実装しているかどうかをチェックしています。 okがtrueの場合、ciはdriver.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 }でした。 - この変更により、
errがnilでない場合に、それがdriver.ErrSkipであるかどうかを明示的にチェックするようになりました。 - もし
errがdriver.ErrSkipでない場合(つまり、真の実行時エラーである場合)、そのエラーを呼び出し元に返します。 - 重要な点: もし
errがdriver.ErrSkipであった場合、このifブロックは実行されません。これにより、コードの実行フローはif execer, ok := ci.(driver.Execer); ok { ... }ブロックの外に進みます。
- 元のコードでは
-
ブロックを抜けた後の処理:
if execer, ok := ci.(driver.Execer); ok { ... }ブロックを抜けた場合、それは以下のいずれかの状況を意味します。ciがdriver.Execerインターフェースを実装していなかった。ciがdriver.Execerを実装しており、execer.Execを呼び出したが、driver.ErrSkipが返された。
- どちらの場合も、
database/sqlパッケージは「遅いパス」に進みます。この「遅いパス」は、ci.Prepare(query)を呼び出してプリペアドステートメントを作成し、そのプリペアドステートメントのExecメソッドを呼び出すことでクエリを実行します。これにより、ドライバが直接処理できないクエリに対しても、database/sqlパッケージがフォールバックとして機能し、適切な方法でクエリを実行できるようになります。
この修正は、database/sql パッケージがドライバの能力をより適切に利用し、driver.ErrSkip のセマンティクスを正しく尊重するための重要な改善です。
関連リンク
- Go言語
database/sqlパッケージのドキュメント: https://pkg.go.dev/database/sql - Go言語
database/sql/driverパッケージのドキュメント: https://pkg.go.dev/database/sql/driver - Go言語の
database/sqlチュートリアル (A Tour of Go): https://go.dev/tour/moretypes/1 (一般的なGoのチュートリアルですが、database/sqlの基本的な使い方を学ぶのに役立ちます)
参考にした情報源リンク
- 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検索を通じて得られた情報)