[インデックス 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
パッケージの現在の実装(進化の比較のため)