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

第14回
Connectionコントロールの実装

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



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



ピンチ

 私は今ピンチに立たされている。人生最大というほどではないが、かなり重大なピンチである。そのピンチというのは、言うまでもなく、この原稿の締め切りのことである。実は、この原稿の締め切りが何日であるかを私は知らない。編集者は締め切りが何々日と指定してくるが、それはかなりのサバを読んでいるものであることが多い。だから、そういうのは最初に聞いてもすぐに忘れてしまう。原稿の真の締め切りとは、雑誌掲載が間に合う最終日のことである。それは、編集者の原稿催促の緊迫感や、あるいは原稿を落とした過去の経験から判断するしかない。連載の場合は、毎月、特定の日付けが締め切りでないかと考える人もいるかと思う。しかし、月によって進行は異なるし、あるいは同じ号に執筆するライターの状況などによって微妙に変わってくる。つまり、私、一人が遅れるならば問題がない場合でも、同時に遅れる人が何人もいるとアウトになるのである(よくご存知で…:編集部注)。
 このようにライターと編集者は、読者の方々によりよい情報を提供するため、毎月、緊迫感と焦燥にとらわれながら生きているのである。

そして限界が…

 そして、限界はすでにせまりつつある。実は半日前にも原稿を書こうとワープロソフトを起動したのだが、書き出しがうまくいかずに、何となく、手元に届いたばかりのVisual Basicマガジンを開いてしまったのである。4月号のVisual Basicマガジンが手元にあるというだけで、事態がいかに緊迫しているかわかるだろう。せめてもの救いは、現在電話回線状態に問題があって、メールが読めないことである。おかげで私は本稿担当の敏腕編集者の過激な原稿催促から、身をかわすことができる。しかし、だからといって締め切りが延びるわけではない。
 書き出しのネタに困ったときは「電脳十牛図」を読むのが一番である。ここには、今日のソフトウェア開発を巡る憂うべき状況が的確に表現されている。4月号では2000年問題が扱われている。しまった、私は2000年問題について高い認識の持ち主ではない。これは、2000年問題を回避することは大変だが、2000年問題の発生しにくいソフトウェアを新規に開発することは難しくないという理由による。

禁断の果実と賭け

 そういえば、私は3月号の本連載で、VBでできる安全なマルチスレッドアプリケーション開発の方法についてW.Stealride氏から聞きそびれた話を書いた。私は知らなかったのだが、彼がVBUG(Visual Basic Users Group)の主催するカンファレンスで同テーマについて講演したことを最近聞いた。そのときのサンプルコードソースを見ると、APIのCreateThread関数を使用している。確か彼は“CreateThread関数を使用しない方法”と言っていた気がするのだが、確かではない。いずれにせよ、その道のエキスパートであるW.Stealride氏が紹介する技法は、いわば、F1マシンの開発技術のようなものだ。強力なパワーを発揮するのには最適だが、公道で走らせるためにはそのための調整をすることが必要になる。そのサンプルコードを私は怖くてまだ試していないのだが、見ただけで、かなり、強力なコードであることがわかる。
 3月号の本連載に掲載したマルチスレッドアプリケーションの開発方法が、COMのスレッド管理機能を利用するものであるのに対して、氏の方法はスレッド管理そのものもVisual Basicのプログラムで制御している。COMのスレッド管理を使用するということは、COMのスレッド管理機能を信用するということでもある。これはひとつの賭けになる。一方、Visual BasicコードでCreateThread関数を使用することは、禁断の果実を口にする行為に等しい。果実の快楽は保証されているにしても、その償いをどこで支払わなければならないのかは想像すらできない。いずれにしても、プログラマは選択を迫られることになる。それは、十分な調査によって決定すべきことだろう。
 ところで、同号の連載において、間違えた記述があった。記事中で私は「CreateObject関数を使用してレイトバインディングを行なう」と書いたのだが、バインディングのタイミングはインスタンシングの方法にではなくて、変数の宣言方法によって決定される。もっとも、バインディングのタイミングは、アプリケーションの動作に影響はない。実行速度には影響するが、それも通常の利用状態における差は少ない。

