実践 クライアント/サーバーデータベースソリューション 第15回 第17回

第16回
Collectionオブジェクトを利用して、Fieldオブジェクトを配列化する

Access,VB6,Jetデータベースエンジンを利用した
C/Sシステム構築に関する考察,実験および実践



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



遅い進行

 この連載もずいぶんと回を重ねている。連載開始当初の目論見通りに展開し、着実に目的地へ近づいている。しかし、その歩みの速度は、予想もしていなかったくらいに遅い。読者によっては、この連載は常に同じところを旋回しているように感じられるかもしれない。その歩みの遅さは、私がこの連載をライフワークのように考えて、結果を急いでいないことからくるものだろう。
 しかし、考えてもみてほしい。この連載では、当初、Socket通信を扱い、ActiveXコンポーネントを使った並列処理についてアイデアを提供し、またVisual Basicによる安全なマルチスレッドアプリケーション開発方法を提唱し、ADOのRecordsetオブジェクトの操作方法について解説し、最近はクライアントライブラリを実装するために、コンポーネントの設計開発方法について説明してきた。結果が出るのは遅くても、その段階において、現在のVisual Basicプログラミングにとって重要な事柄を扱っていることがわかるだろう。この連載の目的通りに利用しなくても、応用の効く技術ばかりを紹介しているのである。

サービス精神も重要

 途中で一回、前号までのあらすじを載せたことがあった。しかしこの連載の目的をいまだに憶えている人こそが少ないのではないか? 一応、簡潔に説明しておくと、この連載は「安くて早いクライアント/サーバーシステム開発を実現するため」に書かれているのである。
 しかし、長い道のりでゴールばかりを見ていては、絶望的な気分になってくる。私は読者のために、もっとサービス精神に満ちた具体的なメリットを提供する必要がある。そう、私はこの連載を、いったい何人の人が読んでいるのかを気にしているのである。とはいっても、編集部からプレッシャーがかかってきたわけではない。しかし、そろそろ気にした方がいいのではないかと考えだしたのだ。

やがて訪れる転換点

 この連載には、やがて大きな転換点がおとずれる。今まではサーバーの実装と、サーバーと通信するためのクライアントコンポーネントの設計と実装について解説してきた。これは残りのひとつの点を除いて、だいたい完成している。
 残りのひとつとは、コンポーネントベースのデータベースの追加編集である。現在のHyper SQLdb Client Libraryはデータの表示こそデータ連結が可能だが、データベースの内容を更新するには、SQLのINSERT文やUPDATE文をプログラムで実行しなければならない。実はこれこそが、もっとも優れたデータベース操作方法なのだが、今日のデータアクセス手法に慣れた開発者には手間のかかるものに映るだろう。
 今回以降、コンポーネントベースでデータ更新ができるように、Hyper SQLdb Client Libraryを強化する。転換点が訪れるのは、その先である。Hyper SQLdb Client Libraryが実用的な最低限の機能を実装し終わった時点で、クライアントアプリケーションの開発が始まる。これは、今までのような低水準な処理ばかりではなく、もっと実務よりの、華やかなものになるはずである。

Hyper SQLdb Client Libraryの実装はVisual Basic 6.0のおかげ

 私はこのクライアントアプリケーションをVisual Basicで記述するつもりだった。つもりだったという意味あり気に書くからには、少しばかりここに至るまでの経緯を説明しておく必要がある。
 連載を始めるにあたって編集部の要望は、Microsoft Accessで業務システムを開発するための記事だったのである。そこで、私はサーバー側も実装することを提案し、できあがったら、Visual Basicなり、Microsoft Accessなりでクライアントの実装をしましょう、という具合に曖昧に開始したのである。しかし考えてみれば、Microsoft Accessのフォームとレコードセットが一体化したアーキテクチャでは、独自実装したサーバーから取得したデータを洗練された形でユーザーインターフェイスに結びつけることは難しい。やはり、Visual Basicを使い、非連結でFlex Gridコントロールあたりに表示するしかないと考えていたところに登場したのがVisual Basic 6.0である。発表当初、Visual Basic 6.0の新機能を高く評価していなかった(実は今でもしていない)私だが、データ連結クラスの作成機能と、Recordsetの作成機能は、この連載にぴったりの強化点だった。もっとも、ADOが配布されている現在、Recordset作成機能はVisual Basic 6.0特有の機能ではない。
 いずれにしてもVisual Basic 6.0のおかげで、ADOライクなプログラミングインターフェイスを用いたHyper SQLdb Client Libraryがここまで完成したのである。

