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


Type7 DB Server ――
汎用的にレコードセットを作成

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


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



前回までのあらすじ…

 この連載もだいぶ回を重ねてきた.初回からすべてを読んでいる読者もすでに多くはないだろう.読み物やマンガでよくあるように,前回までのあらすじを今回は説明しておきたい.
 この連載ではJetデータベースエンジンをデータベースエンジンとして利用し,C/Sシステムを構築するのが目的である.
 この目的を正しく理解してほしい.JetデータベースエンジンにMicrosoft SQL Serverをアタッチして使用したり,MDBファイルをファイル共有型のサーバーとして利用するといった中途半端なものではない.クライアントがリクエストを発行すると,それを処理したサーバーがデータをネットワーク経由で送り返す,正真正銘のマルチユーザーC/Sシステムである.
 このようなものを自作しようという理由は,C/Sデータベースサーバーの価格が高いからである.もっとも,正確にいえば,クライアントライセンスの価格である.たとえば,Sybase SQL Anywhereは実売で5万円という低価格で入手が可能な本格的なデータベースサーバーだが,これを実運用する場合には,使用するクライアントの台数に応じて,クラアントのライセンスフィーを支払う必要がある.このクライアントライセンスは,Microsoft SQL ServerやSybase SQL Anywhereのような低価格なものでも,実売で1クライアントあたり2万円程度する.つまり,10人で利用するC/Sデータベースシステムを構築しようとすると,データベースのライセンスフィーだけで,25万程度が必要になる.

真のC/Sデータベースシステム

 繰り返しになるが,MDBにアタッチしたリモートデータアクセスや,ファイル共有型との違いを簡単に説明しておく.
 アタッチされたデータベースは,リモートデータベースとの間でC/Sシステムとして動作するが,テーブルをオープンした時点でクライアントはサーバーテーブルのキーセットをクライアントに用意する.サーバーのキーセットとクライアントのキーセットは一対一で対応しているので,クライアントは必要に応じてレコード単位でサーバーの値を取得することができる.いうまでもなく,この方法の問題点は,テーブルが大きい場合にキーセットを取得するためのネットワークトラフィックが必要以上に増大することである.もうひとつの問題点は,マルチユーザーでの使用時にクライアントのキーセットとサーバーのキーセットの整合性が確保できなくなる可能性のあることである.それに加え,初期のJetデータベースエンジンではリンクの破壊が起こりやすかったなどの問題があり,本格的なC/Sデータベースシステムとしては,あまり用いられなかった.
 実は,アタッチテーブルのメカニズムは,Jetデータベースエンジンのバージョンが上がるにつれて,変化してきている.安定性と速度も向上してきているようなので,最新バージョンならば実用的なレベルのデータアクセス性能が得られるのかもしれない.しかし私はその可能性を信じていない.なぜなら,アタッチのアーキテクチャが本当に優れたものならば,ODBC Directのような別の方法を用意する理由はないからである.Visual BasicユーザーがODBC Directを使うならば,最初からRDOを利用すればよい.

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

 ファイル共有型のデータベースは,ファイルサーバーとファイルクライアントという意味ではC/Sのスタイルを取るが,データベースアクセスという意味では,C/Sアーキテクチャには該当しない.ファイルサーバーにデータベースファイルがあっても,実際の検索処理を行なうCPUはクライアントマシンのCPUだからである.だから,インデックススキャンの場合でも,インデックスデータはネットワークを疾走することになるし,シングルフィールドのテーブルスキャンだったら,すべてのテーブルデータがネットワークを走ることになる.ただ,ネットワークのトラフィックが少ない環境ならば,今日のようにサーバーとクライアントのマシンの性能差が少ないような時代には,処理が分散できるので,C/Sシステムよりも高速に実行できる可能性もある.
 C/Sシステムも本来は負荷分散の意味がある.しかし,それはホストコンピュータのような中央集中型のシステムと比較した場合の話で,パーソナルコンピュータのように伝統的にファイル共有型のデータベースシステムを利用してきたアーキテクチャと比較すると,負荷をサーバーに集約するアーキテクチャである.

