初期の段階から、.NETの中核的機能は、ADO.NETあるいはSystem.Dataと呼ばれるデータベースアクセスフレームワークだった。このフレームワークの目的は、すべてのデータベースに対して、一貫性のある方法でアクセスできるようにすることだ。しかしながら、対処の必要なギャップが存在することもある。
そうしたギャップのひとつが、複数のSQLステートメントを単一バッチで送信する機能だ。SQL Serverなど一部のデータベースは、ステートメントをセミコロンで区切ることが可能だが、すべてのデータベースで使用できる訳ではない。Shay Rojansky氏によると、
このアプローチの問題は、大部分のデータベースはステートメント毎にプロトコルメッセージを分割する必要がある(PostgreSQL、MySQLなど)ため、データベースのADO.NETプロバイダがSQLをクライアント上で解析して、セミコロンで分割しなければならない点にあります。この方法は信頼性が低い(SQLの解析は難しいので)上に、パフォーマンスもよくありません — 理想としては、ADO.NETプロバイダはユーザの提供するSQLを解析したり書き直したりせず、データベースへ単に送信するべきなのです。
さらにSQL Serverであっても、単一バッチでの複数のストアドプロシージャ呼び出しがサポートされない、などの制限がある。
これに対する.NET 6のソリューションがDbBatchクラスである。提案されているバッチAPIの目標は、次のようなものだ。
- クライアント側でのSQL解析を必要とせず、1回のラウンドトリップで複数のSQLステートメントを実行可能な構造的方法を提供すること。
- APIとして、他のADO.NET APIとの一貫性を維持すること。特に
DbCommand
には(いずれも"実行ファイル"である)近いものにすることで、導入時の概念的な複雑性を軽減すること。 - さまざまな形式のステートメント(挿入、更新、選択)を同じバッチ内に混在可能にすること。現状の連結方式のアプローチではこれが可能であるので、我々のリーダAPI(複数のリザルトセット)も同じにすること。
- バッチ内の個々のコマンドに対して、影響する行数に対する非集計アクセス手段を提供すること。
DbBatchを直接使用する
このモデルでは、最初にデータベース固有のDbBatchクラスを作成する。例えばターゲットがPostgreSQLならば、NpgsqlBatch
のインスタンスを生成する。MySQLならば、クラス名はMySqlBatch
になる。(汎用性のあるプログラムの場合は、DbProviderFactoryを使って適切なDbBatchサブクラスを生成することも可能だ。)
そのDbBatch
クラスの上にコネクション、必要であればトランザクションのオブジェクトを追加し、さらにDbBatchCommand
オブジェクトのリストを追加する。
DbBatchCommand
はDbCommand
のスリム化バージョンである。基本的にはSQL文字列とパラメータのコレクションに過ぎず、それ以外はDbBatch
レベルで処理される。
この時点で、DbBatch
の実行(execute)メソッドが呼び出し可能になる。基本的な選択肢は3つだ。
ExecuteNonQuery
: 影響を受けた行数を返す。ExecuteScalar
: 最初のリザルトセットの最初の行から、最初のカラムを返す。ExecuteReader
: リザルトセットすべてを順序付きで返す。
ExecuteReader
を使用した場合、DbDataReader.NextResult
をコールするまでは、最初のリザルトセットのみが参照可能である。コールした時点で最初のリザルトセットは失われ、次のものが参照できるようになる。すべてのリザルトセットが処理されるまで繰り返すと、NextResult
がfalseを返す。
DbBatch
の実行後は、各DbBatchCommand
のRecordAffected
プロパティがセットされている。ただし、正確な設定タイミングは実装依存であり、すべてのリザルトセットが処理されるまで遅延する場合もある。
DbBatchとORM
大部分のORMは、効率上の理由からバッチで処理する必要がある。従って理論上は、DbBatch APIにスイッチすることで、パフォーマンスの向上が期待できるはずだ。この変更はORMの外部からは見えず、内部で行われるので、アプリケーション開発者にとっては無償のパフォーマンス向上ということになる。
PostgreSQL実装
公式の.NET用PostgreSQLドライバがアップデートされ、バッチAPIがサポートされるようになった。この変更に伴い、raw SQLモードオプションが追加された。
上述のように、PostgreSQLドライバは、SQL文字列からセミコロンを検索して、バッチを単一ステートメントに変換する必要がある。この解析には大きなコストを要する。文字列リテラルに含まれるセミコロンなどを正しく処理しなければならないからだ。
名前付きパラメータの問題にも対処する必要がある。PsotgreSQlは名前付きパラメータをネイティブにはサポートしていないので、名前付きパラメータを位置指定パラメータに変換する処理もクライアント側のパーザがしなくてはならない。
raw SQLモードにスイッチすると、こういった解析処理は無効になる。これによって、ある程度のパフォーマンス改善は得られるが、NpgsqlCommand経由でバッチを利用することはできなくなる。パフォーマンスに関する懸念としては、こちらの方が大きい。
NpsqlBatchによる明示的なバッチ処理が可能になれば、raw SQLモードもオプションとして意味のあるものになる。このモードを使う場合は、パラメータに名前を付けてはならない。
パラメータを使用しないのであれば、以下のコマンドのいずれかを使用する必要がある。
AppContext.SetSwitch("Npgsql.EnableSqlRewriting", false);
cmd.Parameters.Add(new() { Value = 0 });
AppContextを使う方法では、設定はグローバルに変更される。
MySQL実装
MySQLの実装はMySqlConnectorの一部として提供される。このライブラリは一般的に、Oracleが提供する公式版に優ると評価されており、MariaDBもサポートする。
MySqlBatch機能は、実際には2019年に完成していたのだが、対応するフレームワークAPIが.NET Core 3から削除されていたため、あまり注目されていなかった。最終的に.NET 6でリリースされた時の、唯一の重大な変更は、対応する基本クラスからの継承としてマークされたことだ。
SQL Server実装
この機能のSQL Server版は、他の実装よりも遅れている。計画は2018年にスタートしたのだが、重要な決定がまだ行われていないのだ。12月の最新版からは、管理面で問題のあることが見て取れる。
APIはnet 6内にあるので、私たちに必要なのはnet6ビルドターゲットです。APIに対するMS側からの方針が決定すれば、基本クラスが使用できない他のビルドでも使用できるようになるでしょう。
実装に関する限りにおいては、すでに基本機能の多くが用意されている。SqlCommandSetという、同じような内部クラスも長い間使用されてきたが、このクラスにはSqlDataAdapter経由でのみ間接的にアクセス可能であるため、大部分の開発者はその存在さえ知らないのが実情だ。
開発の進捗状況は、"SqlClient: Implement optimized version of the new ADO.NET batching API"というチケットで確認することができる。