Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

[インデックス 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)

プリペアドステートメントは、データベース操作の効率とセキュリティを向上させるための重要な概念です。

  1. 準備 (Prepare): SQLクエリのテンプレートをデータベースに送信し、データベース側でそのクエリを解析、コンパイル、最適化します。この段階では、具体的な値(パラメータ)はプレースホルダー(例: ?$1)で示されます。
  2. 実行 (Execute): 準備されたステートメントに対して、具体的なパラメータ値をバインドして実行します。この際、データベースはクエリを再解析することなく、準備済みの実行計画を再利用できます。

利点:

  • セキュリティ: パラメータ値がクエリ文字列に直接埋め込まれないため、SQLインジェクション攻撃を防ぐことができます。
  • パフォーマンス: クエリの解析と最適化が一度だけ行われるため、同じクエリを複数回実行する場合にオーバーヘッドが削減されます。
  • コードの可読性: クエリとパラメータが分離され、コードが読みやすくなります。

Go言語における並行処理 (Concurrency)

Go言語は、ゴルーチン(goroutine)とチャネル(channel)という軽量な並行処理のプリミティブを提供します。

  • ゴルーチン: Goランタイムによって管理される軽量なスレッドのようなものです。go キーワードを使って関数を呼び出すことで、新しいゴルーチンを起動できます。
  • チャネル: ゴルーチン間で値を安全に送受信するための通信メカニズムです。チャネルを使用することで、共有メモリを直接操作することなく、ゴルーチン間の同期と通信を行うことができます。

並行処理を行う際には、共有リソースへのアクセスを適切に同期させることが重要です。同期メカニズム(ミューテックス、セマフォなど)を使用しないと、競合状態が発生し、プログラムの動作が予測不能になる可能性があります。

技術的詳細

このコミットの技術的な核心は、database/sql パッケージの *Stmt オブジェクトが、Goの並行処理モデルにおいてどのように振る舞うべきかを明確にすることにあります。

DB.Prepare メソッドは、SQLクエリ文字列を受け取り、データベースドライバを通じてそのクエリを準備します。この準備されたクエリは、*Stmt 型のオブジェクトとして返されます。この *Stmt オブジェクトは、Query, QueryRow, Exec などのメソッドを持ち、これらを通じて実際のデータベース操作を実行します。

変更前は、*Stmt が複数のゴルーチンから同時に QueryExec などのメソッドを呼び出された場合に、内部的にどのような同期が行われるのか、あるいは全く行われないのかがドキュメントで不明瞭でした。もし *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つのファイルにあります。

  1. 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
    
  2. 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 の並行利用の安全性を検証するために追加されたテストです。

  1. テストデータベースの準備: newTestDB を使用してテスト用のデータベース接続を確立し、defer closeDB でテスト終了時に接続を閉じます。
  2. プリペアドステートメントの準備: db.Prepare("SELECT|people|age|name=?") を呼び出して、name パラメータに基づいて age を選択するプリペアドステートメントを作成します。これはテスト用のモックデータベースに対するクエリであり、実際のSQLとは異なる形式であることに注意してください。
  3. 並行実行:
    • 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 に送信します。
  4. 結果の検証:
    • for i := 0; i < n; i++ ループで、チャネル ch から10個の結果を待ち受けます。
    • もしエラーが受信された場合、t.Error(err) を呼び出してテストを失敗させます。

このテストは、複数のゴルーチンが同時に同じプリペアドステートメントを使用しても、競合状態が発生せず、すべてのクエリが正しく実行され、期待通りの結果が返されることを確認します。これにより、*Stmt が並行利用に対して安全であるというドキュメントの記述が、実際のコードの振る舞いによって裏付けられます。

関連リンク

参考にした情報源リンク