Microsoftのデータベース
アクセス手法が目指すもの


秋月巌ソリューション事務所
秋月 巌 AKIZUKI,Iwao


Microsoftのデータアクセス手法は数が多い

 Microsoftの言語製品で,最初にデータベースアクセス機能を標準装備したのは,Visual Basic 3.0だったと思う.それまで,言語製品にデータベース機能というのは標準で搭載される性質のものではなかった.だから,データベースアプリケーションを開発するには,FoxProやParadoxのような開発機能つきのデータベース製品を使うか,OracleやInformixなどのデータベースサーバー製品が提供するライブラリを使用するのが一般的だった.
 それから数年が経過し,現在ではほとんどすべての言語製品にデータベースアクセスのためのライブラリが付属している.Microsoftだけをとっても,DAO,RDO,ADOと複数のデータアクセス用のオブジェクトを提供してきた.COMコンポーネントとして実装されている,これらの高水準なプログラミングインターフェイスのほかにも,汎用的なデータベースアクセスのための規格として比較的低水準なインターフェイスであるODBCやOLE DBなどがある.そして,Microsoft SQL Server専用のモジュールとして,VBカスタムコントロール(VBX)VB-SQLやDB Libraryなども,Microsoftのデータベースアクセス手法のキーワードのひとつといえるだろう.
 これらの数多いリリースに対して,多くの開発者が混乱しているので,それらの使い分けを本稿で説明してほしい,というのが編集部からの要因である.実際,この混乱については,Microsoftも認識しているようである.

Microsoft自身は,何といっているのか?

 Microsoftのホームページからダウンロードできる技術資料には,次のような一節がある.

「ものごとは単純になりつつあります」と,Microsoft Data Access Groupのシニアプログラムマネージャ,Michael Pizzoは述べています.「これは高度にコンポーネントベースのアーキテクチャであり,コンポーネント化こそが,データアクセスを単純化するプラグアンドプレイ機能を実現するものなのです」

 実は,この混乱に対する回答も,やはり,Microsoftのホームページから得ることができる.

質問:今からでも,ADOを使用した方がよいですか? ADOはVisual Basic 5.0でサポートされていますが,ADOの現在の実装レベルでは,RDO 2.0やDAO 3.5と比較してどちらがよいか判断しかねるところです.

はい.Microsoftとしては,納得ずくで移行できるのであれば,あるいはその時期がきたらという条件で,顧客にADOへの移行をお勧めしています.
はい.これは,既存のインターフェイスも引き続きサポートされることを意味します.
はい.これは,Microsoftと顧客のADOへの移行が進むにつれ,既存のインターフェイスの全部ではないにせよ,ほとんどについて拡張が行われなくなることを意味します.

 なるほど,納得できるならば,移行した方がいいというのが,ADOやRDOを作った側の言い分である.
 ところで,次のような一節を別の資料から見つけることもできる.もっとも,この資料はちょっと古い.Visual Basic 6.0が出る前のものである.

現在,Visual BasicのリレーショナルなODBCデータソースへのインターフェイスとしては,RDO 2.0が最高のインターフェイスです.これは,JetやISAMデータソースのインターフェイスとしてDAO/Jetが最高の選択肢であるのと同様の関係です.どちらもVisual Basicに高度に統合され,Visual Basicの包括的なデータアクセス戦略の中核をなしています.RDO 2.0は,RDO 1.0に比べて処理速度が劇的に向上しています.RDOとDAOはどちらも,やや成熟したテクノロジで実現されています.そこで,ODBCデータソースにアクセスするときはRDO,ISAMまたはJetデータソースにアクセスするときはDAO/Jetというように使い分ける必要があります.なお,RDOは,Visual Studio開発ツールのEnterpriseバージョンを選択したときにのみ使用できることに注意してください.

後18ヶ月もすると,ActiveX Data Object(ADO)がもうひとつの選択肢として登場します.やがてはこのADOが他のインターフェイスに取って代わることになります.しかし,RDOとADOのアーキテクチャはよく似ているので,現在RDOに向けたコーディングをしていても,ADOに移行する時期が来たときに設計上の軌道修正を迫られることはありません.ADO 1.0はVB5には含まれていませんが,これをホストする機能はサポートしています.ADOの統合は,Visual Basicの今後のリリースで提供されます.

実はどれでもいいのではないだろうか?

 さぁ,これで編集部のリクエストには,応えられただろうか?
 実は,ADOやRDOの使い分けに関して私は定見をもっていない.大体,ADOとRDOのインターフェイスの違いについてすら,正確には理解していないのである.
 もちろん,DAO(データコントロール),RDO(RDC),ADO,それぞれを使ってプログラミングした経験はある.しかし,その時々でそれらを利用したのは,そこにそれらがあったからである.JetデータベースエンジンにアクセスしようとしたらDAOがそこにあり,リモートデータベースにアクセスしようとしたらRDOがそこにあり,Active Server Pagesからデータベースにアクセスしようとしたら,ADOがそこにあった.
 だから,Jetデータベースエンジンへのアクセス用に作られたDAOをそのために使い,リモートデータのアクセス用に作られたRDOをそのために使うというのが,私の姿勢なのだということもできる.この認識は,Microsoftの技術資料とも一致している.もっとも,ADOは,最初のリリースがActive Server Pagesに付属していただけで,別にActive Server Pages用に開発されたわけではない.
 話をややこしくしているのは,DAOがリモートデータのアクセスにも使え,RDOはJetデータベースエンジンのアクセスに利用できるという事実である.そして,そこにリモートデータベースにも,Jetデータベースにもアクセス可能なADOが追加された.
 AのケースではAを使わなければならないし,BのケースにはBを使わなければならないという状況ではないのである.つまり,Microsoftのデータベースアクセス手法をサポートする開発者は,選択の自由が与えられた.それがゆえに,前述の編集部の要望(それは読者の声に支えられている)が生まれたのである.
 私がこれらの使い分けについて明快な意見をもっていないのは,実はどれを使ってもいいと思っているからである.もちろん,厳密な解は存在するし,最適な選択というのがあることも確かである.しかし,Microsoftのテクノロジーは,同社が最適だといっていても,まともには動作しないのである.それだったら,適していないといっているから,うまく動かないというわけではない.

オブジェクトでラップして,再実装を簡易化する方法もある

 もちろん,多少のパフォーマンスの差はあるだろう.しかし,そのパフォーマンスの差が実用と非実用の差を決定づけるほどシビアな状況ならば,パススルークエリーはともかく,ADOやRDOの高機能を利用することは無謀ですらある.結局,バフォーマンスが重要な要件になるようなケースでは,データホリュームに対して十分なマージンがあるかどうか,実行環境に近い状況でテストする必要がある.それならば,そのテストのときにサンプルを書いて,各種のデータアクセス手法を試してみればいい.
 あるいは,どちらか慣れた方で作っておいて,後でデータアクセスコンポーネントを変更するのもいい.開発した後で変更するのが大変ならば,ADOやRDOといったコンポーネントをラップするコンポーネントを作成して使用するのも方法だろう.ちょうど,本号の連載「実践 クライアント/サーバー データベースソリューション」で,ADOのRecordsetオブジエクトをラップするコンポーネントを含んだHyper SQLdb Client Libraryを,コントロールのソースコードつきで紹介している.今回はライブラリの使用方法だけだが,次号ではコントロールの実装方法について解説するので参考にしてほしい.この方法ならば,必要に応じてコンポーネント内の記述を変更するだけで,すべてのデータアクセスコンポーネントを変更することもできる.

