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

[インデックス 15229] ファイルの概要

このコミットは、Go言語の標準ライブラリである database/sql パッケージに、オプションの driver.Queryer インターフェースを追加するものです。これにより、データベースドライバがSQLクエリを実行する際に、事前にステートメントを準備(プリペア)することなく直接クエリを実行できるようになります。これは、既存の driver.Execer インターフェースと同様の目的を持ち、特定のシナリオでのパフォーマンス向上やドライバの実装の柔軟性を提供します。

コミット

commit 2968e239b00e3cfa4b9f146b00f06b01134ae5a1
Author: Julien Schmidt <google@julienschmidt.com>
Date:   Wed Feb 13 15:25:39 2013 -0800

    database/sql: Add an optional Queryer-Interface (like Execer)
    
    Completly the same like the Execer-Interface, just for Queries.
    This allows Drivers to execute Queries without preparing them first
    
    R=golang-dev, bradfitz
    CC=golang-dev
    https://golang.org/cl/7085056

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/2968e239b00e3cfa4b9f146b00f06b01134ae5a1

元コミット内容

このコミットは、Goの database/sql パッケージに Queryer という新しいインターフェースを導入します。これは、Execer インターフェースと同様に、ドライバがクエリを直接実行するためのオプションのメカニズムを提供します。具体的には、DB.Query メソッドが内部でステートメントをプリペアすることなく、ドライバが Queryer インターフェースを実装していれば、その Query メソッドを直接呼び出すことができるようになります。これにより、プリペアのオーバーヘッドを回避し、特に一度しか実行されないようなクエリのパフォーマンスを向上させることが期待されます。

変更の背景

database/sql パッケージの従来の動作では、DB.QueryDB.Exec のような高レベルの関数を呼び出すと、内部的に以下のステップが実行されていました。

  1. Prepare メソッドを呼び出して、SQLステートメントをプリペアする。
  2. プリペアされたステートメントに対して Exec または Query を実行する。
  3. ステートメントをクローズする。

この「プリペア -> 実行 -> クローズ」のサイクルは、同じステートメントを複数回実行する場合(例えば、ループ内で同じINSERT文を異なる値で実行する場合)には非常に効率的です。プリペアされたステートメントはデータベースサーバー側で最適化され、再利用されるため、パースや最適化のコストを削減できます。

しかし、一度しか実行されないような単純なクエリ(例: SELECT * FROM users WHERE id = 1)の場合、プリペアのステップは不要なオーバーヘッドとなる可能性があります。データベースによっては、プリペア自体がネットワークラウンドトリップやサーバー側のリソース消費を伴うため、これをスキップできるとパフォーマンスが向上します。

このコミットは、このようなシナリオに対応するために、ドライバがプリペアなしで直接クエリを実行できる Queryer インターフェースを導入しました。これにより、ドライバ開発者は、特定のデータベースの特性やクエリの性質に応じて、より効率的な実行パスを選択できるようになります。

前提知識の解説

Go言語の database/sql パッケージ

database/sql パッケージは、Go言語でSQLデータベースを操作するための汎用的なインターフェースを提供します。このパッケージ自体は特定のデータベースの実装を含まず、データベースドライバを介して実際のデータベースとの通信を行います。これにより、アプリケーションコードは特定のデータベースシステムに依存することなく、統一されたAPIでデータベースを操作できます。

主要な概念:

  • DB: データベースへの接続プールを表します。Open 関数で作成され、複数のゴルーチンから安全に利用できます。
  • Conn: データベースへの単一の接続を表します。通常、DB から取得され、トランザクションやステートメントの実行に使用されます。database/sql パッケージは、Conn が単一のゴルーチンによってのみ使用されることを保証します。
  • Stmt: プリペアされたSQLステートメントを表します。SQLインジェクション攻撃を防ぎ、同じクエリを複数回効率的に実行するために使用されます。
  • Rows: クエリ結果の行セットを表します。Next() メソッドで次の行に進み、Scan() メソッドで列の値をGoの変数に読み込みます。
  • Result: INSERT, UPDATE, DELETE などのDML操作の結果を表します。LastInsertId()RowsAffected() メソッドを提供します。