Microsoft Access 2000への期待

 さて、一方Microsoft Aceessだが、次期バージョンであるAceess 2000では、当然、ADOへの対応が強化されているはすである。ならばADO同様、COMコンポーネントであるHyper SQLdb Client LibraryがAccess 2000で無理なく使える可能性も高いではないか? そもそも私はVisual Basicでデータベースシステムを作成したいわけではないのである。
 Visual Basicは汎用プログラミング言語であり、Microsoft Accessはデータベース専用プログラミング環境である。特化した機能や高水準のアーキテクチャなど、Visual BasicはMicrosoft Accessに遠く及ばない。Visual Basic 5.0までは、まともなレポート機能すらなかったのである(ごめんなさい。Visual Basic 6.0のレポートツールがまともかどうかは、まだ評価していません)。しかし、実際に開発してみれば、Microsoft Accessのアーキテクチャの洗練のなさや障害の多さといった完成度の問題によって、Visual Basicの方がましかなというのが現状だったのである。Visual Basicは汎用言語だけあって、いざという時の逃げ場が多いのである。また、Microsoft Accessには、リモートデータベースにアクセスする上でのアーキテクチャ上の致命的な問題点があった。もっとも、本連載で開発しているデータベースサーバーは、直接サーバーとSocket通信をするので、このことは問題にならない。
 とにかく、 Visual Basic 6.0がクライアントコンポーネントの実装において救世主となったように、新しいAccess 2000が、クライアントアプリケーション実装の救世主となる可能性もあるのである。

実装方法ばかりでなくノウハウの提供も…

 しかし、ここで気がついた。前々号より堀川明氏の「AccessユーザーのためのC/S開発入門講座」が開始しているではないか。似たような記事は同じ雑誌に2つもいらない。これはVisual Basicでクライアントアプリケーションを実装せよ、という編集部の意図なのだろうか? とりあえず、このことは後で考えよう。
 ようするに、私は転換点、すなわちクライアントアプリケーションの実装を開始するのは、Access 2000の評価が終わってからにしたいと考えているのである。それまではHyper SQLdb Client Libraryの強化を進める。ただし、若干、書き方を変えようと思っている。「安くて早いクライアント/サーバーシステム開発を実現するため」なのは今まで通りなのだが、それを実現するためのプロダクトの開発プロセスを記述していくだけでなく、「安くて早いクライアント/サーバーシステム開発を実現するためのプロダクトを開発するプロセスで蓄積されたノウハウの提供」に重点を置こうと考えているのである。

今回はCollectionオブジェクト

 そこで、今回はCollectionオブジェクトを扱うことにする。本稿を読むことで読者の方はHyper SQLdb Client LibraryのFieldsコレクションの実装内容が理解できるばかりでなく、Collectionオブジェクトの意義や使用法も理解できるようになるのである。
 前回までのHyper SQLdb Client LibraryではFieldsオブジェクトのダミーとして、ADOのRecordsetオブジェクトのFieldsコレクションの参照をFieldsプロパティで渡していた。この方法はADOのRecordsetオブジェクトの内容をダイレクトで操作できるという利点の反面、値が操作されたときにVisual Basicプログラムでハンドリングできないという問題点がある。それはADOのRecordsetオブジェクトやFieldsオブジェクトに、データが書きかえられたとき発生するようなイベントが用意されていないからである。データの変更を検知する方法がないと、データ更新を自動化するプログラムを記述する方法は難しい。
 データ更新を自動化するひとつの方法として、更新の指示が出されたときにFieldオブジェクトのOriginalValueプロパティの値とValueプロパティの値を比較し、違いがあるようならば更新のための処理を実行する、というやり方がある。この方法はかなり有効な手段である。唯一の欠点は、バッチアップデートを実現しようとすると一ヶ所の変更のために全部のレコードとフィールドを走査しないといけないという点である。これは取得したレコードセットのサイズが大きい場合に致命的な欠点となる。
 クライアント/サーバーデータベースシステムにおいて、巨大なレコードセットを作成することそのものが間違いとはいえ、バッチアップデートのサポートの有無は重要であるため、Hyper SQLdb Client Libraryではこの方法を採用しない。