それぞれのメリットをいかして

 また,本稿の後半では,ADOをOLE DBインターフェイスでラップする手法もサンプルつきで紹介する.この手法はデータ連結時の汎用のイベントハンドリングのために利用するためのものだが,コンポーネントの移植のために使用することもできる.
 大切なのは,どのコンポーネントを使用するかということよりも,自分の得意な技術を確立しておくことだろう.そして,どうしてもパフォーマンスがボトルネックになるようならば,その部分だけを別の方法に移植するという手もある.同じデータベースにアクセスするのに,すべて同じ方法でアクセスする必要はないのである.すでにRDOを習得し,その特性を理解しているならばRDOをメインに使い,これからMicrosoftのデータベースアクセス手法を学習するならば,Microsoftに付き合ってADOから始めるのもいい.
 もちろん,DAOを使用するのもいい.DAOは伝統的にリモートデータベースへのアクセスに問題があったが,現在のDAOに内蔵されているODBC DirectはRDOのラッパーである.ゆえに性能は期待できる.すべての機能がマッピングされているわけではないが,用途によっては十分だろう.RDOはVisual Basic Enterprise Editionにしかライセンスが付属していないが,DAOならば,Microsoft Excelを含むMicrosoftのすべての開発ツールから使用することができる.もっとも,ライセンス上のこの制限も,Visual Basic Enterprise Editionのライセンスを持つ人が,RDOをラップするコンポーネントを作成し配布すれば問題がないはずである. 当たり前だが,ODBC Directを開発した人もVisual Basic Enterprise Editionのライセンスをもっていたのである.

Microsoft Accessデベロッパならば…

 悪名高い,MDBファイルのアタッチ機能も現在では大分性能が改善されている.Microsoft Accessのプログラミング技法がすべて使用できるメリットなどを考えれば,試す価値はあるだろう.しかし,注意しておかなければならないのは,ローカルのファイル共有型のデータベースにアクセスするのと,リモートのデータベースサーバーにアクセスするのとでは,テーブルの設計を含めてデータベース全体の作りは大きく変わってくる.プログラミング技法にMicrosoft Accessのそれが使えるにしても,設計手法が共有できるわけではない.Microsoft Access用に作成したシステムにMicrosoft SQL Serverのテーブルをアタッチして性能が向上するということは,まずありえない.

ファイル共有型データベースとサーバー型データベース

 ここまで“データベースアクセス”とひと括りにしてきたが,Jetデータベースエンジンのようなファイル共有型のデータベースにアクセスするのと,データベースサーバーにアクセスするのとでは意味が違う.とくにネットワークまわりの注意点や,最適なカーソルの扱いなどは,大きく異なる.しかし,最終的なプログラミングインターフェイスとして扱われるRecordsetオブジェクト(Resultsetオブジェクト)などの扱いは共通なので,同列に扱ってしまおう.Microsoftのホワイトペーパーなどでも,同じように扱われている.もちろん,これは設計する上では,重大な間違いであることは認識していただきたい.
 私は本誌において,何度もファイル共有型データベースとサーバー型データベースの違いを説明してきたが,それはユーザーがこれらのアーキテクチャの違いを正しく認識しているかについて危惧を抱いているからである.ここでは,その説明は省くが,Microsoft AccessがMicrosoft SQL Serverの廉価版であるかのような,単純な認識ではなく,マルチユーザーがデータベースにアクセスする場合の,メカニズムの違いを理解する必要がある.前述の「実践 クライアント/サーバーデータ ベースソリューション」で解説しているType9 DB Serverは,Jetデータベースエンジンがデータベースサーバーとして動作するように,アーキテクチャを再構築したプロダクトなのである.

データベースは表として保存される

 データベースアクセスは,最終的に表形式のデータへのアクセスを実現する.表は2次元のデータ保存形式であり,2次元の概念をもたないコンピュータにおいて,データは直列化されて扱われる.たとえば,次のような表は,“1,YOSHIDA<CRLF>2,KAMATA”といった文字列で表現することが可能である.

1 YOSHIDA
2 KAMATA

 この表を図1のようなVisual Basicプログラムで表示するには,次のようなコードをボタンのClickイベントプロシージャに記述する.

Private Sub Command1_Click()
 Text1 = Mid("1,YOSHIDA<CRLF>2,KAMATA", 1, 1)
 Text2 = Mid("1,YOSHIDA<CRLF>2,KAMATA", 3, 7)
 Text3 = Mid("1,YOSHIDA<CRLF>2,KAMATA", 16, 1)
 Text4 = Mid("1,YOSHIDA<CRLF>2,KAMATA", 18, 7)
End Sub

図1:表の表示プログラム
図1:表の表示プログラム

 しかし,毎回Mid関数を使ってテキストにアクセスするのは,あまり賢い方法とはいえないだろう.だいたいMid関数の引数の値を計算するのが面倒くさい.

データベースへのアクセスを高水準化する

 そこで,リスト1のようにlistDisp関数を作ってプログラムを構造化する.listDisp関数の最初の引数には行番号,2番目の引数に項目の番号を指定すれば,より簡単に目的のデータをアクセスできるようになる.
 最初のサンプルでは,表へアクセスするAPI関数としてMid関数を用いた.次のサンプルでは,ユーザー定義関数であるlistDisp関数を使って表へのアクセスを簡略化した.listDisp関数の内部ではMid関数を使用している.つまり,listDisp関数は低水準なデータアクセスAPIであるMid関数をラップする高水準な関数だということができる.もちろん,わずか5行からなる関数だから,たいして使いやすくなっているわけではない.

リスト1:プログラムを構造化する試み
Private Sub Command1_Click()
  Text1 = listDisp("1,YOSHIDA<CRLF>2,KAMATA", 1, 1)
  Text2 = listDisp("1,YOSHIDA<CRLF>2,KAMATA", 1, 2)
  Text3 = listDisp("1,YOSHIDA<CRLF>2,KAMATA", 2, 1)
  Text4 = listDisp("1,YOSHIDA<CRLF>2,KAMATA", 2, 2)
End Sub

Private Function listDisp(DataString, Row, Col)
  If Col = 1 Then '1列目の項目の場合
    listDisp = Mid(DataString, Row * 15 - 14, 1)
  Else
    listDisp = Mid(DataString, Row * 15 - 12, 7)
  End If
End Function

モジュールを階層化することで高水準化する

 ここでは関数を用いたが,これをモジュールに置き換えると,ODBCとRDOの関係を説明することができる.ODBCはデータベースアクセスのためのAPI関数を提供するが,用意されている関数は比較的低水準である.理論的にはこのAPIを使えば,データベースアクセスに関するほとんどの処理が実現可能なのだが,それではアプリケーションの開発効率が悪い.これではWin32上での稼動アプリケーションの数を増やして世界征服を企むMicrosoftにとっても都合がよくないし,もちろん,高い開発費用を払わされるユーザーにとっても都合が悪い.もっとも,アプリケーション開発業界にとっては,開発コストが低減しコストが浮くメリットと,売上が減少するデメリットで相殺される.
 RDOがODBCを包み込む様子は,WinAPIをActiveXコントロールが包み込むのに似ている.必要な機能のコンポーネントが提供されれば,開発者はAPIを使用する必要はなくなる.