driver パッケージ

database/sql/driver パッケージは、database/sql パッケージがデータベースドライバと通信するためのインターフェースを定義します。データベースドライバは、これらのインターフェースを実装することで、database/sql パッケージと連携します。

主要なインターフェース:

  • driver.Driver: データベースドライバのエントリポイント。Open メソッドを持ち、データベースへの新しい接続 (driver.Conn) を作成します。
  • driver.Conn: データベースへの単一の接続を表します。Prepare (ステートメントのプリペア) や Begin (トランザクションの開始) などのメソッドを持ちます。
  • driver.Stmt: プリペアされたステートメントを表します。Exec (DML操作) や Query (SELECT操作) などのメソッドを持ちます。
  • driver.Execer: driver.Conn がオプションで実装できるインターフェース。これを実装すると、DB.Exec が内部で Prepare を呼び出すことなく、直接 Exec メソッドを呼び出すことができます。これにより、プリペアのオーバーヘッドを回避できます。
  • driver.Value: Goの型とデータベースの型の間で値を変換するためのインターフェース。

プリペアードステートメントの利点と欠点

利点:

  • SQLインジェクション攻撃の防止: パラメータがクエリ文字列とは別に渡されるため、悪意のある入力がSQLコードとして解釈されるのを防ぎます。
  • パフォーマンスの向上: 同じクエリを複数回実行する場合、データベースサーバーはクエリを一度だけパース、コンパイル、最適化し、その実行計画をキャッシュできます。これにより、後続の実行が高速になります。
  • ネットワークトラフィックの削減: クエリ文字列を一度だけ送信し、その後はパラメータのみを送信することで、ネットワークトラフィックを削減できます。

欠点:

  • オーバーヘッド: 一度しか実行されないクエリの場合、プリペアのステップ自体がオーバーヘッドとなる可能性があります。プリペアには、データベースサーバーとの追加のラウンドトリップや、サーバー側のリソース消費が伴うことがあります。
  • リソース管理: プリペアされたステートメントは、データベースサーバー側でリソース(メモリ、カーソルなど)を消費します。適切にクローズされないと、リソースリークにつながる可能性があります。

技術的詳細

このコミットの核心は、database/sql/driver パッケージに Queryer インターフェースを導入し、database/sql パッケージがこのインターフェースを認識して利用するように変更した点です。

driver.Queryer インターフェースの定義

src/pkg/database/sql/driver/driver.go に以下のインターフェースが追加されました。

// Queryer is an optional interface that may be implemented by a Conn.
//
// If a Conn does not implement Queryer, the db package's DB.Query will
// first prepare a query, execute the statement, and then close the
// statement.
//
// Query may return ErrSkip.
type Queryer interface {
	Query(query string, args []Value) (Rows, error)
}

このインターフェースは、driver.Conn が実装できるオプションのインターフェースです。Query メソッドは、SQLクエリ文字列と driver.Value 型の引数を受け取り、driver.Rows とエラーを返します。

重要なのは、コメントにある Query may return ErrSkip です。これは、ドライバが Queryer インターフェースを実装しているものの、特定のクエリや状況では直接実行できない場合に driver.ErrSkip を返すことができることを意味します。database/sql パッケージは ErrSkip を受け取ると、フォールバックとして従来の「プリペア -> 実行 -> クローズ」のパスを使用します。

DB.Query の変更

src/pkg/database/sql/sql.goDB.Query メソッドが大幅に変更され、新しい Queryer インターフェースを利用するようになりました。