先月号の堀川氏のサンプルとの違い

 ところで、本誌6月号の特集で堀川氏が書いた「ソケットサーバーで一味違うSQL Serverプログラミングを」を読んだ方は、サンプルプログラムが本稿のサンプルであるDB Serverと酷似していることに気づいただろう。堀川氏のサンプルはMicrosoft SQL Serverをターゲットに書かれてるが、ConnectionオブジェクトのProviderプロパティをJetデータベースエンジン用に書きかえれば簡単に移植できる。
 堀川氏のサンプルと本稿のサンプルの違いについて簡単に説明しておこう。本稿のサンプルでは、今のところデータの更新をするのにデータ操作のためのSQL文を実行するしかないが、氏のサンプルではすでにRecordsetオブジェクトベースのデータ更新がサポートされている。氏のサンプルでは、この更新機能を実現するためにレコードセットマーシャリングを使用している。これはMicrosoftのRDSと類似しており、ひとつの有効な方法である。この方法ではクライアント側にサーバー側で作成したRecordsetオブジェクトのダミーを用意し、そのレコードセットに加えられた変更をサーバー側に通知し、サーバー側でRecordsetオブジェクトベースで更新を実現する。
 氏自身が記事の冒頭で、このサンプルが1行ごとに処理を行なっているのはサンプルだからであり、数十行レコード単位で処理するように改良べきだと記述している。バッチ更新のすすめである。ちなみにRDSではバッチ更新処理をサポートしている。
 RDSや氏のサンプルの特徴は、サーバー側でRecordsetオブジェクトが待機することである。RDSはInternet Information Server(あるいはPersonal Web Server)を利用する都合で、セッションレスで動作する。一方、氏のサンプルではセッションが維持されているが、サーバー側にRecordsetオブジェクトが待機しているという意味では同じである。これらの方法はサーバー側のレコードセットをサーバーカーソルと考えることもできる。
 これに対し、本稿のサンプルであるDB Serverは、検索を実行し、クライアントに結果を送り返した時点でRecordsetオブジェクトを破棄する。では、データの更新はどのように行なうのだろうか?
 方法はひとつしかない。クライアントからSQL文を投げることである。Hyper SQLdb Client Libraryの今後の強化点は、データ編集用のSQL文の自動生成機能である。これを実装することで、開発者はRecordsetオブジェクトを操作するだけで、データの編集が可能になる。

カーソルをどこにもつか、それが問題だ

 この方法は目新しいものではない。RDOも内部的には同様の動作をする。もっとも、Hyper SQLdb Client Libraryが最大に影響を受けているのは、RDOやADOではなく、Powersoft PowerBuilderに搭載されているデータウィンドウである。このデータアクセスと表示のためのオブジェクトは、C++開発ツールであるPowersoft Power++にもバンドルされている。データウィンドウは、データアクセスのためのオブジェクトとデータ表示のためのオブジェクトがひとつにまとめられており、私は今でもこのオブジェクトがリモートデータベースアクセスのためのもっとも理想的なアーキテクチャだと信じている。
 ちなみにPowersoft Power++に付属するデータウィンドウはActiveXコントロールを経由して動作するが、Visual Basicから利用することができない。残念である。
 サーバー側にレコードセット(カーソル)をもつべきかもたないべきかというのは、クライアント/サーバーシステムに対する姿勢の問題である。どちらかがすぐれていて、どちらかが劣っているというわけではない。運用条件によって有利不利がかわってくる。ただ、RDSのようにセッションレスでサーバーにカーソルを保持するのは、サーバーリソースにとって非効率的な状況が生まれやすいことは指摘しておく。しかし、だからといって、RDSが劣ったアーキテクチャだというわけではない。私は本誌の99年5月号の特集で「Microsoftのデータベースアクセス手法が目指すもの」というMicrosoftのデータアクセスオブジェクトを総括する記事を掲載し、そこではRDSを紹介しなかったが、実はMicrosoftの提供するデータアクセスメソッドとしてはRDSを最大に評価しているのである。

私がサーバーカーソルの嫌いな理由

 私がサーバーリソースを性急に解放したがるのは、サーバーの負荷というものはクライアントの負荷以上に推測が難しいからである。推測ができないならば、できるだけ余裕を与えた方がいいという単純な発想である。また、サーバーにレコードセットを作成するにしても、更新可能な重いレコードセットを作成するよりは、読取専用の軽いものを作成した方がいいという配慮もある。これは単に、Microsoftの更新可能なRecordsetオブジェクトの性能を信じていないからである。これらはデータ接続のためのドライバ(ODBCやOLE DB)と協調として動作するため、ドライバの出来・不出来に性能が大きく左右される。実際、これらがどのようなメカニズムで内部的に更新機能を実現しているかは、資料が提供されないので動作を実験で解析しないと把握することができない。
 これらが効率よく動作するという前提さえあれば、サーバー側にレコードセットを保持する方法にメリットは大きい。要はMicrosoftやドライバベンダーをどれだけ信用できるかという問題だということである。