RDO-ODBCからADO-OLE DB

 RDOがODBCに依存し,ODBC APIのラッパーオブジェクトであるように,ADOはOLE DBのラッパーオブジェクトである.Microsoftの技術資料を見ていると混乱してくるが(皮肉なことに,これらは混乱を解決するすめに作成された資料なのである),OLE DBはODBCの新型であると考えてかまわない(図2).そして,元の関数が変更されたのだから,それに依存するオブジェクトも変更する必要がある.そうして,生まれたのがADOである.
 では,ODBCは新型のOLE DBになって何か変わったのか? 大きな変化として,次の2つがあげられる.

  1. ODBCがSQLデータベース用に設計されたのに対して,OLE DBはあらゆる表形式のデータに対応する
  2. 標準DLLではなく,COMコンポーネントとして実装された

図2:OLE DB-ADOはODBC-RDOの新型
図2:OLE DB-ADOはODBC-RDOの新型

ADO,RDOのライセンス

 ちなみにMicrosoftはOLE DB-ADOをコアにしたデータベースアクセス手法にUniversal Data Accessという名称を与えている.これらを実現するためのコンポーネントをMicrosoft Data Access Component(MSDAC)といい,Microsoftのホームページからダウンロード可能である.だから,ADOを標準ではサポートしていないVisual Basic 5.0でも,これらを使用することで,技術的にもライセンス的にも問題はないはずである.それに対して,RDOはVisual Basic Enterprise Editionユーザーにしかライセンスされていない.

ADOはODBCを必要としない!

 ここでいくつかの誤解をといておく必要がある.ADOが最初に公開されたのはActive Server Pagesの付属オブジェクトとしてであった.これは単にリリースのタイミングの問題であり,ADOはActive Server Pagesのために開発されたわけではない.そして,Active Server Pagesのデータアクセスコンポーネントとして世に出たとき,ODBC経由でデータアクセスするのが一般的に紹介された.それは,OLE DBプロバイダを提供するデータベースエンジンがなかったからである.
 リスト2(図3)は,Active Server Pagesからデータベースにアクセスするときにプグラミング例である.次の1行でConnectionオブジェクトのOpenメソッドの引数に指定されているorderは,ODBCのデータソース名である.

dbConnection.Open "order"
 つまり,このプログラムにおいては,OLE DBがODBCを経由してデータベースアクセスしていることになる.

リスト2:ASPがADOを使用してデータベースに接続するプログラム例(ODBCを経由)
<HTML>
<BODY>
<% Set dbConnection = Server.CreateObject("ADODB.Connection")
  dbConnection.Open "order"
  Set rsSyohin = dbConnection.Execute("SELECT * FROM Syohin")
%>
<% Do While Not rsSyohin.EOF %>
<% = Cstr(rsSyohin ("syohin_code")) + " " %>
<% = rsSyohin ("syohin_mei") %>
<BR>
<%
  rsSyohin.MoveNext
  Loop
%>
</BODY>
</HTML>

図3:リスト1の実行ページ
図3:リスト1の実行ページ

OLE DBプロバイダの不在を補うODBC for OLE DB

図4:ODBCを経由したOLE DBのデータアクセスモデル
図4:ODBCを経由したOLE DBのデータアクセスモデル

図5:アプリケーションがデータベースにアクセスするのがもっとも高率がいい
図5:アプリケーションがデータベースにアクセスするのがもっとも高率がいい
 図4に,このプログラムのモデル図を掲載する.図2のOLE DBのデータベースアクセスモデルと比較してODBCが余分に階層に埋め込まれているのがわかる.これはODBCとOLE DBという2つの同じ階層のモジュールを経由していることを意味する.本来,図5のようにアプリケーションがデータベースに直接アクセスするのが,もっとも効率的なはずなのに,多くの階層を経由するのは,ひとつは上位階層(データベースサーバー)の個体差を吸収することであり,もうひとつはプログラミング水準をシステムレベルからアプリケーションレベルに引き上げるためである.
 このケースにおいて,プログラミング水準が同等のODBCとOLE DBが共存するのは,データベースサーバーの個体差を吸収するためである.復数種のデータベースサーバーに等価的にアクセスするためには,それぞれに対応したOLE DBプロバイダ(ドライバ)が必要である.しかし,発表されたばかりのOLE DBには対応するドライバが存在しなかった.それならば,ODBC用のブリッジドライバをひとつ用意しておけば,ODBCドライバを提供するすべてのデータベースへのアクセスが可能になる.
 このプログラミンクモデルは,ODBCの設定を変更すれば,アプリケーションの変更をせずに異なる種類のデータベースに接続できるというメリットをもつとはいえ,効率的だということはできない.本来のOLE DBのモデルに従って,プログラミングを行なうには,先の1行を次のように修正すればよい(Jetデータベースエンジンの場合).

cn.Provider = "Microsoft.Jet.OLEDB.3.51"
cn.Open App.Path & "\order.mdb"
 これで,ADOは図2のモデルをもちいて,ODBCを経由せずにJetデータベースエンジンにアクセスする.ここで,ProviderプロパティにMicrosoft.Jet.OLEDB.3.51と指定している.先のODBC経由のプログラムコードで,何も指定されていないのは,Providerプロパティのデフォルト値がODBC for OLE DBだからである.

RDOからADOへのプログラミングスタイルの変換

 次のコードは,連載「実践 クライアント/サーバー データベースソリューション」で紹介したType8 DB Serverのデータベースアクセス部分である.

Set en = rdoEnvironments(0)
Set cn = en.OpenConnection(DSName:=txt_dsn.Text, _
       Prompt:=rdDriverCompleteRequired)
           |
Set rs = cn.OpenResultset(sqlStat, rdOpenStatic, _
       rdConcurReadOnly, rdExecDirect)
 RDOを使ってODBC経由でデータベースに接続し,OpenResultsetメソッドを使用して結果セットを取得している.
 次のコードは,今月号の連載記事で使用したType9 DB Serverのデータベースアクセス部である.

Set cn = New ADODB.Connection
        cn.Open txt_dsn.Text
rs.Open sqlStat, cn, adOpenStatic, adLockReadOnly
 Type9よりデータアクセスがADOに変換されているため,コード記述が変換されている.データベースアクセスのためのコード自体は酷似していることがわかるはずである.ここで,ODBC経由のデータを使用しているのは,JetデータベースエンジンとMicrosoft SQL Serverを切り替えて使うための配慮である.RDOをADOに変更した根拠は,パフォーマンス上の問題ではなく,クライアントライブラリを開発するにあたり,ADOのFieldオブジェクトとRDOのFieldオブジェクトの型を表現する定数の違いを避けたかったからである.クライアント側では,ADOのRecordsetオブジェクト作成機能を使用している都合上,ADOを使わざるを得ない.しかし,サーバー側でRDOを使用すると,フィールド情報を送信する際に,型を表現する定数を変換する必要があるのである.

DAO

 DAOの存在を忘れ,話の力点がRDOとADOの比較に傾きすぎているかもしれない.ADO-OLE DBがRDO-ODBCの新型だと考えれば,この2つを比較してしまうのも,当然だといえるだろう.実際,DAOはこれらの2つとは独立した存在である.とはいえ,DAOがRDOやADOと同様のデータアクセス機能を備えている以上,やはり,比較の対象だということができる.
 まず,Jetデータベースエンジンへのアクセスについて,3つの製品の違いについて簡単に述べておこう.DAOはJetデータベースエンジンのネイティブインターフェイスであり,Jetデータベースエンジンにアクセスする機能の豊富さにおいて,RDOやADOを圧倒する.RDOはJetデータベース用のODBCドライバを使用するし,ADOはOLE DBプロバイダを使用する.ODBCやOLE DBはデータに接続するためのインターフェイスだから,それらがサポートしない機能をドライバやプロバイダが提供することはできない.ゆえに,Jetデータベースエンジンの機能を使いきろうと考えるならば,選択肢はDAOに限定されることになる.もちろん,余計なインターフェイスも経由しないわけだから,理論的にはデータアクセスも高速になる.しかし,それが,大きな差となるようなケースは少ないだろう.