コンポーネントの実装

 前回はクライアント側の機能をコンポーネント化し、その使用方法を説明した。今回はコンポーネント化の実装について解説する。本連載の趣旨はコンポーネントの作成方法の解説ではないので、内部について説明する必要はないかもしれないが、実装において興味深い点も多いので扱うことにする。コンポーネント化するのは、コードの再利用性を高めるための究極的な手段である。同様の目的を持つオブジェクト指向と比較しても、コンポーネントベースの開発方法は、単純さゆえに敷居が低い。ただ、構造化における汎用ルーチンの作成の場合にも似て、コンポーネント化すること自体のコストが発生する。コンポーネント化のメリットは、社内でのコードの共有というよりは、コンポーネントビルダーとコンポーネントユーザーが別々に成立できることにある。
 最近のMicrosoftの技術資料において、COMについて触れていないものは少ない。COMの内部的な知識がないとプログラムが作成できないかのような表現である。しかし、コンポーネント化することで、従来できなかったことが可能になるという例は少ない。一方でプログラムの開発時に言語仕様への依存度も低くなっている。Visual Basic付属のコントロールにしろ、サードパーティのコントロールにしろ、コンポーネントの正しい使用法を理解し、適切に利用することが可動性の高いプログラムを作成するコツだといえるだろう。

Hyper SQLdb Client LibraryはVisual Basic 5.0で使えるか?

 前回紹介したHyper SQLdb Client Libraryは、Type9 DB Serverへのデータベースアクセス機能がコンポーネント化されている。多くの点でADOのインターフェイススタイルを継承しているため、ADOによるプログラミングの経験者ならば、慣れるのは容易だろう。
 ところで、現在、MicrosoftはData Access SDKと呼ばれる開発キットを英語版で公開している。ADOが含まれたこのSDKは、MicrosoftのWebサイトからダウンロード可能である。これを使えば、ADOを使用しているHyper SQLdb Client LibraryをVisual Basic 5.0でも使用できるのではないかと考えたが、考えは甘かった。Hyper SQLdb Client Libraryでは、Visual BasicのClassのデータ連結機能を使用しているが、その機能はVisual Basic 5.0のClassではサポートしていない。Visual Basicもそれだけ進歩しているということである。もっとも、Hyper SQLdb Client Libraryを使用してアプリケーションを開発する分には、Visual Basic 5.0でも問題はない。というより、ActiveXコントロールをサポートしている開発環境ならばすべて使えるはずである。
 Hyper SQLdb Client Libraryは、現在のところ、ConnectionコントロールとRecordsetコントロールという2つのコンポーネントによって構成されている。これらはひとつのプロジェクトにまとめられているため、使用するときにはひとつのコンポーネントを参照設定するだけでいい。

Connectionコントロール

 Connectionコントロールには、図1のようにWinsockコントロールが配置されている。このWinsockコントロールのオブジェクト名はwscConnectである。このコントロールはWindowlessプロパティが、Trueに設定されている。このようなウィンドウレスコントロールはウィンドウハンドルを持たないため、若干だが、リソースを節約することができる。

図1:Winsockコントロールが配置されたConnectionコントロールのデザイン画面
図1:Winsockコントロールが配置されたConnectionコントロールのデザイン画面

3つのメソッドと2つのプロパティ

 記事末のリスト1がConnectionコントロールのソースコードの全文である。Connectionコントロールが公開しているインターフェイスは、次のように3つのメソッドと2つのプロパティである。


cnOpenメソッド

cnCloseメソッド

Executeメソッド

ServerNameプロパティ

ServerPortプロパティ


 Connectionコントロールを使用してDB Serverに接続するための手続きは次のようになる。

(1) ServerNameプロパティ(デフォルトはLocalhost)とServerPort(デフォルトは1010)プロパティを設定
(2) サーバーのODBCのデータソース名を指定してcnOpenメソッドを実行
(3) データベースアクセス処理を実行(Executeメソッド、Recordsetコントロール)
(4) cnCloseメソッドを実行

ServerNameプロパティとServerPortプロパティ

 ServerNameプロパティは値の取得と設定が可能である。これらのプロパティは次のように実装されている。

Public Property Get ServerName() As String
    ServerName = wscConnect.RemoteHost
End Property