Collectionオブジェクト

 Collectionオブジェクトはオブジェクトを管理するための配列である。複数の同一オブジェクトを管理するためのメソッドとプロパティを提供する。単に効率(パフォーマンス)を追求するだけならば、Collectionオブジェクトを使うよりも、オブジェクト配列を使う方が性能はいい。しかし、Collectionオブジェクトにはオブジェクト管理のために、すでにいくつかの機能が用意されおり、これらのメソッドやプロパティは標準化されているので、利用するメリットは大きい。もっとも、これらの管理機能は強力だというわけではないので、相当するものを自力で作成し愛用するのもいいだろう。
 Collectionオブジェクトに関しては、Visual Basicのヘルプはわかりにくい。私はヘルプに従ってコードを記述したが、最後まで動作させることができなかった。結局、最後に頼ったのは、クラスビルダユーティリティが生成したソースコードである。実際にはクラスビルダユーティリティが生成したコレクションオブジェクトを動作させることも私はできなかった。しかし、そのソースコードを別プログラムに埋め込むことで期待の動作を得ることができた。この結論に至るまでに一昼夜を費やし、一時はサンプルを完成させることができないのではないかという不安にとらわれた。

なぜ、コレクションなのか?

 今回のサンプルでコレクションを利用する理由を再確認しておこう。それは、Fieldオブジェクトの参照を容易にするためである。Recordsetオブジェクトは複数のFieldオブジェクトを包含することで柔軟にデータを扱うことができるようになっている。
 たとえば、次のコードでは著者IDの内容をメッセージボックスに表示することができる。

MsgBox rs.Fields("AU_ID").Value

 上記のサンプルではキーを使用して、参照するオブジェクトを指定しているのに対して、次のようにインデックス番号を使用して指定することも可能である。

MsgBox rs.Fields(1).Value

 これはコレクションに要素を追加するときにキーを設定しているからである。

デフォルトプロパティの設定によるコード記述の柔軟性

 ちなみに上記のプログラムは次のように記述しても同じ結果を得ることができる。

MsgBox rs.Fields("AU_ID")

MsgBox rs.Fields(1).Value

MsgBox rs.Fields(1)

MsgBox rs(1).Value

MsgBox rs(1)

 このような記述が可能なのはCollectionオブジェクトの特徴ではない。Racordsetオブジェクトのデフォルト項目としてFieldsコレクションが指定されていて、かつ、FieldオブジェクトのデフォルトプロパティとしてValueプロパティが指定されているからである。
 このように柔軟な記述ができることの是非はともかくHyper SQLdb Client LibraryはADOとの互換性を重視しているので、このように記述法もできる限りサポートしたい。そのためにはFieldsコレクションやFieldオブジェクトを適切に作成する必要がある。

Collectionオブジェクトのインターフェイス

 Collectionオブジェクトは次のようなインターフェイスをサポートする。

Add メソッド コレクションに新しい要素を追加する
Itemメソッド 指定したコレクションの要素を取得する
Removeメソッド コレクションが既存の要素を削除する
Countプロパティ コレクションの要素数を取得する

 これらを組み合わせて使用することで、コレクションの管理を行なうのである。
 では、コレクションを作成する前に、コレクションされるオブジェクト、この場合、Fieldオブジェクトを作成する。

Fieldオブジェクトのインターフェイス

 FieldオブジェクトはADOのFieldオブジェクトをシミュレートするため、いくつかのプロパティを実装する。ADOのFieldオブジェクトには、次のようなインターフェイスが用意されている。

ActualSizeプロパティ
Attributesプロパティ
DefinedSizeプロパティ
Nameプロパティ
NumericScaleプロパティ
OriginalValueプロパティ
Precisionプロパティ
Typeプロパティ
UnderlyingValueプロパティ
Valueプロパティ
AppendChunkメソッド
GetChunkメソッド

 Hyper SQLdb Client Libraryがこれらのインターフェイスを完璧にシミュレートするわけではない。まず、2つのメソッドは今回はサポートしない。これは近い将来にサポートされる。次にTypeプロパティは、同名のプロパティ関数をVisual Basicで使用できないという理由によって、DataTypeという名称に変更されている。実はNameプロパティも、クラスビルダユーティリティでははじかれてしまうのだが、ソースコードを直接変更すると問題なく動作する。あまり望ましくない解決方法だが、とりあえずこれで様子をみよう。