DAOのリモートデータアクセス

 次にDAOがリモートデータベースにアクセスするための仕組みについて説明する.DAOがリモートデータベースにアクセスする方法は大きく分けて3種類があげられるが,どのケースにおいてもODBCが利用される.
 もっとも,一般的なリモートデータベースの接続方法はリンクを用いる方法だろう.リンクはMicrosoft Accessを用いて作成することができる.Microsoft Accessの[ファイル]メニューから「外部データの取り込み」「テーブルのリンク」を選択し,表示された「リンク」ダイアログボックスの「ファイルの種類」に「ODBCデータベース」を選択する.ここでデータソースを選択しリンクするテーブルを指定すればいい.
 Microsoft Accessを用いずにVisual Basicでテーブルのリンクを行なうには,たとえば,次のようなプロシージャを実行する.

Private Sub Command1_Click()
  Dim tDef As TableDef
  Dim db As Database
  Dim ws As Workspace

  Set ws = DBEngine.Workspaces(0)
  Set db = ws.OpenDatabase("db1.mdb")
  Set tDef = db.CreateTableDef("dbo_publishers")
  tDef.Connect = "ODBC;DSN=pubs;UID=sa;PWD=;DATABASE=pubs;"
  tDef.SourceTableName = "publishers"
  db.TableDefs.Append tDef
End Sub
 ここでは,db1.mdbファイルに,pubsというデータソース名で登録されているpubsデータベースのPublisherテーブルをリンクしている.このコードの実行以降,サーバーのpublishersテーブルに,Jetデータベースエンジンのdbo_publishersテーブルを扱うのと同じ手法でアクセスすることができる.
 Jetデータベースエンジンのリンクは,サーバー側にあるデータベースのリンク情報を保持するだけで,実際のデータはクライアント側には存在しない.リンク情報にはテーブルの構造情報が含まれるので,接続は高速化される.サーバーテーブルを1対1でMDBファイルに対応づけているところが特徴である.これによって,DAO/JetデータベースエンジンにMicrosoft Accessのプログラミング方法を,そのままリモートデータベースに適用することを可能にする.
 しかし,プログラミングインターフェイスの互換を維持するこの方法は,Jetデータベースエンジンのクエリー実行を特殊なものにしている.

リンクによるリモートデータアクセス

 たとえば,本のタイトルと出版社名をリストにするクエリーを実行したいとする.ここで用いるサンプルは,Microsoft SQL Serverのサンプルデータベース,pubsのテーブルであるpublishersテーブルとtitlesテーブルを,それぞれdbo_publishersとdbo_titlesという名前でMDBファィルにリンクしてあるとする.
 データは「タイトル(titles)」テーブルと「出版社(publishers)」テーブルから取得する必要がある.このためのクエリーをMicrosoft Accessでは図6のように設計する.実行するクエリーは次のようになる.

SELECT dbo_titles.title, dbo_publishers.pub_name
     FROM dbo_publishers
     INNER JOIN dbo_titles ON dbo_publishers.pub_id = dbo_titles.pub_id;
 このクエリーはMicrosoft Accessが生成したものであり,実行した結果は図7のようになる.何も不自然なことではない.複数のテーブルを用いたクエリーが実行されただけである.

図6:クエリーのテザイン
図6:クエリーのテザイン

図7:クエリーの実行結果
図7:クエリーの実行結果

サーバーは,どのようなSQL文を受け取るのか?

 ところで,このクエリー結果はMicrosoft Accessが出力したものだが,内部的にはJetデータベースエンジンが,Microsoft SQL Serverに問い合わせて結果を取得している.Microsoft SQL ServerのデータにJetデータベースが直接アクセスしているわけではない.Microsoft SQL Serverへの問い合わせには,当然,SQL文が使用されている.では,Jetデータベースエンジンは,どのようなSQL文を用いて,Microsoft SQL Serverに要求を行なっているのだろうか?

ひとつのSELECT文を実行するために5つのSELECT文が実行される

 リスト3は先のクエリーの実行時にMicrosoft SQL Serverが受け取ったSQL文の一部である.受け取ったと表現するのは,途中でODBCドライバが介在しているため,Jetデータベースエンジンが送信した内容そのものではないからである.当然,他社のODBCドライバを使用すれば,Microsoft Accessのクエリーが同じでも,違うSQL文を受け取ることになる.
 Microsoft Accessが実行したのは,2つのテーブルを使用したひとつのクエリーだったのに,ここでは5つのSELECT文が実行されていることがわかる.つまり,図7の結果を受け取るために,5回のSQLが実行されたことになる.

リスト3:JetデータベースエンジンがODBC経由でMicrosoft SQL Serverに送ったSQL文
SELECT "dbo"."publishers"."pub_id","dbo"."titles"."title_id" ----------------
       FROM "dbo"."titles","dbo"."publishers"                |--クエリー1
       WHERE ("dbo"."publishers"."pub_id" = "dbo"."titles"."pub_id" ) --------
go
SELECT "pub_id","pub_name" ---------------------------------------------------
       FROM "dbo"."publishers"                                               |
       WHERE "pub_id" = '1389' OR "pub_id" = '1389'                          |
          OR "pub_id" = '0736' OR "pub_id" = '1389'                          |--クエリー2
          OR "pub_id" = '0877' OR "pub_id" = '0877'                          |
          OR "pub_id" = '0877' OR "pub_id" = '1389'             |
          OR "pub_id" = '1389' OR "pub_id" = '1389' --------------------------
go
SELECT "title_id","title","pub_id" -------------------------------------------
       FROM "dbo"."titles"                             |
       WHERE "title_id" = 'BU1032' OR "title_id" = 'BU1111'         |
          OR "title_id" = 'BU2075' OR "title_id" = 'BU7832'         |--クエリー3
          OR "title_id" = 'MC2222' OR "title_id" = 'MC3021'         |
          OR "title_id" = 'MC3026' OR "title_id" = 'PC1035'         |
          OR "title_id" = 'PC8888' OR "title_id" = 'PC9999' ------------------
go
SELECT "pub_id","pub_name" ---------------------------------------------------
      FROM "dbo"."publishers"                           |
      WHERE "pub_id" = '0877' OR "pub_id" = '0736'                  |
         OR "pub_id" = '0736' OR "pub_id" = '0736'                  |--クエリー4
         OR "pub_id" = '0736' OR "pub_id" = '0877'                  |
         OR "pub_id" = '0877' OR "pub_id" = '0877'                  |
         OR "pub_id" = '0877' OR "pub_id" = '0877' ---------------------------
go
SELECT "title_id","title","pub_id" -------------------------------------------
      FROM "dbo"."titles"                                 |
      WHERE "title_id" = 'PS1372' OR "title_id" = 'PS2091'              |
         OR "title_id" = 'PS2106' OR "title_id" = 'PS3333'              |--クエリー5
         OR "title_id" = 'PS7777' OR "title_id" = 'TC3218'              |
         OR "title_id" = 'TC4203' OR "title_id" = 'TC7777'              |
         OR "title_id" = 'TC7777' OR "title_id" = 'TC7777' -------------------

図8:クエリー1の実行結果
図8:クエリー1の実行結果