誰が処理を担当するのか?

 負荷をどこにかけるのがベストかは,運用プランによって決まってくる.各マシンの性能,ネットワークのパフォーマンス,利用状況などが総体的に関係してくるので,すべてにおいてベストな方法というものはない.Microsoftが提供する方法の中で,もっともサーバー負荷を集約するタイプが,Windows Terminalで,もっとも高度に負荷を分散させるデータベースアーキテクチャが,レプリケーションによって運用するシステムであろう.一般的にサーバーに負荷が集中するほど,メインテナンスコストが小さくなり,分散化が進むほど,メインテナンスの手間は大きくなる.しかし,だからといって,すべての負荷をサーバーに集中すればTCOの削減が可能になるというほど話が簡単ではないのは,ひとつの高速なCPUよりも,半分の性能のCPUを2つ買う方が安いという事実も関係している.

C/Sシステムとして動作するサンプル

 さて,本稿で開発をすすめているのは,C/Sアーキテクチャのデータベースである.具体的にいえば,データ要求のSQL文をリモートマシンに送信すれば,それに該当する結果だけを送り返してくるスタイルである.データを更新するにはSQLのUPDATE文を送信すればよい.データ連結に慣れている開発者は,C/Sシステムを作るうえで,もしかしたらSQL文を使用していないかもしれないが,それはRDCなどのミドルウェアが自動的にSQL文を生成し実行しているからである.
 図1はこの連載のサンプルの動作メカニズムを表わしている.1台のクライアント接続に対してサーバーマシン内にひとつのDBsvr.exe(図2)が,それぞれのプロセスで起動する.DBsvr.exeはActiveXサーバーとして実装され,それぞれにJetデータベースエンジンを搭載し,MDBファイルにアクセスする汎用データベースプログラムである.
 ポイントになるのは,サーバーマシンとクライアントマシン間ではC/Sアーキテクチャをとるが,サーバーマシン内だけみると,複数プロセスからひとつデータベースにアクセスするファイル共有型として動作していることである.この仕掛けによって,ファイル共有型のデータベースエンジンであるJetデータベースエンジンを,C/Sシステムとして動作させようというのである.

図1:サンプルのデータベースサーバーの動作図
図1

図2:サーバー側で起動し,データベースにアクセスするDBsvr.exe
図2

サーバー側の負荷分散も想定

 データベースにアクセスするためのDBsvr.exeは,独自プロセスとして動作するActiveX.EXEとして実装されているので,図3のようにサーバーを複数に分割して負荷を分散することができる.データベースアクセスのような負荷の大きな処理を分散できることは,スケーラビリティの向上につながる.この場合,サーバー間でのトラフィックが増大するので,サーバー同士を,クライアントとサーバー間をつなぐネットワークとは,独自に構成することが好ましい.でないと,ネットワークトラフィックの増大によるパフォーマンスの劣化が,負荷分散のメリットを上回ることもありえる.DBsvr.exeを別マシン上で起動するためには,DCOMを使用する必要がある.DCOMは現時点でまだ熟成されてないテクノロジーだが,このケースのように独自プロセスを起動するためだけに使用するならば,十分に実用になるはずである.

図3:サーバー側で負荷分散した場合の動作図
図3

コールバックよる通信,そして問題点

 クライアントとサーバーの通信は,最初はクライアントがサーバー(DBsvrrb.exe)を呼び出す形でスタートするが,DBアクセスサーバー(DBsvr.exe)の起動後は,逆にサーバーがクライアントを呼びだす形になる.コールバックである.
 今の時点でのこのデータベースサーバーの問題点として,以下の2つをあげることができる.
  1. 各プロセスで,5MBのメモリを消費する
  2. サーバーがコールバックするため,マルチユーザーの使用時に不特定のポートを複数開ける必要がある
 この問題点は,同時に解決することが可能である.その答えはマルチスレッド化にある.DBsvr.exeをマルチスレッドサーバーとして実装することで,1接続あたりのメモリ消費は激減し,また,同じプロセスで動作することによって,コールバックの必要がなくなる.
 サンプルアプリケーションのDBsvr.exeのプロパティ設定を変更するだけで,マルチスレッド化は実現できるが,マルチスレッドへの本格的な対応は,Microsoft Accessの次のバージョンに搭載されるはずのデータベースエンジンの発売を待ってからにしたい.
 次のMicrosoft Accessには,現在のJetデータベースエンジンのニューバージョンと,Microsoft SQL Serverとエンジンを共有するMicrosoft Database Engine(MSDE)の2つが搭載されると発表されている.どちらのエンジンを利用するかはユーザーが選択できるという.この連載でも2つのエンジンの評価を行ない,適切な方を選択するようにしたい.