ADOのRecordsetオブジェクトのプロパティをマッピング

 リスト1を見てもらうとわかるように、ほとんどのプロパティは、ADOのRecordsetオブジェクトのプロパティをマッピングしているだけである。なお、RecordsetオブジェクトをRecordsetコントロールと共有するため、Recordsetオブジェクト変数の宣言は、Recordsetコントロール内からモジュールに移動している。
 単純なマッピングではない唯一の例外は、次のValueプロパティである。

Public Property Let Value( _
 ByVal vData As Variant)
    MsgBox Name & "項目の値が'" & _
     dsRecordset.Fields(Name).OriginalValue & _
     "'から'" & vData & "'に変更されます。"
    dsRecordset.Fields(Name) = vData
End Property

 もちろん、このプロパティプロシージャは、完成したものではない。本来は、メッセージボックスを表示する代わりにSQLのUPDATE文の生成処理がここに記述される。Valueプロパティをカスタマイズしたいというだけの理由でFieldオブジェクトを作成し、他のプロパティをマッピングしているのである。

リスト1:Fieldオブジェクト(Field.cls)のソースリスト
Public Name As String
Private mvarActualSize As Long
Private mvarAttributes As Long
Private mvarDefinedSize As Long
Private mvarNumericScale As Byte
Private mvarOriginalValue As Variant
Private mvarPrecision As Byte
Private mvarDataType As Long
Private mvarUnderlyingValue As Variant

Public Property Get UnderlyingValue() As Variant
    UnderlyingValue = dsRecordset.Fields(Name).UnderlyingValue
End Property

Public Property Let DataType(ByVal vData As Long)
    dsRecordset.Fields(Name).Type = vData
End Property

Public Property Get DataType() As Long
    DataType = dsRecordset.Fields(Name).Type
End Property

Public Property Get Precision() As Byte
    Precision = dsRecordset.Fields(Name).Precision
End Property

Public Property Get OriginalValue() As Variant
    OriginalValue = dsRecordset.Fields(Name).OriginalValue
End Property

Public Property Let NumericScale(ByVal vData As Byte)
    dsRecordset.Fields(Name).NumericScale = vData
End Property

Public Property Get NumericScale() As Byte
    NumericScale = dsRecordset.Fields(Name).NumericScale
End Property

Public Property Get DefinedSize() As Long
    DefinedSize = dsRecordset.Fields(Name).DefinedSize
End Property

Public Property Let Attributes(ByVal vData As Long)
    dsRecordset.Fields(Name).Attributes = vData
End Property

Public Property Get Attributes() As Long
    Attributes = dsRecordset.Fields(Name).Attributes
End Property

Public Property Get ActualSize() As Long
    ActualSize = dsRecordset.Fields(Name).ActualSize
End Property

Public Property Let Value(ByVal vData As Variant)
    MsgBox Name & "項目の値が'" & dsRecordset.Fields(Name).OriginalValue & _
     "'から'" & vData & "'に変更されます。"
    dsRecordset.Fields(Name) = vData
End Property

Public Property Get Value() As Variant
    Value = dsRecordset.Fields(Name).Value
End Property

クラスのデフォルトプロパティの設定

 前述のように次の2つのコードが同じように動作するためには、Valueプロパティをデフォルトに設定する必要がある。

MsgBox rs.Fields("AU_ID").Value
MsgBox rs.Fields("AU_ID").Value

 そのためには、クラスビルダユーティリティでプロパティを追加するときに「プロパティビルダ」ダイアログの「規定プロパティ」チェックボックスをチェックする(図1)。ところで、この「規定プロパティ」チェックボックスを一度チェックして確定してしまうと2度と解除することができない。この回避方法は後述する。

図1:「プロパティビルダ」ダイアログの「規定プロパティ」チェックプロパティをチェック
図1:「プロパティビルダ」ダイアログの「規定プロパティ」チェックプロパティをチェック

コレクションの実装

 Fieldオブジェクトのインターフェイスが実装しおわったところで、コレクションの実装を解説する。コレクションの作成をRecordsetコントロール内で行なうのは、Recordsetの作成と同時にコレクションの作成を行なうのと、コレクションの参照を取得するFieldsプロパティがRecordsetコントロールのメンバだからである。
 たとえば、次のようなコード記述が可能なコレクションを作成するには、2つの方法が考えられる。

 Value = rs.Fields(Name).Value

 ひとつはコンポーネント内でCollectionオブジェクトを作成し、Collectionオブジェクトの参照をFieldsプロパティで取得する方法、もうひとつは、Fieldsコレクションオブジェクトを作成する方法である。
 Visual Basicのクラスビルダユーティリティはコレクションオブジェクトの作成をサポートする。Fieldsコレクションオブジェクトを作成するには、コレクションビルダの名前項目にFieldsと入力し、メンバクラスからFieldクラスを選択する(図2)。