Public Property Let ServerName( _
 ByVal New_ServerName As String)
    wscConnect.RemoteHost() = New_ServerName
    PropertyChanged "ServerName"
End Property

 コードをみるとわかるように、ServerNameプロパティに設定された値は、そのまま「wscConnect」WinsockコントロールのRemoteHostプロパティに設定されている。また、値の取得時にも、同プロパティの値を取得し、そのまま返している。
 また、ServerPortプロパティに対応しているのは、次のように「wscConnect」WinsockコントロールのRemoteHostプロパティである。

Public Property Get ServerPort() As Long
    ServerPort = wscConnect.RemotePort
End Property

Public Property Let ServerPort( _
 ByVal New_ServerPort As Long)
    wscConnect.RemotePort() = New_ServerPort
    PropertyChanged "ServerPort"
End Property

 これらのプロパティをデフォルトで設定しているのは、次のプロシージャである。

Private Sub UserControl_ReadProperties( _
 PropBag As PropertyBag)
    wscConnect.RemoteHost = PropBag.ReadProperty _
     ("ServerName", "localhost")
    wscConnect.RemotePort = PropBag.ReadProperty _
     ("ServerPort", 1010)
End Sub

 ポート番号の1010は、Type9 DB Serverのデフォルトのポート番号であり、Type9 DB Server自身にはポート番号を設定する機能はない。つまり、クライアント側でServerPortプロパティを変更する場合には、Type9 DB Serverのプログラムを変更して再コンパイルする必要がある。

cnOpenメソッドの実装

 この2つのプロパティを設定すれば、サーバーに接続する準備が終了する。実際にサーバーに接続するには、cnOpenメソッドを実行する。
 cnOpenメソッドの実装部は次のようになっている。

Public Sub cnOpen( _
 Optional ConnectionString As String, _
 Optional UserID As String, _
 Optional Password As String, _
 Optional OpenOptions As Long)
    DSN = ConnectionString
    If wscConnect.State <> sckClosed Then _
     wscConnect.Close
    wscConnect.Connect
End Sub

 cnOpenメソッドの引数で指定されたODBCのデータソース名は、モジュールレベル変数であるDSN変数に格納される。この値は、このプロシージャ内では利用されない。
 次にWinsockコントロールのStateプロパティを使用して、現在の接続状態を取得する。もしサーバーとの接続が切断されていないならば、明示的に切断し、それからWinsockコントロールのConnectメソッドを使用して接続を確立する。これらの処理によってWinsockコントロールとサーバーとの接続は完了するが、Connectionコントロールがサーバーとの接続を終了するには、データソース名を送信する必要がある。
 Winsockコントロールは、接続処理が完了したときにConnectイベントが発生する。それに対応したイベントプロシージャが、次に示すサブプロシージャである。

Private Sub wscConnect_Connect()
  wscConnect.SendData LOCAL_HOST & "," & DSN
End Sub

 このプロシージャでは、接続が完了したサーバーにクライアント側のホスト名とcnOpenメソッドの引数で指定されたデータソース名をサーバーに送信している。サーバーはここで送信されたデータソース名によって、サーバー側で接続するデータベースを決定する。
 このデータソース名は、もちろんクライアント側で設定する必要はない。サーバー側で待機しているType9 DB Serverがサーバー側でアクセスするデータベースを特定するために使用する。

SQL文を実行するためのExecuteメソッド

 SQL文を実行するためには、Executeメソッドを利用する。Executeメソッドの実装は、次のように極めて簡単なものである。

Public Function Execute( _
 CommandText As String, _
 Optional RecordsAffected As Long, _
 Optional Options As Long) _
 As ADOR.Recordset
    wscConnect.SendData CommandText
End Function

 単純に引数で指定された文字列をサーバーに送信する。受信した文字列によってサーバーは処理を行なう。Executeメソッドは、SQLのINSERT文やUPDATE文などに利用できる。SELECT文のような結果セットの取得が必要なSQLを実行するときには、ConnectionコントロールのExecuteメソッドではなく、RecordsetコントロールのrsOpenメソッドを使用する必要がある。