図9:クエリー2の実行結果
図9:クエリー2の実行結果

図11:クエリー4の実行結果
図11:クエリー4の実行結果

図12:クエリー5の実行結果
図12:クエリー5の実行結果

図10:クエリー3の実行結果
図10:クエリー3の実行結果


図13:クエリーの実行結果にID列を表示
図13:クエリーの実行結果にID列を表示

分割して実行されるクエリー

 これらのSQL文には,リンク情報として使われているpub_id項目とtitle_id項目が頻出する.解析する上での手助けとして,それぞれのIDが表示された実行結果が図13である.また,それぞれのクエリーを単体で実行した結果が,図8〜図12である.
 まず最初のクエリー1の実行結果(図8)は,結果セットのキーセットを入手している.図13のpub_id項目とtitle_id項目の内容だけが,リストされた内容になっている.クエリー2とクエリー4は分割された2つのクエリーだということができる.このSQLの条件に指定されている1389,1389,0736...(クエリー2),あるいは0877,0736,0736...(クエリー4)といった数字の羅列は,クエリー1のpubs_id項目のデータに呼応している.
 そして,クエリー3とクエリー5が,同一のクエリーが分割されたものであることを想像することも難しくない.これらのクエリーの条件に列挙されているtitle_id項目データは,クエリー1の同項目の結果に対応する.つまり,Jetデータベースエンジンは,受信した図8-図12の5つのクエリーの結果を組み立てて,図7の結果を合成するのである.
 ここでは,複数のテーブルを使用したクエリーを例に用いたが,単一テーブルへのアクセスでも,Jetデータベースエンジンは,最初にキーセットを取得してから,データ本体を取得する.この方法がよいか悪いかの判断は避けるが,私はあまり好きではない.

図14:データアクセスサンプルアプリケーションの実行画面
図14:データアクセスサンプルアプリケーションの実行画面

リスト4:ODBCDirectを使用したデータアクセス
'[ODBCDirectでクエリー実行]ボタンのイベントプロシージャ
Private Sub Command1_Click()
    Dim ws As Workspace
    Dim cn As Connection
    Dim sParam As String
    Dim rs As Recordset

    Set ws = DBEngine.CreateWorkspace("sample_ws", "sa", "", dbUseODBC)
    Set cn = ws.OpenConnection("sample_cn", dbDriverNoPrompt, True, _
        "ODBC;DSN=pubs;UID=sa;PWD=;DATABASE=pubs;")
    sParam = "SELECT titles.title, publishers.pub_name " & _
                "FROM publishers INNER JOIN titles ON publishers.pub_id = titles.pub_id"
    Set rs = cn.OpenRecordset(sParam, dbOpenForwardOnly, 0, dbReadOnly)

    Dim i As Integer
    Do While Not rs.EOF
        For i = 0 To rs.Fields.Count - 1
            Text1 = Text1 & rs.Fields(i).Value & " :  "
        Next
        Text1 = Text1 & vbCrLf
        rs.MoveNext
    Loop
End Sub

ODBCDirect

 ODBCDirectは,Jetデータベースエンジンがリモートデータベースにアクセスする上での強力なパワーを提供する.このデータベースアクセス手法がRDOを経由するものであることは説明した.ODBCDirectはDAOインターフェイスに内蔵されているが,実行時にJetデータベースエンジンをロードしないので,その分,リソースへの負担は軽い.もっとも,RDOを経由するならば,最初からRDOを使えばいいというVisual Basicデベロッパもいるだろう.しかし,Visual Basic Enterprise Editionを持たないVisual Basicプログラマや,Microsoft Accessデベロッパーにとって,RDOのライセンスなしでRDOを使用できるメリットは大きい.
 ODBCDriectを用いたデータベースアクセスのサンプルプロシージャをリスト4に示す.Workspaceオブジェクトのインスタンスの作成後,次のようにOpenConnectionメソッドを使用してサーバーへの接続を確立する.

Set cn = ws.OpenConnection("sample_cn", _
    dbDriverNoPrompt, True, _
    "ODBC;DSN=pubs;UID=sa;PWD=;DATABASE=pubs;")
 レコードセットを取得するためには,ConnectionオブジェクトのOpenRecordsetメソッドを使用してRecordsetオブジェクトを作成する.

sParam = "SELECT titles.title, publishers.pub_name " & _
         "FROM publishers INNER JOIN titles ON publishers.pub_id = titles.pub_id"
Set rs = cn.OpenRecordset(sParam, dbOpenForwardOnly, 0, dbReadOnly)
 ここで実行しているSELECT文は,リンクによるデータベースアクセスに用いたものと同じである.このSQL文が実行された際に,Microsoft SQL Serverが受け取るSQL文がリスト5である.ここでは,サーバー側のカーソルを開いているが,リンクによるデータアクセスのときのように,SELECT文を分割するような処理は行なっていない.

リスト5:ODBCDrectの使用時にMicrosoft SQL Serverが受け取るSQL文
sp_cursoropen 0, "SELECT titles.title, publishers.pub_name FROM publishers ⇒
    INNER JOIN titles ON publishers.pub_id = titles.pub_id", 4, 1, 1
go
sp_cursorfetch 19225392, 2, 1, 100
go
sp_cursorfetch 19225392, 2, 1, 100
go
sp_cursorclose 19225392

パススルークエリー

 パススルークエリーは,Jetデータベースエンジンのクエリープロセッサを介さずにODBCインターフェイスに渡される.リスト6は,データアクセスサンプルアプリケーションの[SQLパススルー]ボタンをクリックした時に実行される.送信されるクエリーは,ODBCDirectのサンプルと同様であり,実行画面も図14と変わらない.これは今後解説する,RDOやADOのサンプルでも同様である.
 データアクセスのためのコードは,次のコードに集約される.

qDef.Connect = "ODBC;DSN=pubs;UID=sa;PWD=;DATABASE=pubs;"
qDef.SQL = "SELECT titles.title, publishers.pub_name " & _
           "FROM publishers INNER JOIN titles ON publishers.pub_id = titles.pub_id"
qDef.ReturnsRecords = True
Set rs = qDef.OpenRecordset
 QueryDefオブジェクトのConnectプロパティに,ODBCのデータソース名を含む接続文字列を指定する.ReturnsRecordsプロパティをTrueに設定し,OpenRecordsetメソッドを実行すると結果が取得できる.
 実行時にMicrosoft SQL Serverが受け取るSQL文が以下である.さすがに,パススルークエリーの名称通り,実行したクエリーがそのまま渡されている.

SELECT titles.title, publishers.pub_name ⇒
  FROM publishers INNER JOIN titles ON publishers.pub_id = titles.pub_id
リスト6:パススルークエリー実行のサンプル
'[SQLパススルー]ボタンのクリック時
Private Sub Command2_Click()
  Dim db As Database
  Dim rs As Recordset
  Dim qDef As QueryDef

  Set db = Workspaces(0).OpenDatabase("db1.mdb")
  db.QueryDefs.Delete "sample_qdef"
  Set qDef = db.CreateQueryDef("sample_qdef")
  qDef.Connect = "ODBC;DSN=pubs;UID=sa;PWD=;DATABASE=pubs;"
  qDef.SQL = "SELECT titles.title, publishers.pub_name " & _
             "FROM publishers INNER JOIN titles ON publishers.pub_id = titles.pub_id"
  qDef.ReturnsRecords = True
  Set rs = qDef.OpenRecordset

  Dim i As Integer
  Do While Not rs.EOF
    For i = 0 To rs.Fields.Count - 1
      Text1 = Text1 & rs.Fields(i).Value & " :  "
    Next
    Text1 = Text1 & vbCrLf
    rs.MoveNext
  Loop