図2 コレクションビルダでFieldsコレクションオブジェクトを作成
図2 コレクションビルダでFieldsコレクションオブジェクトを作成

 クラスビルダユーティリティが生成したコードがリスト2である。コレクションクラスというと、何か特殊なクラスのような印象をもつが、Collectionオブジェクトをラップした普通のクラスである。コレクションビルダはCollectionオブジェクトのマッピングを自動化する。ソースリストをみるとAddメソッドやCountプロパティがマッピングされているのがわかる。

リスト2:クラスビルダユーティリティが生成したFieldsコレクションクラス
' コレクションを保持するローカル変数
Private mCol As Collection

Public Function Add(Name As String, Optional sKey As String) As DBCliLib.Field
    ' 新規オフジェクトを作成します
    Dim objNewMember As DBCliLib.Field
    Set objNewMember = New DBCliLib.Field

    ' メソッドに渡すプロパティを設定します
    objNewMember.Name = Name
    If Len(sKey) = 0 Then
        mCol.Add objNewMember
    Else
        mCol.Add objNewMember, sKey
    End If

    ' 作成されたオブジェクトを返します
    Set Add = objNewMember
    Set objNewMember = Nothing
End Function

Public Property Get Item(vntIndexKey As Variant) As DBCliLib.Field
    ' コレクションの要素を参照するときに使用します
    ' vntIndexKeyはインデックスまたはキーのどちらかを
    ' 保持するためにVariantで宣言されています
    ' 構文: Set foo = x.Item(xyz) または Set foo = x.Item(5)
    Set Item = mCol(vntIndexKey)
End Property

Public Property Get Count() As Long
    ' コレクションの要素数を取得するときに使用します
    ' 構文: Debug.Print x.Count
    Count = mCol.Count
End Property

Public Sub Remove(vntIndexKey As Variant)
    ' コレクションから要素を削除するときに使用します
    ' vntIndexKeyはインデックスまたはキーのどちらかを
    ' 保持するためにVariantで宣言されています
    ' 構文: x.Remove(xyz)
    mCol.Remove vntIndexKey
End Sub

Public Property Get NewEnum() As IUnknown
    ' このプロパティは、For...Each 構文を使用して
    ' コレクションを列挙できるようにします
    Set NewEnum = mCol.[_NewEnum]
End Property

Private Sub Class_Initialize()
    ' このクラスが作成されたときに、コレクションを作成します
    Set mCol = New Collection
End Sub

Private Sub Class_Terminate()
    ' このクラスが終了するときに、コレクションを破棄します
    Set mCol = Nothing
End Sub

Addメソッドはちょっと違う

 ただ、 Addメソッドの記述は単なるマッピングではない。要素となるオブジェクトのインスタンスを作成するコードが追加されているのと、最初の引数がオブジェクト型ではなくString型として指定されている。また、受け取った引数をクラスのNameプロパティに代入している。CollectionオブジェクトのAddメソッドの最初の引数には、オブジェクト変数が指定されるのが一般的だから、この違いは大きい。クラスビルダユーティリティが生成するコレクションオブジェクトは、Addメソッドの内部でクラスのインスタンスを作成するため、オブジェクト変数を指定する必要がないのである。しかし、これではコレクションオブジェクトとしてのインターフェイスの統一性に問題があるような気もするのだが、とりあえず忘れることにしよう。

参照する方法がわからない

 それ以上に忘れられないのは、このオブジェクトを参照する方法がわからないことである。次のようにFieldsコレクションを扱うには、RecordsetコントロールのメンバとしてFieldsコレクションが扱われねばならない。

Value = rs.Fields(Name).Value

 Visual Basicのクラスは別のクラスをメンバとして定義することはできないので、クラス内で他のクラスのインスタンスを作成して、その参照を保持するオブジェクト変数を公開することになる。
 しかし、さまざまな方法を使って試したが、うまくいかない。こんなことではいけないのだが、とっくに締め切りを過ぎているという時間的制約もある。しかたがないが、これも忘れてしまおう。だからといって、今回のサンプルが完成しないということではない。クラスビルダユーティリティが生成したコレクションオブジェクトをあきらめるというだけのことである。