最新サンプル・Type7 DB Server

 まず,図4のType7 DB Clientの画面イメージをみてほしい.BIBLIO.MDBから取得したデータが,3種類の方法で表示されている.一番上は従来通りテキストボックスにテキストで表示されている.真中のグリッドには,表形式で表示されている.一番下は3つのテキストボックスにはカード形式で1レコードのデータが表示される.グリッドとテキストボックスにはデータベースのデータが連結されており,グリッドでデータを選択すれば,自動的に該当データがテキストボックスに表示される.
 テキストボックスへのデータ連結は先月号で説明した.グリッドへのデータ連結は簡単に行なえる.Type7 DB Clientの最大の進歩は,レコードセットの作成が汎用的に行なえるようになったことである.Type6 DB Clientはレコードセットを作成し,テキストボックスとの連結を実現したが,それはあくまでもAuthorsテーブル専用のものだった.Type7 DB Clientでは,実行するSELECT文に任意のテーブルを指定すれば,自動的に取得レコードに合わせたRcordsetオブジェクトを作成する.そのために,Type7 DB Serverはレコードを返すときに,先頭にレコードセットの情報を添付している.その情報に基づいてクライアント側でレコードセットを作成する.

図4:グリッドへのデータ連結を行なったType7 DB Client
図4

Visual Basic 6.0で動作

 まだデータ連結した状態でのデータの追加更新はサポートしていないが,レコードセットに連結した表示コンポーネントがデータを表示する様子は,一人前のデータベースシステムらしくなってきた.本連載のサンプルを作成する上で,Visual Basic 6.0のADOが提供する新機能は,すばらしい効果を発揮している.Visual Basic 5.0ユーザーがこのサンプルを実行できないことを残念に思う.もっとも,このサンプルのためだけにVisual Basic 6.0を購入する価値は,今のところまだないかもしれない.
 このデータベースサーバーが最終的な形で完成するのは,マルチスレッドに対応するか,サーバー側での分散処理を実現したときである.新しいデータベースエンジンとOLE DBを組み合わせたマルチスレッドシステムは,強力なローコストソリューションを実現するだろう.それまでは,技術蓄積のときである.

データ転送フォーマットの変更

 先に述べたように,Type6 DB ServerでもRecordsetオブジェクトの作成は可能だったが,作成するレコードセットはBIBLIO.MDBのAuthorsテーブルによって作成されたものに限定されていた.これはクライアント側がテキストで検索結果を受信するため,作成するレコードセットのフィールド型を特定できなかったためである.
 そのため,最新のType7 DB Serverでは,クライアントに結果を返信するときに,レコードセットのフィールド情報をヘッダに付加している.
 たとえば次のようなSQL文を実行したとき,サーバーがクライアントに返信するレコードセット情報はリスト1のようなものになる.

SELECT * FROM Authors WHERE Au_ID < 10
 フィールド情報が付加された以外に,データ自体のフィールド区切りも従来のセミコロンから,TAB区切りに変更されている.そのため,Type7 DB Server/ClientとType6 DB Server/Clientは,相互に互換性がないものになっている.

リスト1:Type7 DB Serverサーバーがクライアントに返信するレコードセット情報(テキスト)
Field information
Au_ID	4	4
Author	12	50
Year Born	5	2
Lint	4	4
Sint	5	2
Dflot	8	8
Sflot	7	4
End field information

Result Start : _
 [SELECT * FROM Authors WHERE Au_ID < 10]
1	Jacobs, Russell	65
2	Metzger, Philip W.	54
3	Boddie, John	57
4	Sydow, Dan Parks	
6	Lloyd, John	
8	Thiel, James R.						
Result End : _
 [SELECT * FROM Authors WHERE Au_ID < 10]

Data send complete

フィールド情報の付加

 クライアントが送信するテキストデータにフィールド情報を付加しているのが,DBsvr.exeのWinsock1_DataArrivalプロシージャの次の部分である.

MaxfieldCount = rs.rdoColumns.Count
RetResult = "Field information" & vbCrLf
For i = 0 To MaxfieldCount - 1 
    RetResult = RetResult & _
     rs(i).Name & Chr(9) & _
     rs(i).Type & Chr(9) & _
     rs(i).Size & vbCrLf
Next
RecCount = 0
Winsock1.SendData RetResult & _
 "End field information" & vbCrLf & vbCrLf & _
 "Result Start :[" & sqlStat & "]" & vbCrLf
 For〜Loopは,レコードセットにあるフィールドの項目の数だけループを行なう.ループ内ではRecordsetオブジェクトのNameプロパティ,Typeプロパティ,Sizeプロパティを使用してレコードセットの情報をテキスト化している.クライアントに送信するのは,ループを脱出してからである.