End Sub

RDO使用のサンプル

 RDOによるデータベースアクセスのプログラミング方法は,本誌でも再三扱われているので,詳しくは述べない.ただ,ODBCDirectでアクセスした場合と比較して,Microsoft SQL Serverが受信するSQL文にどのような違いがあるかに興味がある.
 リスト7は[RDO]ボタンがクリックされたときに実行されるプロシージャである.データベースへの接続と,アクセスする部分は次のようなコード記述になる.

Set cn = rdoEnvironments(0).OpenConnection("pubs", , True, ⇒
        "DSN=pubs;UID=sa;PWD=;DATABASE=pubs;")
sParam = "SELECT titles.title, publishers.pub_name " & _
         "FROM publishers INNER JOIN titles ON publishers.pub_id = titles.pub_id"
Set rs = cn.OpenResultset(sParam, rdOpenForwardOnly, rdConcurReadOnly)
 他のデータアクセスオブジェクトが,Recordsetという名称なのに,Resultsetと呼ばれるところが興味深い.ConnectionオブジェクトのOpenResultsetメソッドによって,SELECT文が実行される.
 その結果,Microsoft SQL Serverが受信したSQL文は,予想通り,ODBCDirectと同じものだった.

リスト7:RDO使用のサンプル
' [RDO]ボタンのクリック時
Private Sub Command3_Click()
  Dim cn As rdoConnection
  Dim sParam As String
  Dim rs As rdoResultset

  Set cn = rdoEnvironments(0).OpenConnection("pubs", , True, ⇒
        "DSN=pubs;UID=sa;PWD=;DATABASE=pubs;")
  sParam = "SELECT titles.title, publishers.pub_name " & _
           "FROM publishers INNER JOIN titles ON publishers.pub_id = titles.pub_id"
  Set rs = cn.OpenResultset(sParam, rdOpenForwardOnly, rdConcurReadOnly)

  Dim i As Integer
  Do While Not rs.EOF
    For i = 0 To rs.rdoColumns.Count - 1
      Text1 = Text1 & rs.rdoColumns(i).Value & " :  "
    Next
    Text1 = Text1 & vbCrLf
    rs.MoveNext
  Loop
End Sub

ADO(ODBC)

[ADO odbc]ボタンをクリックしたときに実行されるのが,リスト8のプロシージャである.ADO(ODBC)によるデータベースアクセスは,ConnectionオブジェクトのOpenメソッドの実行時に,接続文字列としてODBCのデータソース名を使う方法である.この場合,Microsoft SQL ServerのOLE DBデータプロバイダは使用されず,Microsoft SQL ServerのODBCドライバとMicrosoft ODBC Provider for OLE DBが使用される.
 接続を確立し,SQL文を実行しているのが次の部分である.

sParam = "SELECT titles.title, publishers.pub_name " & _
         "FROM publishers INNER JOIN titles ON publishers.pub_id = titles.pub_id"
cn.Open "pubs", "sa", ""
rs.Open sParam, cn, adOpenForwardOnly, adLockReadOnly
 Microsoft SQL Serverが受信したSQL文は,パススルークエリーの使用時に受信したものと同じである.つまりADO(ODBC)の経由のアクセスでは,このケースでは指定したSQLが,そのままサーバーに渡されたことになる.

ADO(OLE DB)

 Microsoft ODBC Provider for OLE DBを使用せずに,ADOが直接,Microsoft SQL ServerのOLE DBプロバイダに接続するのが,リスト9のプロシージャである.このサンプルはADO(ODBC)の場合のコード記述と比較して,次の1行が違うだけである.

cn.Open "Provider=SQLOLEDB.1;Persist Security Info=False; ⇒
        User ID=sa;Initial Catalog=pubs;Data Source=SANTIAGO"
 ConnectionオブジェクトのOpenメソッドのProvider項目にSQLOLEDB.1を指定することで,ADOはMicrosoft SQL ServerのOLE DBプロバイダのインターフェイスを参照する.
 このプロシージャを実行した場合にMicrosoft SQL Serverが受け取るSQL文は,ADO(ODBC)やパススルークエリーと同じもので,指定したSELECT文がそのままサーバーに渡された.

リスト8:ADO(OLE DB)使用のサンプル
'[ADO - OLE DB]ボタンのクリック時
Private Sub Command5_Click()
  Dim cn As ADODB.Connection
  Dim rs As ADODB.Recordset
  Dim sParam As String
  Set cn = New ADODB.Connection
  Set rs = New ADODB.Recordset

  sParam = "SELECT titles.title, publishers.pub_name " & _
           "FROM publishers INNER JOIN titles ON publishers.pub_id = titles.pub_id"

  cn.Open "Provider=SQLOLEDB.1;Persist Security Info=False; ⇒
    User ID=sa;Initial Catalog=pubs;Data Source=SANTIAGO"
  rs.Open sParam, cn, adOpenForwardOnly, adLockReadOnly

  Dim i As Integer
  Do While Not rs.EOF
    For i = 0 To rs.Fields.Count - 1
      Text1 = Text1 & rs.Fields(i).Value & " :  "
    Next
    Text1 = Text1 & vbCrLf
    rs.MoveNext
  Loop

End Sub

リスト9:ADO(ODBC)使用のサンプル
'[ADO odbc]ボタンのクリック時
Private Sub Command4_Click()
  Dim cn As ADODB.Connection
  Dim rs As ADODB.Recordset
  Dim sParam As String
  Set cn = New ADODB.Connection
  Set rs = New ADODB.Recordset

  sParam = "SELECT titles.title, publishers.pub_name " & _
           "FROM publishers INNER JOIN titles ON publishers.pub_id = titles.pub_id"
    cn.Open "pubs", "sa", ""
  rs.Open sParam, cn, adOpenForwardOnly, adLockReadOnly

  Dim i As Integer
  Do While Not rs.EOF
    For i = 0 To rs.Fields.Count - 1
      Text1 = Text1 & rs.Fields(i).Value & " :  "
    Next
    Text1 = Text1 & vbCrLf
    rs.MoveNext
  Loop

End Sub

OLE DB

 ADOでOLE DBプロバイダを用いてアクセスする例を除けば,すべてODBC経由のデータベースアクセス方法を解説してきた.当然その新型であるOLE DBプロバイダについても十分に説明する必要があるだろう.ここまでは,ODBCによるリモートデータベースへのアクセスを扱ったが,ODBCそのものには触れなかった.ここからは,OLE DBそのものについて解説する.
 Visual Basicプログラマから見た場合のOLE DBとODBCの差は“Visual BasicでOLE DBプロバイダは作成できるが,ODBCドライバは作成できない”という点につきる.また,OLE DBはADOやRDOを経由せずに,データグリッドなどの連結コントロールに連結できるというメリットがある.それらの特徴を利用して作成したのが,次に紹介するサンプル(図15・16)である.データベースにアクセスした内容がフォームに表示されているだけだが,このグリッドへのデータ連結にはOLE DBを用いている.ポイントはデータを追加したり削除したりしようとしたときに,メッセージボックスが表示されることである.

図15:OLE DBシンプルプロバイダサンプル
図15:OLE DBシンプルプロバイダサンプル

図16:データの追加や削除で自動的にメッセージを表示
図16:データの追加や削除で自動的にメッセージを表示