あきらめたFieldsコレクションのかわりに…

 つまり、Fieldsコレクションオブジェクトを作成せずに、汎用のCollectionオブジェクトを使ってFieldオブジェクトを管理する方法である。リスト3では、Collectionオブジェクトを実体化し、Fieldオブジェクトをアイテムとして追加するために追記したコードを網掛けで示している。
 次のコードはFieldオブジォクトのインスタンスを作成してから、Nameプロパティにフィールド名を代入し、Collectionオブジェクトの要素として追加している。

Dim fieldObj As DBCliLib.Field
Set fieldObj = New DBCliLib.Field
fieldObj.Name = AppendParamName
fieldsCol.Add fieldObj, AppendParamName
Set fieldObj = Nothing

 このCollectionオブジェクトを外部から参照できるようにしているのが、次のプロパティ関数である。

Public Property Get Fields(index As Variant) _
 As DBCliLib.Field
    Set Fields = fieldsCol(index)
End Property

 ここでは戻り値として、Collectionオブジェクト変数が指定されている。結果的Collectionオブジェクトに外部からFieldsという名称でアクセスすることができる。

リスト3:FieldオブジェクトをアイテムとしたCollectionオブジェクトをRecordsetコントロールに追加
     |
Private fieldsCol As Collection
     |
Dim m_Fields As Variant
     |
Public Property Get Fields(index As Variant) As DBCliLib.Field
  Set Fields = fieldsCol(index)
End Property
     |
Public Sub MakeRecordset(RecordsetString As String)
     |
  ' Dim fieldObj As DBCliLib.Field
  Set fieldsCol = New Collection ' DBCliLib.Fields
 
  Do While True ' intPosRow > 0
     |
    ' FieldをRecordsetオブジェクトに追加
    dsRecordset.Fields.Append AppendParamName, adLongVarChar, _
    AppendParamSize, adFldUpdatable ' adFldRowID
    Dim fieldObj As DBCliLib.Field
    Set fieldObj = New DBCliLib.Field
    ' コレクションに追加するFieldオブジェクトの
    ' Nameプロパティにフィールド名を設定
    fieldObj.Name = AppendParamName
    ' フィールド名をキーとして、コレクションにFieldオブジェクトを追加
    fieldsCol.Add fieldObj, AppendParamName
    Set fieldObj = Nothing
    ' Fieldオブジェクトの追加はフィールド数行なわれる
    
  Loop
   |
End Sub
 

ActiveXコントロールのデフォルトプロパティの設定

 さて、ここでもうひとつのテーマがある。フィールドのデータにアクセスするときに、次のどちらの記述方法でもアクセスできるようにする方法である。

MsgBox rs.Fields("AU_ID")

MsgBox rs ("AU_ID")

 FieldオブジェクトのValueプロパティをデフォルトプロパティとして設定したように、RecordsetコントロールのデフォルトプロパティとしてFieldsプロパティを指定すればいいことは想像がつく。しかし、クラスビルダユーティリティとは違い、ActiveXコントロールインターフェイスウィザードには、探した範囲ではデフォルトプロパティの設定項目が見つからない。
 それならば、Fieldオブジェクトのコードのどこかに、Valueプロパティがデフォルトであることを指定する記述があるはすだと考えるのは当然の成りゆきだろう。しかし、開発環境のどこを探しても、それらしいものはない。
 しかたがなく、Field.clsファイルをテキストエディタで開いてみると、Valueプロパティ関数の記述が次のようになっている。

Public Property Get Value() As Variant
Attribute Value.VB_UserMemId = 0
    Value = dsRecordset.Fields(Name).Value
End Property

 開発環境から同じ関数を見ると2行目が削除されている。
 それならば、Recordset.ctlをテキストエディタで開いて、次の一行をFieldsプロパティの宣言部の次の行に挿入すれば、デフォルトプロパティとして認識されそうである。

Attribute Fields.VB_UserMemId = 0

 大成功! こんなことをしていいのかどうかはわからないし、Visual Basicがバージョンアップしたときに扱いがどうなるかはわからないが、少なくとも現時点では正常に動作している。
 これで想像がつくと思うが、前述したようにクラスビルダユーティリティでデフォルトプロパティを一度設定したら解除できない問題は、該当行を削除することで解決できることになる。

新しいイベントをRecordsetコントロールに追加

 今回はCollectionオブジェクトを追加しただけでなく、Recordsetコントロールに2つのイベントを追加した。AfterMoveRecordイベントとBeforeMoveRecordイベントである。これらのイベントはコードでレコードセットを移動する前後に発生する。Recordsetコントロールに追加したコードはリスト4の網掛け部分のようになる。

リスト4:AfterMoveRecordイベントとBeforeMoveRecordイベントをRecordsetコントロールに追加
   |