最適化されたレコードセットの作成

 テキストをレコードセットに変換しているのは,Type7 DB ClientのWinsock2_DataArrivalプロシージャの次の部分である.このプロシージャは,コールバックを受けたWinsockコントロールがデータをサーバーから受信したときに呼び出されるイベントプロシージャである.

Set mkRs = New MakeRecordsetCls
    mkRs.MakeRecordset (resultSet)
 MakeRecordsetClsクラスをインスタンス化し,メンバーであるMakeRecordset関数を呼び出している.MakeRecordsetClsクラスのMakeRecordset関数が記事末リスト2である.  この関数はType6 DB Serverでも実装されていたが,大量のレコードを含むRecordsetオブジェクトを受信したときのパフォーマンスに問題があったため,レコードセット作成のアルゴリズムも変更されている.
フィールド型に応じたFieldオブジェクトの作成

 レコードセットのフィールド情報を文字列から切り出す方法は,レコードセットのデータを取得する方法と同じだが,ポイントになるのは,次の部分である.

Select Case i
    Case 1
        AppendParamName = strField
    Case 2
        AppendParamTypeVal = Val(AppendParamType)
        ' RDOのレコードセットのタイプをADOに変換
        Select Case AppendParamTypeVal
            Case 2 Or 3
                strField = adNumeric
            Case 4
                strField = adInteger
            Case 5
                strField = adSmallInt
            Case 6 Or 7 Or 8
                strField = adDouble
            Case -6
                strField = adTinyInt
        End Select
        AppendParamType = strField
    Case 3
        AppendParamSize = Val(strField)
End Select

Next
dsRecordset.Fields. _
 Append AppendParamName, adLongVarChar, _
 AppendParamSize, adFldUpdatable  ' adFldRowID
 このコードでは,サーバーが返すRDOのフィールドの型をクライアントのADOのフィールド型に変換している.これは二者で使用される定数の値が違うことから必要になる処理である.将来的にはサーバー側もADOで実装しなおす可能性もあるが,現時点ではサーバーのデータベースアクセスはRDOを利用して行なっている.サーバー側にもADOを使用すれば,当然,この変換処理は必要なくなる.
 最後に取得したフィールド情報を引数にRecordsetオブジェクトのFieldsAppendメソッドを実行している.これで,返信されたレコードセットに応じたRecordsetオブジェクトがクライアントに作成され,必要なデータを格納できる.

DBグリッドへのデータ連結

 Type7 DB ClientではDBグリッドコントロールへのデータ連結表示もサポートしている.データ連結を指定しているのは, Type7 DB ClientのWinsock2_DataArrivalプロシージャの次の部分である.

Set DataGrid.DataSource = mkRs
 DBグリッドコントロールのDataSourceプロパティに作成したRecordsetオブジェクトを指定すればよい.テキストボックスにデータ連結する場合のように,BindingCollectionオブジェクトを作成する必要もない.
 この1行を記述するだけでDBグリッドにデータが表示され,BindingCollectionオブジェクトによってテキストボックスに連結されたデータとの同期がはかられる.すばらしい.

まとめ

 ADOのRecordsetオブジェクトを使用し,データ連結を行なうことで,クライアントの画面イメージが飛躍的によくなったことがわかると思う.しかし,重要なのはデータ連結が可能になったことではなく,Recordsetオブジェクトとしてクライアントに結果データを格納できるようになったことで,つまり,プログラミングインターフェイスがRDOやDAOと共通化されたことである.新しいスキルをそれほど習得することなく,サンプルのデータベースサーバーが利用できるようになる.

