[インデックス 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検索を通じて得られた情報)