[インデックス 10516] ファイルの概要
このコミットは、Go言語の実験的なexp/sqlパッケージ(現在のdatabase/sqlパッケージの前身)において、トランザクション内で既存のプリペアドステートメントを再利用するためのTx.Stmtメソッドを追加するものです。これにより、データベース操作の効率性と柔軟性が向上します。
コミット
commit e77099daa2167cb394d133bd525fa5dc1c0771a8
Author: Brad Fitzpatrick <bradfitz@golang.org>
Date: Mon Nov 28 11:00:32 2011 -0500
sql: add Tx.Stmt to use an existing prepared stmt in a transaction
R=rsc
CC=golang-dev
https://golang.org/cl/5433059
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/e77099daa2167cb394d133bd525fa5dc1c0771a8
元コミット内容
sql: add Tx.Stmt to use an existing prepared stmt in a transaction
R=rsc
CC=golang-dev
https://golang.org/cl/5433059
変更の背景
Go言語のdatabase/sqlパッケージ(当時はexp/sql)では、データベース操作を効率化するためにプリペアドステートメント(Prepared Statement)が提供されています。プリペアドステートメントは、SQLクエリを事前にデータベースに送信してコンパイル・最適化しておくことで、同じクエリを複数回実行する際のオーバーヘッドを削減します。
しかし、このコミット以前のexp/sqlパッケージでは、トランザクション(Tx)内でプリペアドステートメントを使用する際に、いくつかの課題がありました。特に、DB.Prepareで作成された既存のプリペアドステートメントを、新たに開始したトランザクション内で直接再利用する効率的なメカニズムが不足していました。
Tx.Prepareメソッドは存在しましたが、これはトランザクションのスコープ内でのみ有効な新しいプリペアドステートメントを作成するものでした。つまり、グローバルに(DBオブジェクトに対して)一度だけプリペアしたステートメントを、複数のトランザクションで効率的に使い回すことが困難でした。この制限は、特に頻繁に実行されるがトランザクションの分離が必要な操作において、パフォーマンス上のボトルネックとなる可能性がありました。
このコミットは、この課題を解決し、開発者が既存のプリペアドステートメントをトランザクション内でより柔軟かつ効率的に利用できるようにするために導入されました。
前提知識の解説
1. プリペアドステートメント (Prepared Statement)
プリペアドステートメントは、データベースシステムにおけるSQLクエリの実行を最適化するための機能です。
- 準備 (Prepare): SQLクエリのテンプレート(プレースホルダーを含む)をデータベースに一度だけ送信し、データベース側でそのクエリの構文解析、セマンティックチェック、実行計画の生成などを行います。この「準備」された状態のクエリがプリペアドステートメントです。
- 実行 (Execute): 準備されたステートメントに対して、実際のパラメータ値をバインドして複数回実行します。
- 利点:
- パフォーマンス向上: クエリの解析と最適化が一度で済むため、同じクエリを繰り返し実行する際のオーバーヘッドが減少します。
- SQLインジェクション対策: パラメータがデータとして扱われるため、悪意のあるSQLコードの挿入を防ぐことができます。
- ネットワークトラフィック削減: クエリ文字列全体を毎回送信する必要がなく、パラメータのみを送信すればよいため、ネットワーク帯域の使用量を削減できます。
2. データベーストランザクション (Database Transaction)
トランザクションは、データベース操作の論理的な単位であり、以下のACID特性を保証します。
- 原子性 (Atomicity): トランザクション内のすべての操作が成功するか、すべて失敗するかのいずれかであり、部分的な完了は許されません。
- 一貫性 (Consistency): トランザクションの開始時と終了時で、データベースは一貫した状態を保ちます。
- 分離性 (Isolation): 複数のトランザクションが同時に実行されても、それぞれが独立して実行されているかのように見えます。
- 永続性 (Durability): コミットされたトランザクションの結果は、システム障害が発生しても失われません。
Goのdatabase/sqlパッケージでは、DB.Begin()メソッドでトランザクションを開始し、Tx.Commit()でコミット、Tx.Rollback()でロールバックします。
3. database/sqlパッケージの構造(当時のexp/sql)
DB: データベースへの接続プールを管理するオブジェクト。Tx: 進行中のトランザクションを表すオブジェクト。Stmt: プリペアドステートメントを表すオブジェクト。DB.PrepareまたはTx.Prepareで作成されます。
技術的詳細
このコミットの主要な変更点は、Tx型にStmtメソッドを追加したことです。このメソッドは、既存の*Stmt(通常はDB.Prepareで作成されたもの)を受け取り、それを現在のトランザクションのコンテキストで実行可能な*Stmtを返します。
Tx.PrepareとTx.Stmtの違い
-
Tx.Prepare(query string) (*Stmt, error):- このメソッドは、指定されたクエリ文字列に対して、現在のトランザクション専用の新しいプリペアドステートメントを作成します。
- 返される
*Stmtは、そのトランザクションがコミットまたはロールバックされると無効になります。 - 内部的には、トランザクションが使用するコネクション上で
Prepare操作を実行します。
-
Tx.Stmt(stmt *Stmt) *Stmt:- このメソッドは、既存の
*Stmtオブジェクト(例えば、DB.Prepareで作成され、複数のトランザクションで共有されることを意図したもの)を、現在のトランザクションのコンテキストで利用できるように変換します。 - 返される
*Stmtは、元のstmtと同じクエリを使用しますが、実行は現在のトランザクションのコネクション上で行われます。 - コミットメッセージのTODOコメントにもあるように、この初期実装では、内部的に元のステートメントのクエリをトランザクションのコネクション上で「再プリペア」しています。これは最適化の余地があることを示唆しており、将来的にはコネクションごとのプリペアドステートメントのキャッシュが検討されていました。しかし、APIのセマンティクスとしては、既存のステートメントをトランザクションに「アタッチ」する役割を果たします。
- このメソッドは、既存の
Stmt構造体への変更
Stmt構造体には、エラーを保持するためのstickyErr errorフィールドが追加されました。これは、Tx.Stmtがエラーを返す可能性がある場合に、そのエラーをStmtオブジェクト自体に保持し、後続のExecやQuery呼び出しでそのエラーを返すためのメカニズムです。これにより、Tx.Stmtの呼び出し元は、返された*Stmtオブジェクトが有効かどうかをすぐに確認でき、エラーハンドリングが容易になります。
エラーハンドリングの改善
Stmt.connStmt()およびStmt.Close()メソッドにstickyErrのチェックが追加されました。これにより、Tx.Stmtでエラーが発生した場合、その後のステートメント操作は即座にstickyErrを返すようになり、無効なステートメントでの操作を防ぎます。
テストケースの追加
TestTxStmtという新しいテストケースが追加され、Tx.Stmtの基本的な機能が検証されています。このテストでは、DB.Prepareでステートメントを作成し、それをトランザクション内でtx.Stmt(stmt).Exec()として使用し、最終的にトランザクションをコミットする一連の流れが確認されています。
コアとなるコードの変更箇所
src/pkg/exp/sql/sql.go
Tx.Prepareメソッドのコメントが更新され、Tx.Stmtへの参照が追加されました。また、将来的な最適化に関するTODOコメントが詳細化されました。- 新しいメソッド
func (tx *Tx) Stmt(stmt *Stmt) *Stmtが追加されました。- このメソッドは、
tx.db != stmt.dbの場合にエラーを返すことで、異なるデータベースからのステートメントの使用を防ぎます。 tx.grabConn()でトランザクションのコネクションを取得します。ci.Prepare(stmt.query)を呼び出して、トランザクションのコネクション上で元のステートメントのクエリを再プリペアします。- 新しい
*Stmtオブジェクトを構築し、tx、si(トランザクション固有のドライバーステートメント)、query、そして発生したエラー(stickyErr)を設定して返します。
- このメソッドは、
Stmt構造体にstickyErr errorフィールドが追加されました。Stmt.connStmt()メソッドに、s.stickyErr != nilの場合に即座にエラーを返すチェックが追加されました。Stmt.Close()メソッドに、s.stickyErr != nilの場合に即座にエラーを返すチェックが追加されました。
src/pkg/exp/sql/sql_test.go
TestDb関数がTestExecにリネームされました。- 新しいテスト関数
func TestTxStmt(t *testing.T)が追加されました。- このテストは、
db.Prepareでプリペアドステートメントを作成し、db.Begin()でトランザクションを開始し、tx.Stmt(stmt).Exec()でトランザクション内でステートメントを実行し、tx.Commit()でトランザクションをコミットする一連の操作を検証します。
- このテストは、
コアとなるコードの解説
Tx.Stmtメソッドのロジック
func (tx *Tx) Stmt(stmt *Stmt) *Stmt {
// TODO(bradfitz): optimize this. Currently this re-prepares
// each time. This is fine for now to illustrate the API but
// we should really cache already-prepared statements
// per-Conn. See also the big comment in Tx.Prepare.
if tx.db != stmt.db {
return &Stmt{stickyErr: errors.New("sql: Tx.Stmt: statement from different database used")}
}
ci, err := tx.grabConn()
if err != nil {
return &Stmt{stickyErr: err}
}
defer tx.releaseConn()
si, err := ci.Prepare(stmt.query) // ここで再プリペアが行われる
return &Stmt{
db: tx.db,
tx: tx,
txsi: si, // トランザクション固有のドライバーステートメント
query: stmt.query,
stickyErr: err,
}
}
このメソッドは、既存のStmtオブジェクト(stmt)を引数として受け取ります。
- データベースの一致チェック:
tx.db != stmt.dbで、トランザクションが属するデータベースと、渡されたステートメントが作成されたデータベースが同じであることを確認します。異なる場合はエラーを含むStmtを返します。これは、異なるデータベースインスタンス間でステートメントを誤って使用するのを防ぐための安全策です。 - コネクションの取得:
tx.grabConn()を呼び出して、現在のトランザクションが使用するデータベースコネクション(driver.Connインターフェース)を取得します。トランザクションは特定のコネクションに紐付けられるため、そのコネクション上で操作を行う必要があります。 - コネクションの解放:
defer tx.releaseConn()により、メソッドの終了時にコネクションが適切に解放されるようにします。 - ステートメントの再プリペア:
ci.Prepare(stmt.query)がこのメソッドの核心です。ここで、元のステートメントが持っていたクエリ文字列(stmt.query)を、現在のトランザクションが使用しているコネクション(ci)上で再度プリペアします。これにより、返されるStmtは、このトランザクションのコンテキストで有効なプリペアドステートメントとなります。 - 新しい
Stmtオブジェクトの構築: 新しいStmtオブジェクトが作成され、そのフィールドが設定されます。db: 元のデータベースオブジェクト。tx: 現在のトランザクションオブジェクト。これにより、このStmtがトランザクションに紐付けられていることが示されます。txsi: トランザクション固有のドライバーステートメント。これは、ci.Prepareによって返されたものです。query: 元のクエリ文字列。stickyErr:ci.Prepareでエラーが発生した場合、そのエラーがここに格納されます。このエラーは、後続のExecやQuery呼び出しで即座に返されるようになります。
この「再プリペア」の動作は、コメントにもあるように、将来的な最適化の対象でした。理想的には、コネクションが既にそのクエリをプリペアしている場合は、再プリペアせずに既存のプリペアドステートメントを再利用するメカニズムが望ましいとされていました。しかし、APIのセマンティクスを確立し、既存のステートメントをトランザクションで利用可能にするという目的は、この実装で達成されています。
Stmt構造体とエラーハンドリング
Stmt構造体にstickyErr errorが追加されたことで、Tx.Stmtがエラーを返した場合でも、そのエラーをStmtオブジェクト自体に埋め込むことができるようになりました。これにより、以下のようなコードパターンが可能になります。
updateMoney, err := db.Prepare("UPDATE balance SET money=money+? WHERE id=?")
// ... エラーハンドリング ...
tx, err := db.Begin()
// ... エラーハンドリング ...
// Tx.Stmtがエラーを返す可能性がある
txStmt := tx.Stmt(updateMoney)
// txStmt.Exec()を呼び出す前にエラーチェックを忘れても、
// Exec内部でstickyErrがチェックされるため安全
res, err := txStmt.Exec(123.45, 98293203)
if err != nil {
// ここでTx.Stmtで発生したエラーも捕捉できる
// またはExec自体のエラーも捕捉できる
fmt.Println("Exec error:", err)
}
これは、Goのエラーハンドリングの慣習に沿ったもので、エラーを早期に伝播させるための堅牢なメカニズムを提供します。
関連リンク
- Go
database/sqlパッケージのドキュメント (現在のバージョン): https://pkg.go.dev/database/sql - Go
database/sqlパッケージのチュートリアル (Go Wiki): https://go.dev/wiki/SQLDrivers - Go
database/sqlパッケージの設計に関する議論 (当時のメーリングリストなど):golang.org/cl/5433059(このコミットのGerritレビュー): https://golang.org/cl/5433059
参考にした情報源リンク
- Go言語の公式ドキュメントおよびGo Wiki
- データベースのプリペアドステートメントとトランザクションに関する一般的な知識
- Gitコミット履歴と差分
exp/sqlパッケージの当時のソースコードdatabase/sqlパッケージの現在の実装(進化の比較のため)