[インデックス 15347] ファイルの概要
このコミットは、Go言語の標準ライブラリである database/sql
パッケージにおける DB.Prepare
メソッドのドキュメントを明確にし、その振る舞いを検証するためのテストを追加するものです。具体的には、Prepare
メソッドが返す *Stmt
オブジェクトが並行利用に対して安全であることを明示し、その安全性を保証するテストケースが追加されています。
コミット
commit c53fab969c31e3f15306a5b5b714928d2fd6b1df
Author: Brad Fitzpatrick <bradfitz@golang.org>
Date: Wed Feb 20 22:15:36 2013 -0800
database/sql: clarify that DB.Prepare's stmt is safe for concurrent use
And add a test too, for Alex. :)
Fixes #3734
R=golang-dev, adg
CC=golang-dev
https://golang.org/cl/7399046
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/c53fab969c31e3f15306a5b5b714928d2fd6b1df
元コミット内容
database/sql: clarify that DB.Prepare's stmt is safe for concurrent use
And add a test too, for Alex. :)
Fixes #3734
このコミットは、database/sql
パッケージの DB.Prepare
メソッドによって返されるステートメント(*Stmt
)が、複数のゴルーチンから同時に安全に使用できることを明確にするものです。また、この並行利用の安全性を検証するためのテストが追加されています。コミットメッセージには「Fixes #3734」とありますが、Go言語の公式リポジトリの公開Issueトラッカーでは、この番号のIssueが直接この変更に関連する内容として見つかりませんでした。これは、内部的なトラッキング番号であるか、あるいは別のプロジェクトのIssueを参照している可能性があります。
変更の背景
Go言語の database/sql
パッケージは、データベース操作のための汎用的なインターフェースを提供します。Prepare
メソッドは、SQLステートメントをプリコンパイル(準備)し、その後の複数回の実行でパフォーマンスを向上させるために使用されます。しかし、この準備されたステートメント(*Stmt
)が複数のゴルーチンから同時に使用された場合に安全であるかどうかが、ドキュメント上で明確ではありませんでした。
並行処理がGo言語の重要な特徴であるため、開発者は *Stmt
オブジェクトを複数のゴルーチン間で共有し、同時にクエリを実行したいと考えるのが自然です。もし *Stmt
が並行利用に対して安全でない場合、競合状態(race condition)が発生し、予期せぬ結果やデータ破損につながる可能性があります。
このコミットは、この曖昧さを解消し、*Stmt
が並行利用に対して安全であることを明示することで、開発者が安心して並行処理を実装できるようにすることを目的としています。また、その安全性を保証するためのテストを追加することで、将来的な回帰バグを防ぐ狙いもあります。コミットメッセージにある「for Alex」は、おそらくGoチームのメンバーであるAlex Brainman氏からの要望や議論があったことを示唆しています。
前提知識の解説
Go言語の database/sql
パッケージ
database/sql
パッケージは、GoプログラムからSQLデータベースにアクセスするための標準インターフェースを提供します。このパッケージは、特定のデータベースドライバに依存しない抽象化レイヤーであり、MySQL、PostgreSQL、SQLiteなど、様々なデータベースに対応するドライバをプラグインとして利用できます。
主要なコンポーネントは以下の通りです。
DB
: データベースへの接続プールを表します。通常、アプリケーションの起動時に一度だけ作成され、アプリケーション全体で共有されます。driver
インターフェース: データベースドライバが実装すべきインターフェースを定義します。Stmt
: 準備されたステートメント(Prepared Statement)を表します。SQLクエリを一度準備(プリコンパイル)し、パラメータを変えて複数回実行するために使用されます。これにより、SQLインジェクション攻撃を防ぎ、クエリの解析オーバーヘッドを削減し、パフォーマンスを向上させることができます。Tx
: トランザクションを表します。複数のデータベース操作をアトミックに実行するために使用されます。
プリペアドステートメント (Prepared Statement)
プリペアドステートメントは、データベース操作の効率とセキュリティを向上させるための重要な概念です。
- 準備 (Prepare): SQLクエリのテンプレートをデータベースに送信し、データベース側でそのクエリを解析、コンパイル、最適化します。この段階では、具体的な値(パラメータ)はプレースホルダー(例:
?
や$1
)で示されます。 - 実行 (Execute): 準備されたステートメントに対して、具体的なパラメータ値をバインドして実行します。この際、データベースはクエリを再解析することなく、準備済みの実行計画を再利用できます。
利点:
- セキュリティ: パラメータ値がクエリ文字列に直接埋め込まれないため、SQLインジェクション攻撃を防ぐことができます。
- パフォーマンス: クエリの解析と最適化が一度だけ行われるため、同じクエリを複数回実行する場合にオーバーヘッドが削減されます。
- コードの可読性: クエリとパラメータが分離され、コードが読みやすくなります。
Go言語における並行処理 (Concurrency)
Go言語は、ゴルーチン(goroutine)とチャネル(channel)という軽量な並行処理のプリミティブを提供します。
- ゴルーチン: Goランタイムによって管理される軽量なスレッドのようなものです。
go
キーワードを使って関数を呼び出すことで、新しいゴルーチンを起動できます。 - チャネル: ゴルーチン間で値を安全に送受信するための通信メカニズムです。チャネルを使用することで、共有メモリを直接操作することなく、ゴルーチン間の同期と通信を行うことができます。
並行処理を行う際には、共有リソースへのアクセスを適切に同期させることが重要です。同期メカニズム(ミューテックス、セマフォなど)を使用しないと、競合状態が発生し、プログラムの動作が予測不能になる可能性があります。
技術的詳細
このコミットの技術的な核心は、database/sql
パッケージの *Stmt
オブジェクトが、Goの並行処理モデルにおいてどのように振る舞うべきかを明確にすることにあります。
DB.Prepare
メソッドは、SQLクエリ文字列を受け取り、データベースドライバを通じてそのクエリを準備します。この準備されたクエリは、*Stmt
型のオブジェクトとして返されます。この *Stmt
オブジェクトは、Query
, QueryRow
, Exec
などのメソッドを持ち、これらを通じて実際のデータベース操作を実行します。
変更前は、*Stmt
が複数のゴルーチンから同時に Query
や Exec
などのメソッドを呼び出された場合に、内部的にどのような同期が行われるのか、あるいは全く行われないのかがドキュメントで不明瞭でした。もし *Stmt
の内部状態が複数のゴルーチンによって同時に変更される可能性があり、かつ適切なロックメカニズムが提供されていない場合、データ破損やクラッシュといった深刻な問題を引き起こす可能性があります。
このコミットでは、DB.Prepare
のドキュメントに「Multiple queries or executions may be run concurrently from the returned statement.」という記述を追加することで、*Stmt
が並行利用に対して安全である(つまり、内部で適切な同期メカニズムが実装されている)ことを明示しています。これは、database/sql
パッケージの設計思想として、*Stmt
がスレッドセーフであることを保証していることを意味します。
具体的には、*Stmt
のメソッド(Query
, QueryRow
, Exec
など)が呼び出される際、必要に応じて内部的にミューテックスなどの同期プリミティブを使用して、データベースドライバへのアクセスや内部状態の更新を保護していると考えられます。これにより、複数のゴルーチンが同時に同じ *Stmt
オブジェクトを使用しても、競合状態が発生することなく、期待通りの結果が得られるようになります。
追加されたテスト TestStatementQueryRowConcurrent
は、この並行利用の安全性を具体的に検証します。複数のゴルーチンを起動し、それぞれが同じ *Stmt
オブジェクトに対して QueryRow
メソッドを呼び出し、結果を検証します。もし *Stmt
が並行利用に対して安全でなければ、このテストは失敗するか、予測不能な動作を示すはずです。テストが成功することで、*Stmt
の並行利用の安全性が保証されます。
コアとなるコードの変更箇所
このコミットによるコードの変更は、主に以下の2つのファイルにあります。
-
src/pkg/database/sql/sql.go
:DB.Prepare
メソッドのコメントが更新されました。--- a/src/pkg/database/sql/sql.go +++ b/src/pkg/database/sql/sql.go @@ -408,7 +408,9 @@ func (db *DB) putConn(c driver.Conn, err error) { c.Close() } -// Prepare creates a prepared statement for later execution. +// Prepare creates a prepared statement for later queries or executions. +// Multiple queries or executions may be run concurrently from the +// returned statement. func (db *DB) Prepare(query string) (*Stmt, error) { var stmt *Stmt var err error
-
src/pkg/database/sql/sql_test.go
:TestStatementQueryRowConcurrent
という新しいテスト関数が追加されました。--- a/src/pkg/database/sql/sql_test.go +++ b/src/pkg/database/sql/sql_test.go @@ -273,6 +273,35 @@ func TestStatementQueryRow(t *testing.T) { } +// golang.org/issue/3734 +func TestStatementQueryRowConcurrent(t *testing.T) { + db := newTestDB(t, "people") + defer closeDB(t, db) + stmt, err := db.Prepare("SELECT|people|age|name=?") + if err != nil { + t.Fatalf("Prepare: %v", err) + } + defer stmt.Close() + + const n = 10 + ch := make(chan error, n) + for i := 0; i < n; i++ { + go func() { + var age int + err := stmt.QueryRow("Alice").Scan(&age) + if err == nil && age != 1 { + err = fmt.Errorf("unexpected age %d", age) + } + ch <- err + }() + } + for i := 0; i < n; i++ { + if err := <-ch; err != nil { + t.Error(err) + } + } +} + // just a test of fakedb itself func TestBogusPreboundParameters(t *testing.T) { db := newTestDB(t, "foo")
コアとなるコードの解説
src/pkg/database/sql/sql.go
の変更
DB.Prepare
メソッドのコメントに以下の2行が追加されました。
// Multiple queries or executions may be run concurrently from the
// returned statement.
この変更は、DB.Prepare
が返す *Stmt
オブジェクトが、複数のゴルーチンから同時にクエリや実行を行うことができる、つまり並行利用に対して安全であることを明示しています。これは、database/sql
パッケージの設計における重要な保証であり、開発者が並行処理を実装する際の混乱を避けることを目的としています。このコメントにより、開発者は *Stmt
を安心して複数のゴルーチン間で共有し、並行してデータベース操作を実行できることが明確になります。
src/pkg/database/sql/sql_test.go
の変更
TestStatementQueryRowConcurrent
関数は、*Stmt
の並行利用の安全性を検証するために追加されたテストです。
- テストデータベースの準備:
newTestDB
を使用してテスト用のデータベース接続を確立し、defer closeDB
でテスト終了時に接続を閉じます。 - プリペアドステートメントの準備:
db.Prepare("SELECT|people|age|name=?")
を呼び出して、name
パラメータに基づいてage
を選択するプリペアドステートメントを作成します。これはテスト用のモックデータベースに対するクエリであり、実際のSQLとは異なる形式であることに注意してください。 - 並行実行:
const n = 10
で、10個のゴルーチンを起動することを示します。ch := make(chan error, n)
で、各ゴルーチンからのエラーを収集するためのバッファ付きチャネルを作成します。for i := 0; i < n; i++
ループ内で、10個のゴルーチンを起動します。- 各ゴルーチンは、同じ
stmt
オブジェクトに対してstmt.QueryRow("Alice").Scan(&age)
を呼び出します。これは、Alice
という名前の人のage
を取得しようとします。 - 取得した
age
が期待値(このテストケースでは1
)と異なる場合、またはエラーが発生した場合、そのエラーをチャネルch
に送信します。
- 結果の検証:
for i := 0; i < n; i++
ループで、チャネルch
から10個の結果を待ち受けます。- もしエラーが受信された場合、
t.Error(err)
を呼び出してテストを失敗させます。
このテストは、複数のゴルーチンが同時に同じプリペアドステートメントを使用しても、競合状態が発生せず、すべてのクエリが正しく実行され、期待通りの結果が返されることを確認します。これにより、*Stmt
が並行利用に対して安全であるというドキュメントの記述が、実際のコードの振る舞いによって裏付けられます。
関連リンク
- Go言語
database/sql
パッケージのドキュメント: https://pkg.go.dev/database/sql - Go言語の並行処理に関する公式ブログ記事(例: Concurrency is not Parallelism): https://go.dev/blog/concurrency-is-not-parallelism
参考にした情報源リンク
- GitHub: https://github.com/golang/go/commit/c53fab969c31e3f15306a5b5b714928d2fd6b1df
- Go言語のIssueトラッカー(Issue #3734に関する情報は見つかりませんでしたが、一般的なIssueの検索に使用): https://github.com/golang/go/issues
- Go言語の公式ドキュメント: https://go.dev/
- Go言語のテストに関するドキュメント: https://go.dev/pkg/testing/
- プリペアドステートメントに関する一般的な情報(データベースの概念)