ADOの先端にOLE DBプロバイダを実装する

 このサンプルでは,図17のようなアーキテクチャでデータを扱っている.OLE DB-ADOによってデータベースにアクセスした結果は,ADOのRecordsetオブジェクトに格納される.このサンプルでは,このRecordsetオブジェクト自体をデータベースとして扱い,OLE DBインターフェイスを使って公開する.
 もちろん,ADOのRecordsetオブジェクトはデータ連結をサポートするから,図18のように直接,連結コントロールに連結する方法もある.しかし,Recordsetオブジェクトのふるまいを開発者は変更できない.そのため,データ連結は実際の開発の現場では利用されることは少ない.柔軟性が低すぎるのである.たとえば,サンプルのようにデータの追加や削除が行なわれたたときに任意のメッセージを表示することはできない.もちろん,連結コントロールのイベントハンドラに同様の処理を記述することはできる.しかしその場合,他のコントロールに連結すると処理を再記述することが必要になる.

図17:OLE DBシンプルプロバイダサンプルの構造
図17:OLE DBシンプルプロバイダサンプルの構造
図18:通常のADOデータ連結
図18:通常のADOデータ連結

OLE DBの趣旨からはずれた応用例

 本来,OLE DBは,このようにデータベースアプリケーションの開発テクニックのために設計されたものではない.あくまでも,広範囲なデータベースに対してプラグインで接続できるようになることを目的に開発されたものである.しかし,ここではADOのRecordsetオブジェクトをデータベースと仮想することで,データ連結の柔軟性を新しい次元に導くことができる.もちろん,この方法はMicrosoftの技術資料には載っていないし,推奨もしていない.

OLE DBシンプルプロバイダサンプルの構成

 図19はOLE DBシンプルプロバイダサンプルのプロジェクトである.2つのプロジェクトから構成されるプロジェクトグループで,ひとつのプロジェクトには2つのクラスモジュールがあり,もうひとつのプロジェクトには,ひとつのフォームがある.
 Visual Basicで作成できるOLE DBプロバイダは,OLE DB Simple Providerと呼ばれる簡易バージョンである.OLE DB Simple Providerを作成するにはMicrosoft OLE DB Simple Provider 1.5 LibraryとMicrosoft OLE DB Error Libraryを参照可能にする必要がある(図20).このコンポーネントはActiveXコンポーネントとして実装されるため,クラスモジュールとして実装されている.
 OLE DBシンプルプロバイダを連結するフォームでは,Microsoft Data Adapter LibraryとOLE DBプロバイダを実装するコンポーネントを参照可能に指定する(図21).

図19:OLE DBシンプルプロバイダサンプル
図19:OLE DBシンプルプロバイダサンプル

図20:Microsoft OLE DB Simple Provider 1.5 LibraryとMicrosoft OLE DB Error Libraryを参照可能に設定
図20:Microsoft OLE DB Simple Provider 1.5 LibraryとMicrosoft OLE DB Error Libraryを参照可能に設定

図21:データ連結のためにMicrosoft Data Adapter Library
図21:データ連結のためにMicrosoft Data Adapter Library

フォームモジュールに記述されたコードでデータ連結を実現

 データ連結をするフォームモジュールに記述されたコードは,オブジェクト変数の宣言部を除けば,フォームのLoadイベントに記述された2行がすべてである.

Private DAdapter As New DataAdapter
Private OSPDSource As New OSPDataSource
Private Sub Form_Load()
  Set DAdapter.Object = OSPDSource
  Set DataGrid1.DataSource = DAdapter
End Sub
 ここでは,Data AdapterオブジォクトのObjectプロパティに自作のデータソースであるOSPDSourceオブジェクトを設定し,DataGridコントロールのDataSourceプロパティにData Adapterオブジォクトを指定している.これでデータ連結は完成する.ほとんどすべてのプログラムは,クラスにカプセル化されているのである.
 では,OSPDSourceクラスの内容を見てみよう.実は,このクラスも内容がほとんどない.データソースの実体であるOSPObjクラスとデータ連結するためのインターフェイスだということができる.このクラスはDataSourceBehaviorプロパティがvbDataSourceに設定されている.このプロパティがこの値に設定されているクラスやActiveXコントロールは,連結コントロールのデータソースになることが可能であり,かつGetDataMemberイベントをもつ.

OSPDataSourceオブジェクトはOSPObjオブジェクトのラッパー

 次のGetDataMemberイベントプロシージャでは,OSPObjクラスのインスタンスを作成し,データを読みこむためのLoadDataメソッドを実行し,データソースの実体としてOSPObjオブジェクトを指定している.

Private Sub Class_GetDataMember(DataMember As String, _
                                Data As Object)
  Dim OSPObj As New OSPObj
  OSPObj.LoadData
  Set Data = OSPObj
End Sub
 この3行のコードが, OSPDSourceクラスに記述されたすべてのコードである.サンプルでは,特定のテーブルのすべてのデータを対象しているが,もし,データを限定したい場合は,DataMamber引数で文字式を受け取り,この場合はLoadDataメソッドの引数として受け渡すことで,扱うデータを特定できる.

データソースの実体とOLE DB Simple Providerインターフェイスのサポート

 OSPObjクラスのソースコードを章末リスト10に示す.このクラスのコードが長大になるのには理由がある.宣言部でOLEDBSimpleProviderオブジェクトに対するImplementsキーワードが使用されている.これはOLEDBSimpleProviderオブジェクトのインターフェイスの,すべてを実装することが強制される.OSPObjクラスのメンバーのうち,OLEDBSimpleProviderオブジェクトのインターフェイス以外のメソッドは,LoadDataメソッドだけである.他の関数はインターフェイスの互換性維持のために,内容がなくても記述されている必要がある.コメントだけのプロシージャがあるのは,そのためである.
 これらのイベントプロシージャには,連結コントロールによって発生するイベントに対応する処理が記述されている.各イベントは,データの削除や変更,追加などの他,レコード件数の照会などによって呼び出される.各々のコードについて詳細を解説するのは避けるが,これらのイベントプロシージャが,OLE DBのインターフェイスであることが理解できるだろう.

ADOから,OLE DB Simple Providerにアクセスする

 ところで,ADOはOLE DBのためのアプリケーションインターフェイスである.ということは,ここで作成したOLE DBシンプルプロバイダも,ADOに接続できるはず,と考えたとしたらそれは正しい.このOLE DBデータプロバイダにADOを接続するには,OSPDataSourceクラスとOSPObjクラスをActiveXコンポーネントとしてコンパイルした後で,新しいアプリケーションに次のようなプロシージャを追加する.

Private Sub Command1_Click()
  Set cn = New ADODB.Connection
  Set rs = New ADODB.Recordset

  cn.Open "Provider=MSDAOSP.1;Data Source=OSPSample.OSPDataSource;Persist Security Info=False"
  rs.Open , cn

  Set DataGrid1.DataSource = rs

End Sub
図22:意味のない階層モデル
図22:意味のない階層モデル

 これでOLE DBシンプルプロバイダのインターフェイスを経由してコントロールに連結されたADOがデータベースにアクセスする.接続とレコードセットの作成は,ConnectionオブジェクトとRecordsetオブジェクトのOpenメソッドが実行されるわずか2行で完結している.しかし,資料が少ないため,私はこの2行を正常に動作させるために半日の時間を費やした.
 この場合,データアクセスのモデルは図22のようになる. MSDAOSPがADOと自作OLE DBシンプルプロバイダの間に入るのは,OLE DBシンプルプロバイダの提供するインターフェイスが,コンプリートなOLE DBプロバイダのインターフェイスに満たないからである. MSDAOSPが不足するインターフェイスを補うことで,ADOとの連携が可能になる.
 もっとも,このサンプルプロシージャは,自作のOLE DBシンプルプロバイダがADOを接続できることを示すためだけのものである.結果として図22のような階層モデルとして動作することには,何のメリットもない.
 なお,今回のMicrosoft SQL Serverとの接続は,すべてMicrosoft SQL Server 6.5で行なっている.Microsoft SQL Server 7.0による結果は,近々開始される本誌連載を期待していただきたい.