変更後の DB.Query の内部動作は以下のようになります。

  1. db.conn() を呼び出して、データベース接続 (driver.Conn) を取得します。
  2. 取得した driver.Conndriver.Queryer インターフェースを実装しているかチェックします (if queryer, ok := ci.(driver.Queryer); ok { ... })。
  3. Queryer を実装している場合:
    • 引数を driver.Value 型に変換します。
    • queryer.Query(query, dargs) を直接呼び出します。
    • もし queryer.Querydriver.ErrSkip を返さなかった場合(つまり、クエリが直接実行された場合)、その結果 (driver.Rows) を使用して *Rows オブジェクトを作成し、返します。
    • エラーが発生した場合、接続を解放し、エラーを返します。
  4. Queryer を実装していない、または Queryer.Querydriver.ErrSkip を返した場合:
    • 従来の動作に戻り、ci.Prepare(query) を呼び出してステートメントをプリペアします。
    • プリペアされたステートメントに対して Query を実行します。
    • 結果 (driver.Rows) を使用して *Rows オブジェクトを作成し、返します。この際、closeStmt フィールドにプリペアされたステートメントを保持し、Rows がクローズされるときにステートメントもクローズされるようにします。

このロジックにより、database/sql パッケージは、ドライバが Queryer をサポートしていればその最適化パスを優先し、サポートしていなければ安全に従来のパスにフォールバックする、という柔軟な動作を実現しています。

Tx.Query の変更

同様に、src/pkg/database/sql/sql.goTx.Query メソッドも変更され、DB.Query と同じく queryConn ヘルパー関数を呼び出すようになりました。これにより、トランザクション内のクエリも Queryer インターフェースの恩恵を受けられるようになります。

fakedb_test.go の変更

テスト目的で、fakedb_test.go にある fakeConn 構造体に Query メソッドが追加されました。この実装は常に driver.ErrSkip を返すため、fakeConnQueryer インターフェースを実装しているにもかかわらず、database/sql パッケージがフォールバックパス(プリペア経由)を使用することを確認できます。これは、Queryer インターフェースのオプション性と ErrSkip の動作をテストするために重要です。

コアとなるコードの変更箇所

このコミットで変更された主要なファイルは以下の通りです。

  1. src/pkg/database/sql/driver/driver.go:

    • Queryer インターフェースが新しく定義されました。
  2. src/pkg/database/sql/fakedb_test.go:

    • fakeConn 構造体に Query メソッドが追加され、driver.Queryer インターフェースを実装しました。このメソッドは常に driver.ErrSkip を返します。
  3. src/pkg/database/sql/sql.go:

    • DB.Query メソッドが大幅にリファクタリングされ、driver.Queryer インターフェースの利用を優先するロジックが追加されました。
    • queryConn という新しいヘルパー関数が導入され、DB.QueryTx.Query の両方から呼び出されるようになりました。
    • Tx.Query メソッドも queryConn を利用するように変更されました。
    • Stmt.Query メソッドから一部のロジックが rowsiFromStatement という新しいヘルパー関数に抽出されました。
    • Rows 構造体の closeStmt フィールドの型が *Stmt から driver.Stmt に変更されました。これは、Queryer パスでプリペアされたステートメントがない場合でも、closeStmt が適切に扱われるようにするためです。

コアとなるコードの解説

src/pkg/database/sql/driver/driver.go

type Queryer interface {
	Query(query string, args []Value) (Rows, error)
}

このコードは、新しい Queryer インターフェースを定義しています。このインターフェースは、driver.Conn が実装することで、database/sql パッケージがプリペアステップをスキップして直接クエリを実行できるようになります。Query メソッドは、SQLクエリ文字列と引数のスライスを受け取り、結果セット (Rows) とエラーを返します。

src/pkg/database/sql/sql.goDB.QueryqueryConn

func (db *DB) Query(query string, args ...interface{}) (*Rows, error) {
	var rows *Rows
	var err error
	for i := 0; i < 10; i++ { // リトライロジック
		rows, err = db.query(query, args)
		if err != driver.ErrBadConn {
			break
		}
	}
	return rows, err
}