リスト2:テキスト文字列からrecordsetオブジェクトを生成するMakeRcordset関数
Public Function MakeRecordset(strParam As String)
    Dim fld As ADODB.Field
    Dim strRow As String
    Dim strField As String
    Dim lngCurPointer As Long
    Dim CurRecordLen As Integer
    Dim intFieldStartPos As Integer
    Dim intFieldNextPos As Integer
    Dim i As Integer

    CPOS = Chr(9)
    CPOSlen = Len(CPOS)

    '---------------------------------------
    ' フィールド情報の先頭に移動
    Set dsRecordset = New ADODB.Recordset
    lngCurPointer = InStr(strParam, "Field information")
    Do While True  ' intPosRow > 0
        ' 処理する行の先頭ポインタを取得
        lngCurPointer = InStr(lngCurPointer, strParam, vbCrLf) + 2
        ' 処理する行の長さを取得
        CurRecordLen = InStr(lngCurPointer, strParam, vbCrLf) - lngCurPointer
        ' 処理する行を取得
        strRow = Mid(strParam, lngCurPointer, CurRecordLen)
        ' フィールド情報の最後ならば,ループを終了
        If Left(strRow, 21) = "End field information" Then
            Exit Do
        End If

        intFieldNextPos = 1
        Dim AppendParamName As String
        Dim AppendParamType As String
        Dim AppendParamTypeVal As Integer
        Dim AppendParamSize As String
        For i = 1 To 3
            intFieldStartPos = intFieldNextPos
                ' 区切り記号が見つかったら,左側がフィールドの値
                intFieldNextPos = InStr(intFieldStartPos, strRow, CPOS) + CPOSlen
                If intFieldNextPos <> 0 + CPOSlen Then
                ' 区切り記号まで位置を移動します
                ' intPos = InStr(intFieldStartPos, strRow, CPOS)
                ' 値を strField 変数に割り当てます
                strField = Mid(strRow, intFieldStartPos, _
                 intFieldNextPos - intFieldStartPos - CPOSlen)  ' Left(strRow, intPos - 1)
            Else
                ' 区切り記号が見つからなければ,最後のフィールドです
                strField = Mid(strRow, intFieldStartPos, Len(strField) - CPOSlen + 1)
            End If

            Select Case i
                Case 1
                    AppendParamName = strField
                Case 2
                    AppendParamTypeVal = Val(AppendParamType)
                    ' RDOのレコードセットのタイプをADOに変換
                    Select Case AppendParamTypeVal
                        Case 2 Or 3
                            strField = adNumeric
                        Case 4
                            strField = adInteger
                        Case 5
                            strField = adSmallInt
                        Case 6 Or 7 Or 8
                            strField = adDouble
                        Case -6
                            strField = adTinyInt
                    End Select
                    AppendParamType = strField
                Case 3
                    AppendParamSize = Val(strField)
            End Select
        Next
        dsRecordset.Fields. Append AppendParamName, adLongVarChar, _
         AppendParamSize, adFldUpdatable  ' adFldRowID
    Loop
    dsRecordset.CursorType = adOpenKeyset
    dsRecordset.LockType = adLockOptimistic
    dsRecordset.Open

    '---------------------------------------
    ' レコードのスタート位置にポインタを移動
    lngCurPointer = InStr(strParam, "Result Start")
    Do While True ' intPosRow > 0
        ' 処理するレコードの先頭ポインタを取得
        lngCurPointer = InStr(lngCurPointer, strParam, vbCrLf) + 2
        ' 処理するレコードの長さを取得
        CurRecordLen = InStr(lngCurPointer, strParam, vbCrLf) - lngCurPointer
        ' 処理するレコードを取得
        strRow = Mid(strParam, lngCurPointer, CurRecordLen)
        If Left(strRow, 10) = "Result End" Then ' Modi
            Exit Do
        End If
        ' 新規レコードを追加
        dsRecordset.AddNew
        intFieldNextPos = 1
        For Each fld In dsRecordset.Fields
            intFieldStartPos = intFieldNextPos
                ' 区切り記号が見つかったら,左側がフィールドの値
                intFieldNextPos = InStr(intFieldStartPos, strRow, CPOS) + CPOSlen
                If intFieldNextPos <> 0 + CPOSlen Then
                    ' 値を strField 変数に割り当てます
                    strField = Mid(strRow, intFieldStartPos, _
                     intFieldNextPos - intFieldStartPos - CPOSlen)  ' Left(strRow, intPos - 1)
                Else
                    ' 区切り記号が見つからなければ,最後のフィールドです
                    strField = Mid(strRow, intFieldStartPos, Len(strField) - CPOSlen + 1)
                End If

                If fld.Type = 17 Or fld.Type = 18 Or _
                 fld.Type = 19 Or fld.Type = 16 Or _
                 fld.Type = 2 Or fld.Type = 4 Or _
                 fld.Type = 131 Or fld.Type = 3 Or _
                 fld.Type = 5 Then ' 数値型の場合数値に変換
                    fld.Value = Val(strField)
                Else
                    fld.Value = strField
                End If
        Next
        dsRecordset.Update
        dsRecordset.MoveFirst
    Loop
End Function

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