リスト10:データソースの実体であるOSPObjクラスのソースコード
Option Explicit

Implements OLEDBSimpleProvider
Dim RowCount As Integer
Dim ColCount As Integer
Dim colListeners As New Collection
Dim ospl As OLEDBSimpleProviderListener
Dim cn As ADODB.Connection
Dim rs As ADODB.Recordset

' レコードセットの作成
Public Sub LoadData()
  Set cn = New ADODB.Connection
  Set rs = New ADODB.Recordset
  cn.Provider = "Microsoft.Jet.OLEDB.3.51"
  cn.Open App.Path & "\order.mdb"
  rs.Open "SELECT syohin_code,syohin_mei FROM Syohin", cn, adOpenKeyset, adLockOptimistic
  RowCount = rs.RecordCount
  ColCount = rs.Fields.Count
End Sub

' データ変更時のクラスを通知する listener を追加します.
Private Sub OLEDBSimpleProvider_addOLEDBSimpleProviderListener(ByVal ⇒
    pospIListener As MSDAOSP.OLEDBSimpleProviderListener)
  'Listeners コレクションにリスナを追加します.
  If Not (pospIListener Is Nothing) Then
    Set ospl = pospIListener
    colListeners.Add ospl
  End If
End Sub

'データを削除します
Private Function OLEDBSimpleProvider_deleteRows(ByVal iRow As Long, ⇒
   ByVal cRows As Long) As Long
  Dim listener As OLEDBSimpleProviderListener
  Dim vlistener As Variant
  MsgBox iRow & "行目のレコードを削除します.", vbInformation

  ' 各リスナに通知します.
  For Each vlistener In colListeners
    Set listener = vlistener
    listener.aboutToDeleteRows iRow, cRows
  Next

  rs.Move iRow - 1, adBookmarkFirst
  rs.Delete

  ' 実際の行数を再設定します.
  RowCount = RowCount - cRows

  ' 各リスナに通知します.
  For Each vlistener In colListeners
    Set listener = vlistener
    listener.deletedRows iRow, cRows
  Next

  ' 削除された行の数を返します.
  OLEDBSimpleProvider_deleteRows = cRows

End Function
' データを検索します
Private Function OLEDBSimpleProvider_find(ByVal iRowStart As Long, _
                                          ByVal iColumn As Long, _
                                          ByVal val As Variant, _
                                          ByVal findFlags As MSDAOSP.OSPFIND, _
                                          ByVal compType As MSDAOSP.OSPCOMP) As Long
' データを検索
End Function
' フィールド数を返します
Private Function OLEDBSimpleProvider_getColumnCount() As Long
  OLEDBSimpleProvider_getColumnCount = ColCount
End Function
' データの推測行数を返します
Private Function OLEDBSimpleProvider_getEstimatedRows() As Long
  OLEDBSimpleProvider_getEstimatedRows = RowCount
End Function

Private Function OLEDBSimpleProvider_getLocale() As String
  OLEDBSimpleProvider_getLocale = ""
End Function
' データの行数を返します
Private Function OLEDBSimpleProvider_getRowCount() As Long
  OLEDBSimpleProvider_getRowCount = RowCount
End Function
' 列ごとに読み取りと書き込みのステータスを設定します.
' この場合,最初の列が読み取り専用で,残りの列は読み取りと書き込みが可能になります.
Private Function OLEDBSimpleProvider_getRWStatus(ByVal iRow As Long, _
                                                 ByVal iColumn As Long) As MSDAOSP.OSPRW
  If iColumn = 1 Then
    ' 先頭の列を読み取り専用にします.
    OLEDBSimpleProvider_getRWStatus = OSPRW_READONLY
  Else
    ' 他の列を読み取り書き込み可能にします.
    OLEDBSimpleProvider_getRWStatus = OSPRW_READWRITE
  End If
End Function
' 特定の行と列に格納されているデータを返します
Private Function OLEDBSimpleProvider_getVariant(ByVal iRow As Long, _
                                                ByVal iColumn As Long, _
                                                ByVal format As MSDAOSP.OSPFORMAT) As Variant
  If iRow = 0 Then
    OLEDBSimpleProvider_getVariant = rs.Fields(iColumn - 1).Name
  Else
    rs.Move iRow - 1, adBookmarkFirst
    OLEDBSimpleProvider_getVariant = rs.Fields(iColumn - 1).Value
  End If
End Function
' 新しいデータの行を挿入
Private Function OLEDBSimpleProvider_insertRows(ByVal iRow As Long, _
                                                ByVal cRows As Long) As Long
  Dim listener As OLEDBSimpleProviderListener
  Dim vlistener As Variant

  MsgBox iRow & "行目にレコードを追加します.", vbInformation

  ' 各リスナに通知します.
  For Each vlistener In colListeners
    Set listener = vlistener
    listener.aboutToInsertRows iRow, cRows
  Next

  rs.AddNew

  ' 実際の行数を再設定します.
  RowCount = RowCount + cRows

  ' 各リスナに通知します.
  For Each vlistener In colListeners
    Set listener = vlistener
    listener.insertedRows iRow, cRows
  Next

  ' 挿入された行数を返します.
  OLEDBSimpleProvider_insertRows = cRows

End Function
' OSP が非同期でデータを返すことができるかどうかを判断します
Private Function OLEDBSimpleProvider_isAsync() As Long
  OLEDBSimpleProvider_isAsync = False
End Function
' listener を削除します
Private Sub OLEDBSimpleProvider_removeOLEDBSimpleProviderListener⇒
        (ByVal pospIListener As MSDAOSP.OLEDBSimpleProviderListener)
  Dim i As Integer

  ' リスナを削除します.
  For i = 1 To colListeners.Count
    If colListeners(i) Is pospIListener Then
       colListeners.Remove i
    End If
  Next

End Sub
' 特定の行と列からデータを取り込み,
' データが変更されたという通知を提供する
' listenerを指定します
Private Sub OLEDBSimpleProvider_setVariant(ByVal iRow As Long, _
                                           ByVal iColumn As Long, _
                                           ByVal format As MSDAOSP.OSPFORMAT, _
                                           ByVal Var As Variant)
  Dim listener As OLEDBSimpleProviderListener
  Dim vlistener As Variant

  For Each vlistener In colListeners
    Set listener = vlistener
    listener.aboutToChangeCell iRow, iColumn    ' 通知の前
  Next

  rs.Move iRow - 1, adBookmarkFirst
  rs.Fields(iColumn - 1).Value = Var
  rs.Update

  For Each vlistener In colListeners
    Set listener = vlistener
    listener.cellChanged iRow, iColumn          ' 通知の後
  Next

End Sub

Private Sub OLEDBSimpleProvider_stopTransfer()
   ' 未実装
End Sub


VB Magazine ライブラリ | Visual Basic WorkGroup
int21 ホームページ | PCDN ホームページ


Copyright (c) 1998 int21 Corporation All Rights Reserved.
For questions or comments, please send mail to: pcdn@int21.co.jp