ADO 時代の非接続型データアクセス
さてさて、つらつらとさっき Web サイトを巡回してたら、ADO.NET では非接続型データアクセスと呼ばれるデータアクセス手法が新規に導入されたので、ADO 時代のデータアクセスとは考え方が変わっている……という話を見かけたのですが、いやいやそれは違いますよ~;、とツッコミを入れたくなったり。実は ADO の時代には、
の 2 パターンがちゃんとサポートされていた上に、さらに ADO.NET ではできない
- サーバカーソルの利用
- 複数テーブルから JOIN したデータに対する非接続型のデータ更新メカニズム
までサポートしていたりします。でも、この辺の話って重要な割にはわかりやすくまとめられている資料がない、という問題があって、VB6 や ASP で開発している人でも知らない人の方が多かったりするんですよね。というわけで、このエントリではちょっと昔を振り返って、
- ADO 時代の非接続型データアクセスがどんなものだったのか?
- ADO から ADO.NET になる際に、どのような進化があったのか?
を説明してみたいと思います。
[ADO Recordset の基本的なコーディングパターン ]
ADO を使う場合、Recordset の開き方は実に多種多様で、様々な「ショートカット的なオープン」が可能になっていました。ところがこの多種多様さゆえに Recordset の正しい開き方がわからなくなっている側面があり、結果として Recordset の誤用が多発していました。
まず、万能かつ基本的な Recordset の開き方は以下のコーディングパターンになります。(他にもさまざまなオープン方法がありますが、それらはいずれもこのコーディングパターンでカバーができます。)
Dim con As ADODB.Connection
Dim cmd As ADODB.Command
Dim rs As ADODB.Recordset
Set con = CreateObject("ADODB.Connection")
Set cmd = CreateObject("ADODB.Command")
Set rs = CreateObject("ADODB.Recordset")
con.Open "Provider=SQLOLEDB;Data Source=sqlsrv00;Initial Catalog=pubs;Trusted_Connection=yes"
Set cmd.ActiveConnection = con
cmd.CommandType = adCmdText
cmd.CommandText = "SELECT * FROM authors"
rs.CursorLocation = adUseClient
rs.CursorType = adOpenStatic
rs.LockType = adBatchOptimistic
rs.Open cmd
このコードで最も重要なのが、Recordset のオープン直前に行っている 3 つのオプション設定です。
rs.CursorLocation = adUseClient
rs.CursorType = adOpenStatic
rs.LockType = adBatchOptimistic
3 つのオプションの意味と設定可能な値は以下の通りです。
- CursorLocation : カーソル位置を指定する。
adUseClient、adUseServer(default)
- CursorType : カーソルタイプを指定する。
adOpenForwardOnly(default)、adOpenStatic、adOpenKeyset、adOpenDynamic
- LockType : ロックタイプを指定する。
adOpenReadOnly(default)、adOpenPessimistic、adOpenOptimistic、adOpenBatchOptimistic
そして、ADO の Recordset オブジェクトは、この 3 つのオプションの組み合わせ次第で、内部の挙動が変化するように設計・実装されています。代表的な使い方は以下の 3 つです。
- ① カーソルプロパティをデフォルト値のまま利用する。(adUseServer, adOpenForwardOnly, adOpenReadOnly)
この場合には、ファイアホースカーソルによる接続型データアクセスが行われます。すべてのTransact-SQLステートメントを送出することができ、複数結果セットも利用できます。
- ② カーソルタイプとロックタイプだけを変更する。(adUseServer)
この場合、SQL Server のサーバーカーソル(API サーバカーソルと呼ばれるもの)が利用されます。単一結果セットを取得するTransact-SQLしか利用できません。
- ③ ADO クライアントカーソルを使う。(adUseClient)
この場合、ADO のクライアントカーソル機能が利用され、サーバ側データのスナップショットコピーがクライアント側の Recordset オブジェクト内に一括して取り出されます。
そして、ADO と ADO.NET との対応関係は以下の通りになっています。
- ① ADO でカーソルプロパティをデフォルト値のまま利用する = ADO.NET の接続型データアクセス
- ② ADO でカーソルタイプとロックタイプだけを変更する = (対応する ADO.NET の方式なし)
- ③ ADO クライアントカーソルを使う = ADO.NET の非接続型データアクセス
気を付けなければいけないのは、うかつに Recordset を開くと②のサーバカーソルが開く可能性がある、という点です。実は ADO の時代でも、Web アプリケーションなどでは一番下に書かれた方式(非接続型データアクセス)でアプリケーションを開発するのが望ましいのです。しかし、カーソルオプションを正しく指定しないとこの方式が利用できないことや、そもそもこの方式の存在が知られていないといった問題があり、多くのアプリケーションで誤ったサーバカーソルが利用されていました(そしてこれが各種の性能問題を引き起こす例がかなり見られました)。ちなみに ADO クライアントカーソルを使った非接続型データアクセスの正しいコーディング方法は、以下の通りになります。
Dim con As ADODB.Connection
Dim cmd As ADODB.Command
Dim rs As ADODB.Recordset
Set con = CreateObject("ADODB.Connection")
Set cmd = CreateObject("ADODB.Command")
Set rs = CreateObject("ADODB.Recordset")
con.Open "Provider=SQLOLEDB;Data Source=sqlsrv00;Initial Catalog=pubs;Trusted_Connection=yes"
Set cmd.ActiveConnection = con
cmd.CommandType = adCmdText
cmd.CommandText = "SELECT * FROM authors"
rs.CursorLocation = adUseClient
rs.CursorType = adOpenStatic
rs.LockType = adBatchOptimistic
rs.Open cmd
Set cmd.ActiveConnection = Nothing
Set cmd = Nothing
Set rs.ActiveConnection = Nothing
con.Close
Set con = Nothing
※ ここまで来ると、ADO Recordset はデータベース接続から切り離された DataSet オブジェクトのような状態になるので、プロセス間などでの引き渡しもできるようになる。
このように、非接続型データアクセスの手法を利用し、Connection オブジェクトから切り離された状態で存在する ADO Recordset オブジェクトのことを、当時は切断型レコードセット(Disconnected Recordset)と呼んでいました。(DataSet オブジェクトは、この切断型レコードセットの設計を元にして、これを進化・発展する形で作られています。)
※ なお、ADO Recordset オブジェクトには、カーソルオプションの自動調整機能が備わっており、指定されたカーソルオプションは、カーソルオープン時にテーブルの状況(一意なインデックスの存在)を勘案して自動的に調整されます。テーブルがプライマリキーを持つ場合の自動調整結果を以下に示しますが、これは特に覚える必要はありません。というのは、実際に業務アプリケーションで利用するのは①と③のパターンだけだからです。
[CCE(Client Cursor Engine) と QBU(Query-based Update)]
引き続き、切断型レコードセットを利用した場合の、データベースに対するデータ更新メカニズムについて解説します。
前述したように、ADO カーソルオープン時に adUseClient を利用すると、一括してクライアント側にデータが取り込まれます。このとき、クライアント側の ADO 内部で動作するエンジンのことを CCE (Client Cursor Engine)と呼んでおり、ADO.NET で言うところの DataSet や DataView 的な機能がこれにより実現できるようになっています。CCE が持つ代表的な可能としては、以下のようなものがあります。
- データのフィルタリング、検索、ソート
Recordset オブジェクトのFilterプロパティ、Findメソッド、Sortプロパティを利用すると、データのフィルタリングなどがクライアント側で実施できます。
例) rs.Filter = "State = 'MA'"
rs.Find "State = 'MA'"
rs.Sort = "BalanceDue DESC"
- 単純更新、バッチ更新
Recordset オブジェクトの Update または UpdateBatch メソッドにより、DB に対して、クエリーベースの更新(QBU, Query-based Update)が行えます。
例) rs.Fields("counter") = 100
rs.Update
とすると、以下のような SQL 文(楽観同時実行制御機能つき更新クエリ)が組み立てられ、DB に対して発行される。
UPDATE Counter SET counter=100 WHERE pk='test00' AND counter=50
さらには更新再同期機能(これは DataAdapter にもあります)や更新矛盾時のハンドリング、さらには階層化レコードセットなど(これらはない)などの機能もありますが、解説しだすとキリがないのでやめます;。実際に CCE/QBU によりデータ更新を行うためには、以下のようなコードを書きますが、これは現在の DataSet/DataAdapter に非常によく似た挙動になっています。
Dim con As ADODB.Connection
Dim rs As ADODB.Recordset
Set con = CreateObject("ADODB.Connection")
Set rs = CreateObject("ADODB.Recordset")
con.Open "Provider=SQLOLEDB;Data Source=win2ksv\sql2k;Initial Catalog=Northwind;Trusted_Connection=yes"
rs.CursorLocation = adUseClient ' CCE利用を指定
rs.CursorType = adOpenStatic ' adOpenStatic以外を指定してもこの値に変更される
rs.LockType = adLockBatchOptimistic ' adLockReadOnly以外はこの値に変更される
strSQL = "SELECT CustomerID, BalanceDue FROM Customers(UPDLOCK) WHERE CustomerID='7'"
' データベース上に更新ロックを残すことで、ロック制御を簡単にします。
rs.Open strSQL, con, , , adCmdText
Set rs.ActiveConnection = Nothing ' 切断レコードセットにする
---- (この切断レコードセットをコンポーネント間で受け渡しするなどして変更)
rs.Fields("BalanceDue") = rs.Fields("BalanceDue") + 50 ' ここではUpdateしない
---- (この切断レコードセットをコンポーネント間で受け渡しするなどする)
Set rs.ActiveConnection = con ' 再びデータベースに接続
rs.UpdateBatch
' この処理により、以下のSQL文がデータベースに送られる
' UPDATE Customers SET BalanceDue=150 WHERE CustomerID='7' AND BalanceDue=50
rs.Close
Set rs = Nothing
con.Close
Set con = Nothing
ここでは難しすぎるので深堀はしませんが、ADO 切断型 Recordset によるデータ更新機能は、現在の DataSet や DataAdapter によるデータ更新に比べて(実は)かなりリッチでした。具体的には、以下のようなこともできました。
- 複数のテーブルから JOIN して持ってきたデータの更新書き戻し(書き戻す際に、各テーブルにバラして書き戻す、という機能が備わっていました)
- 楽観同時実行制御のために生成する WHERE 句の形式(PK 列のみ、更新された列のみ、すべての列のみ、タイムスタンプ利用、という 4 パターンすべてがサポートされていました)
- 内部的に「現在の DB 上の最新の値」を持つこと(OriginalValue, CurrentValue の他に、UnderlyingValue というのを持つことができ、これにより、更新衝突時に、Recordset オブジェクト内に現在の DB 上の値を入れて返してやることができるようになっていました。)
がしかし、正直なところ、これらを正確に使いこなすことは極端に難しい、というのが実際のところです。たとえば生成される WHERE 句の形式を変更するためには、
rs.Properties("Update Criteria") = adCriteriaUpdCols
というコードを記述するのですが……普通に考えてそんなの無茶です;。また、複数のテーブルから JOIN して持ってきたデータの更新書き戻しの際には、親/子テーブルに対してどのような反映の仕方をすべきかの細かい判断や制御のための設定も必要で、「機能は充実しているけれども誰も使いこなせない」というライブラリになってしまっていたのが実際かと思います。
※ ちなみに ADO については、David Sceppa 氏の "Progarmming ADO" という名著があり、それにこれらの解説すべてが書かれているのですが、残念ながら邦訳されていないのですよね....
[ADO から ADO.NET へ]
ADO から ADO.NET へ移行する際、.NET Framework の開発チームは ADO 時代の反省を踏まえて ADO.NET のライブラリを設計したそうなのですが、特に大きなポイントとしては以下のものがあったらしいです。(すいません、元の文献が見つからないので記憶を手繰ります。なので間違っているかも。)
- プログラミングモデルを明確化すること。
ADO では、3 つのカーソルオプションを変更することで内部挙動を変更していましたが、これは非常にわかりにくいです。そこで、最初から内部挙動に合わせて利用するクラスを明確に分けるスタイルを取ることにしました。
これにより導入されたのが、接続型データアクセスと非接続型データアクセスという考え方でした。これらのデータアクセスでは、明確に利用するライブラリを分ける、という戦略を取りました。
- CCE/QBU によるデータ更新の方法をすっきりさせること。
非接続型データアクセスにおける QBU は、実は古くは ODBC の時代から存在していたのですが(というかたぶん知らないですよね...)、機能が多すぎる & 複雑すぎることから、使いこなすのが極端に難しくなっていました。
この問題を解決するために、Recordset オブジェクトがもっていた機能(データ保持機能と QBU 機能)とを分離し、それぞれ DataSet と DataAdapter オブジェクトに実装しました。また、利用方法を複雑化させる大きな要因であった「複数テーブルから JOIN で生成されたテーブルに対する QBU」を禁止しています。これにより、DataSet / DataAdapter によるデータ書き戻しが非常にわかりやすくなっています。
- サーバカーソルを使わせないようにすること。
ADO を利用した Web アプリケーション(*.asp アプリケーション)で、性能関係の問題を引き起こした非常に大きな要因の一つが、サーバカーソルの誤用でした。サーバカーソルは、特にサーバ上の大きなテーブルを開く場合などには有効なのですが、一方でサーバとの接続を開いたままだらだらと処理することになります。このため、正しいオプション指定を行わないと、サーバ上に大量のロックを残留させてしまい、大量のブロッキングやデッドロック、ひいては性能問題を引き起こすことがあります。
こうした誤用は ADO ライブラリを開発者が正しく理解して使っていないことが原因ではありますが、一方で、誤用を引き起こしやすいライブラリになっていることも原因の一つです。本来、ライブラリとは「どんな使い方をされてもおかしな動作をしない」ことが望ましいわけで、その観点からみると、ADO Recordset ライブラリの API は、個人的には正直あまりよい API とは思えません。(というか、これは機能拡張を繰り返してきた歴史があるので仕方のない部分ではあるのですが。) この問題を解決するため、ADO.NET ではサーバカーソルの利用そのものができないようになりました。
※ 最後のポイントについて少し捕捉しますと、サーバカーソルは「使ってはいけないもの」ではなく、「間違って使うととんでもないトラブルを引き起こしやすいもの」です。実際、たとえば 100 万件のデータを逐次処理しようと思うと、サーバカーソルなしでは非常に処理が難しいのも事実であり、.NET Framework 2.0 では .NET ストアドプロシージャのサポートと関連して、Resultset オブジェクトと呼ばれるサーバカーソル機能の導入が検討されていました。しかし、β2 のときには存在していた(しかもちゃんと動いていた)この機能、結果的には RTM (製品出荷時)には取り除かれることになりました。(正直なところ、個人的にはかなりほっとした話ではあったりします。)
[まとめ]
というわけでつらつらといろいろ書いてきましたが、ADO.NET 時代を理解してから ADO 時代にさかのぼると、ADO の時代も相当先進的なことをやってたんだなぁと改めて思うのですが(楽観同時実行制御メカニズムが ADO/OLE DB はおろか DAO/ODBC 時代から存在していたということに驚かれる人もいるのでは^^)、ライブラリの進化という観点から見た場合、ADO.NET ライブラリの割り切りは非常にすぐれている、と思います。ADO → ADO.NET は、単なる .NET への変化という以上に、開発者にとって使いやすい API を提供する、という目標があり、それに合致しているからこそ優れたライブラリになっているのではないか、と個人的には思っています。
にしても……アメリカ本社の Microsoft の開発チームの人ってホントすごいと思います。私も Microsoft の人間なのでこういうことを書くと自作自演っぽく見えるかもしれないのですが、一人のエンジニアとしてやはり US の人たちはすごいなと思わずにはいられません。彼らがどんなことを考えてそれを作ったのか、を考えるだけでもいろいろ勉強になります、はい。