func (db *DB) query(query string, args []interface{}) (*Rows, error) {
	ci, err := db.conn() // 接続を取得
	if err != nil {
		return nil, err
	}

	releaseConn := func(err error) { db.putConn(ci, err) } // 接続解放用のクロージャ

	return db.queryConn(ci, releaseConn, query, args) // 実際のクエリ実行ロジック
}

func (db *DB) queryConn(ci driver.Conn, releaseConn func(error), query string, args []interface{}) (*Rows, error) {
	if queryer, ok := ci.(driver.Queryer); ok { // Queryer インターフェースを実装しているかチェック
		dargs, err := driverArgs(nil, args) // 引数を driver.Value に変換
		if err != nil {
			releaseConn(err)
			return nil, err
		}
		rowsi, err := queryer.Query(query, dargs) // Queryer の Query メソッドを直接呼び出し
		if err != driver.ErrSkip { // ErrSkip でなければ成功
			if err != nil {
				releaseConn(err)
				return nil, err
			}
			// Rows オブジェクトを作成し、接続の所有権を Rows に渡す
			rows := &Rows{
				db:          db,
				ci:          ci,
				releaseConn: releaseConn,
				rowsi:       rowsi,
			}
			return rows, nil
		}
	}

	// Queryer を実装していない、または ErrSkip が返された場合のフォールバックパス
	sti, err := ci.Prepare(query) // ステートメントをプリペア
	if err != nil {
		releaseConn(err)
		return nil, err
	}

	rowsi, err := rowsiFromStatement(sti, args...) // プリペアされたステートメントでクエリ実行
	if err != nil {
		releaseConn(err)
		sti.Close() // エラー時はステートメントもクローズ
		return nil, err
	}

	// Rows オブジェクトを作成し、接続とステートメントの所有権を Rows に渡す
	rows := &Rows{
		db:          db,
		ci:          ci,
		releaseConn: releaseConn,
		rowsi:       rowsi,
		closeStmt:   sti, // プリペアされたステートメントを保持
	}
	return rows, nil
}

DB.Query は、内部で db.query を呼び出し、さらに db.queryConn を呼び出すように変更されました。queryConn 関数が、driver.Queryer インターフェースの実装をチェックし、もし実装されていればその Query メソッドを直接呼び出します。driver.ErrSkip が返された場合や、Queryer が実装されていない場合は、従来の Prepare を経由するパスにフォールバックします。これにより、ドライバはプリペアのオーバーヘッドを回避できる選択肢を持つことになります。

src/pkg/database/sql/sql.goTx.Query

func (tx *Tx) Query(query string, args ...interface{}) (*Rows, error) {
	ci, err := tx.grabConn() // トランザクションから接続を取得
	if err != nil {
		return nil, err
	}

	releaseConn := func(err error) { tx.releaseConn() } // 接続解放用のクロージャ

	return tx.db.queryConn(ci, releaseConn, query, args) // DB の queryConn を利用
}

Tx.QueryDB.Query と同様に、db.queryConn ヘルパー関数を利用するように変更されました。これにより、トランザクション内で実行されるクエリも、ドライバが Queryer インターフェースを実装していれば、プリペアなしで直接実行される可能性が生まれます。

src/pkg/database/sql/sql.goRows 構造体

type Rows struct {
	// ... (既存のフィールド)
	closeStmt driver.Stmt // if non-nil, statement to Close on close
}

Rows 構造体の closeStmt フィールドの型が *Stmt から driver.Stmt に変更されました。これは、Queryer パスでクエリが実行された場合、Stmt オブジェクトが作成されないため、closeStmtnil になることを許容し、かつ、プリペアパスで作成された driver.Stmt を保持できるようにするためです。Rows がクローズされる際に、この closeStmtnil でなければクローズされます。

関連リンク

参考にした情報源リンク