接続のためのオブジェクトと結果セットを操作するオブジェクトを分離

 そもそもHyper SQLdb Client Libraryが、このような構成になった理由を説明する必要があるだろう。Visual Basicデベロッパーはコントロールの使用方法を十分に理解する必要があるが、そのコントロールが「なぜ」そのようなアーキテクチャになったのかを考えることは少ないはずなので参考になるだろう。
 コントロールをConnectionコントロールとRecordsetコントロールに分割したのは、任意のコネクションを複数の結果セットから利用するためである。コントロールを統合し、ひとつのコントロールで接続からSELECT文の実行までを行なうと、各結果セットごとにサーバーに接続する必要が生じる。これはひとつの方法だが、サーバー側のリソース事情を考えると好ましくない。サーバーはクライアントと異なり、使用状況の限界点を推測することがむずかしいからである。
 接続のためのオブジェクトを単独で作成できれば、その通信経路を共有してサーバーとの間でデータのやりとりを行なうことができる。反面、接続を共有した場合には、複数の結果セットを同時に取得することができなくなる。もっとも、この制約に対してはConnectionコントロールのインスタンスを複数作成することで解決できる。つまり、時間のかかるクエリーを同時に2つ実行する場合には、Connectionコントロールをフォームに2つ配置すればいいのである。

なぜ、ActiveXコントロールなのか?

 もうひとつ、なぜ、ActiveX DLLではなく、ActiveXコントロールとして実装したのかについて説明しておきたい。ActiveXコントロールのActiveX DLLに対する大きなアドバンテージとして、ビジュアルオブジェクトとして実装できるということがあげられる。しかし、ConnectionコンポーネントやRecordsetコンポーネントはフォームに表示する必要がないので、これはメリットにはならない。実際、ADOのConnectionオブジェクトやRecordsetオブジェクトはActiveXコントロールではない。
 まず、Visual Basic付属のWinsockコントロールを内部的に使用したため、コントロールにせざるを得なかったという事情もあるが、Socket通信をするためにWindosckコントロールを使わなければならない事情はない。にも関わらず、ActiveXコントロールにしたのは、「Visual Basicで使用されるプログラミングコンポーネントは、ActiveXコントロールとして実装されるべきだ」というのが私自身の主張だからである。ActiveX DLLのようなコードコンポーネントよりも、ビジュアルコンポーネントの方が、やはり直感的に理解できる。たとえばタイマーコントロールは誰もがなじみのあるコントロールだが、このコントロールがビジュアルオブジェクトである必要はない。しかし、フォームにタイマーコントロールを配置し、Intervalプロパティを設定し、Timerイベントプロシージャに処理を記述する、というスタイルが、やはり一番Visual Basicらしい。タイマーコントロールがコードコンポーネントとして実装されていたら、それだけで初学者にとって憂鬱のたねとなるだろう。

ActiveXコントロールをActiveX DLLと同様に扱う

 逆にActiveXコントロールの最大の欠点は、コントロール配列を除けばインスタンシングのタイミングを制御できないことだった。使う必要のあるコントロールは、すべて事前にフォームに配置しておかなければならないため、常にフォームのロード時にすべてのコントロールのインスタンスが作成された。これはコードコンポーネントにたとえれば、フォームのLoadイベントプロシージャ内ですべてのオブジェクトのインスタンスを作成しているようなものである。
 クライアントのリソース消費はたかがしれているし、別にそれでもいいと思うのだが、やはり、良心的なプログラマの中には許せない人もいるだろう。しかし、Visual Basic 6.0ではコントロールの動的な生成をサポートするので、この問題も解決したということができる。
 ConnectionコントロールやRecordsetコントロールをコードから動的に生成して使用するには、先月号で紹介したシンプルクライアントプログラムのコントロールをフォームから削除して、コードを記事末のリスト2のように修正すればいい。
 モジュールレベルで宣言されている各コントロールに対応する変数が、WithEventsキーワードを使って宣言されていることに注意してほしい。

Dim WithEvents cn As Connection
Dim WithEvents rs As Recordset

 あとは、コントロールを使用する直前に、次のようにインスタンス化することでActiveXコントロールを使用できるようになる。