Event AfterMoveRecord()
Event BeforeMoveRecord()
   |
Public Sub MoveFirst()
  RaiseEvent BeforeMoveRecord
  dsRecordset.MoveFirst
  RaiseEvent AfterMoveRecord
End Sub
 
Public Sub MovePrevious()
  RaiseEvent BeforeMoveRecord
  dsRecordset.MovePrevious
  RaiseEvent AfterMoveRecord
End Sub
 
Public Sub MoveNext()
  RaiseEvent BeforeMoveRecord
  dsRecordset.MoveNext
  RaiseEvent AfterMoveRecord
End Sub
 
Public Sub MoveLast()
  RaiseEvent BeforeMoveRecord
  dsRecordset.MoveLast
  RaiseEvent AfterMoveRecord
End Sub
   |

新しいHyper SQLdb Client Libraryのサンプル

 新しいHyper SQLdb Client Libraryを利用したクライアントプログラムのサンプルが図3である。今回からHyper SQLdb Client Libraryとサンプルをプロジェクトグループ化したのでサンプルの実行が簡単になった。Type9 DB Serverを実行しているのと同じマシンで、CD-ROMに収録されているClientLib.vbgをVisual Basic 6.0で実行すれば動作できるはすである。
[AU_Id項目に値を代入]ボタンをクリックすると次のプロシージャが実行され、「Au_ID」フィールドに値が代入される。

Private Sub Command1_Click()
    rs("Au_ID") = 1000
End Sub

 結果としてFieldオブジェクトのValueプロパティ関数に記述された次のコードが実行され、図4のメッセージが表示される。

MsgBox Name & "項目の値が'" & _
 dsRecordset.Fields(Name).OriginalValue & _
 "'から'" & vData & "'に変更されます。"

 また、[前へ][次へ]ボタンをクリックすると、次のイベントプロシージャが実行され、カレントレコードが移動する。

Private Sub cmdPrev_Click()
    rs.MovePrevious
End Sub

Private Sub cmdNext_Click()
    rs.MoveNext
End Sub

 結果的にRecordsetコントロールのBeforeMoveRecord イベントやAfterMoveRecordイベントが発生し、以下のイベントプロシージャが実行される。

Private Sub rs_AfterMoveRecord()
   MsgBox "レコードセットを移動しました"
End Sub

Private Sub rs_BeforeMoveRecord()
   MsgBox "レコードセットを移動します"
End Sub

図3:リスト5の実行画面
図3:リスト5の実行画面

図4:FieldオブジェクトのValueプロパティに値を代入したときに表示されるメッセージ
図4:FieldオブジェクトのValueプロパティに値を代入したときに表示されるメッセージ

リスト5:アップデートされたHyper SQLdb Client Libraryを使用したサンプル
Option Explicit

Private Sub cmdConn_Click()
    ' Set cn = Controls.Add("DBCliLib.Connection", "control1")
    cmdConn.Enabled = False
    cmdSend.Enabled = True
    
    cn.ServerName = "localhost"
    cn.ServerPort = 1010
    cn.cnOpen "BIBLIO"
End Sub

Private Sub cmdNext_Click()
    rs.MoveNext
End Sub

Private Sub cmdPrev_Click()
    rs.MovePrevious
End Sub

Private Sub cmdSend_Click()
    ' Set rs = Controls.Add("DBCliLib.Recordset", "control2")
    rs.rsOpen "SELECT * FROM Authors WHERE Author like 'a%'", cn
End Sub

Private Sub Command1_Click()
    rs("Au_ID") = 1000
End Sub

Private Sub rs_AfterMoveRecord()
    MsgBox "レコードセットを移動しました"
End Sub

Private Sub rs_BeforeMoveRecord()
    MsgBox "レコードセットを移動します"
End Sub

Private Sub rs_RecordsetComplete()
    Set DataGrid1.DataSource = rs
End Sub

次号では

 サンプルではグリッドにデータ連結している。このサンプルを操作した読者のなかには、致命的な問題があることに気づいたかもしれない。ボタンをクリックして値を代入したり、レコードを移動した場合にはメッセージボックスが表示されるが、連結グリッドを操作してカレントレコードを移動したり、値を変更してもメッセージボックスは表示されない。これではユーザーの操作をプログラムで完全にハンドリングすることはできない。
 この問題を回避するためには、連結グリッドとレコードセットの間にもプログラムコードが介入するたの階層を挟み込む必要がある。次号では、この間に入るオブジェクトとしてOLE DBプロバイダを利用する方法を解説する。


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

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