Set cn = Controls.Add _
 ("DBCliLib.Connection", "control1")

 ここでは、ノンビジュアルなコンポーネントを使用しているため関係ないが、ビジュアルコンポーネントの位置やサイズを指定するには、この後でコードによって指定する必要がある。
 一般のCOMコンポーネントのインスタンス化とは若干構文が異なるが、慣れることに大きな障壁はない。いったん、インスタンス化してしまえば、使用方法はフォームに配置されたActiveXコントロールと何も違いはない。
 結局、ActiveXコントロールはActiveX DLLと同様に使用できるということになる。おそらくこの方法の最大の欠点は、Visual Basic 5.0で利用できないということだろう。Visual Basic 6.0の登場当初はずいぶんとネガティブなことを書いたが、Visual Basicだってちゃんと進歩しているのである。
 さて、次号ではRecordsetコンポーネントの実装について説明する。


リスト1:Connectionコントロールのソースコード
Option Explicit
Private DSN As String
Private rsObjR As Object
Private resultSet As String
Private Const LOCAL_HOST As String = "localhost"

Public Property Get ServerName() As String
    ServerName = wscConnect.RemoteHost
End Property

Public Property Let ServerName(ByVal New_ServerName As String)
    wscConnect.RemoteHost() = New_ServerName
    PropertyChanged "ServerName"
End Property

Public Property Get ServerPort() As Long
    ServerPort = wscConnect.RemotePort
End Property

Public Property Let ServerPort(ByVal New_ServerPort As Long)
    wscConnect.RemotePort() = New_ServerPort
    PropertyChanged "ServerPort"
End Property

Public Sub cnOpen( _
 Optional ConnectionString As String, _
 Optional UserID As String, _
 Optional Password As String, _
 Optional OpenOptions As Long)
    DSN = ConnectionString
    If wscConnect.State <> sckClosed Then wscConnect.Close
    wscConnect.Connect
End Sub

Public Sub cnClose()
    wscConnect.Close
End Sub

' 記憶領域からプロパティ値を読み込みます
Private Sub UserControl_ReadProperties(PropBag As PropertyBag)
    wscConnect.RemoteHost = PropBag.ReadProperty("ServerName", "localhost")
    wscConnect.RemotePort = PropBag.ReadProperty("ServerPort", 1010)
End Sub

Private Sub UserControl_Terminate()
    wscConnect.Close
End Sub

' 記憶領域にプロパティ値を書き込みます
Private Sub UserControl_WriteProperties(PropBag As PropertyBag)
    Call PropBag.WriteProperty("ServerName", wscConnect.RemoteHost, "localhost")
    Call PropBag.WriteProperty("ServerPort", wscConnect.RemotePort, 1010)
End Sub

Public Function Execute( _
 CommandText As String, _
 Optional RecordsAffected As Long, _
 Optional Options As Long) As ADOR.Recordset
    wscConnect.SendData CommandText
End Function

Public Sub SelectRequest(SqlStatement As Variant, RecordsetObj As Object)
    Set rsObjR = RecordsetObj
    wscConnect.SendData SqlStatement
End Sub

Private Sub wscConnect_Close()
    wscConnect.Close
End Sub

Private Sub wscConnect_Connect()
    ' ローカルホスト名とODBCデータソース名を送信
    wscConnect.SendData LOCAL_HOST & "," & DSN
End Sub

Private Sub wscConnect_DataArrival(ByVal bytesTotal As Long)
    Dim strData As String
    ' DBsvr(ActiveXexe)からデータを受け取る
    wscConnect.GetData strData, vbString

    resultSet = resultSet & strData
    If InStr(resultSet, "Result Start") > 0 And InStr(resultSet, "Result End") > 0 Then
        ' テキスト(resultSet)を元にレコードセットを作成
        rsObjR.MakeRecordset resultSet
        resultSet = ""
    End If
End Sub

リスト2:Hyper SQLdb Client Libraryを動的に追加して使用
Option Explicit
Dim WithEvents cn As Connection
Dim WithEvents rs As Recordset

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 cmdSend_Click()
    Set rs = Controls.Add("DBCliLib.Recordset", "control2")
    rs.rsOpen "SELECT * FROM Authors WHERE Author like 'a%'", cn
End Sub

Private Sub Form_Load()

End Sub

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


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