「間違いだらけのデータアクセス」

から脱却するために

ダイレクトミドルウェアシリーズのおすすめ
Direct ORA / Direct SQL

有限会社イント・ツーワン 代表取締役
木村忠文
 


このドキュメントは、RDBMSを使ったC/S開発におけるダイレクトミドルウェアシリーズの必然性と、使い方の概要を説明するためのものです。本文は、木村忠文が月刊アスキーNT(アスキー刊)に1999年前半に執筆し、大きな反響をいただきました連載記事「間違いだらけのデータアクセス」の内容をベースに、新たに書き起こしたものです。
 

RDBMS開発現場の実情

RDBMSを使ったシステムは、今やトレンドである。社内システムにせよ、インターネットで公開するシステムにせよ、RDBMSを使ったシステムを導入する企業にとってステータスであり、システムを開発できる企業にとっても技術力の証なのである。
これほどのトレンドであるから、すでに一般化しているRDBMSを使ったシステムは、当然みんな快調に動いているのだろうと思ってしまうのだが、どうやら必ずしもそうではないらしい。RDBMSを使ったシステムの開発現場の声を聞くと、むしろうまく行っていないところの方が多いように思える。
これは、いったいどうしたことだろうか。RDBMSへのアクセスは、とっくに実用になっている技術のハズである。
実は、RDBMSの使用には、いたるところに陥りやすいワナがある。私自身もこれらのワナに何度もハマっては苦闘した覚えがある。よくよく話を聞いてみると、他の方もほぼ例外なく、そういったワナにハマっているのである。そして、困ったことに、これらのワナは一般には正しいと思われていることや、先進的と思われていることだったりするのだ。つまり、一般に正しいと通っているコトの多くが間違いだったのである。
間違いは開発に関わるあらゆる部分にある。RDBMS本体の理解、ミドルウェアの役割を理解した上での選択、適切なツールの選択、Webの仕組みの理解。いずれも、こういった個々の技術の本質を分かっていないで、ベンダーの提供するソフトウェアの使い方をマスターすることだけに固執することが間違いの本質である。これはごく当然のことである。たとえば、WindowsのメッセージやAPIなどのおおよその仕組みを理解し、GUIアプリケーションを作成するためのルールを知らずして、Visual BasicでプログラミングしたらWindowsアプリケーションが書けた気になってしまっても、それは本当の理解ではないのと同じなのだ。
 

RDBMSの構造を理解しよう

SQL ServerやOracleなどを使っていると、最初からそれらのもっているさまざまな機能の使い方を知ろうと思いがちである。しかし、その前にRDBMS自体がどのようにして動いているか、どうしてそのような機能が必要なのかといったことを考えるべきだ。RDBMSの目的、それを実現するために考えられた仕組みを知れば、表面的な機能の使い方だけでなく、どのように使えば効率がよいかも見えてくるはずだ。
RDBMSの目的は、複数のユーザーが、データに効率よくアクセス(保持、検索、修正)できることである。しかし、ハードディスク上にあるデータは、実際にはある位置に書き込まれているデータに過ぎない。この位置を効率よく高速に特定するためには、索引、すなわちインデックスを利用するのが一般的である。ある本に書いてある特定の情報を読もうと思ったら、索引を引くのが速いのと同じことである。これをプログラムとして高度に実現したのが、ISAMやVSAMといった仕組みである。
たしかにこのような仕組みで効率よくデータアクセスをする基本はできる。しかし、RDBMSでは複数のユーザーがネットワークを介して同時にアクセスする。となると、クライアント側からインデックスを検索してレコードを得、対応する実データファイルの該当レコードにアクセスするといったことをするには問題がある。というのも、インデックスは常に変更される可能性があるからだ。
たとえば、クライアントAが起動されたときにインデックスをクライアントに持ってくるとしよう。これには多少なりともネットワークの負荷がかかる。その後、クライアントBも同様に起動され、一行追加されたとする。ところが、クライアントAは古いインデックスを使ってアクセスするから、不整合が生じてしまう。それならば、クライアントBから一行追加されたときに、なんらかのイベントがクライアントAに発生して、インデックスの再読み込みをすればよいだろう。しかし、こんなことを繰り返していれば、ネットワークの負荷は大きくなるばかりである。
確かに、インデックスは効率よくアクセスするための手法であるが、それは内部的なものであり、本来は外部からはデータの検索、追加、変更、削除ができればよいだけの話である。したがって、インデックスを使った処理は内部に隠蔽されるべきもので、外部からはもっと適したインターフェイスがあればよい。
そこで考えられたのが、SQL文である。SQL文は、ご存じのようにSELECT, INSERT, UPDATE, DELETEといった基本的な文と、条件を決めるWHERE節などを組み合わせて、特定の行や列の集合にアクセスするための文である。
SQL文は純然たる数学の集合論として考えられたものであるから、本当の理解のためには集合論を理解する必要がある。しかし、ANSIやISOで決められたこのSQL文も、実際には不明確な部分が存在することも確かであり、個人的には数学的に美しいとは言えないように思う。もっと厳密なルールに従った構文となるべきだ。
ともあれ、SQL文を使えば、データを集合としてプログラミングの素人でも容易に扱うことができるので、実に便利である。
RDBMSでは、このいいかげんなSQL文を解釈し、適切な内部処理に置き換えて結果を返すような仕組みになっている。さらに、データアクセスのみならず、テーブル定義やインデックスの作成、さらにはユーザーのアクセス管理などまで、RDBMSの機能のほとんどすべてがSQL文で実行されるようになっている。

ただし、ここで一つ確認しておきたいのは、RDBMSにおけるデータアクセスは、すべてSQL文が基本となっていることである。すなわち、適切なSQL文さえ実行できれば、クライアントからのRDBMSへのアクセスは、すべてできてしまうということだ。他にクライアントに必要となるのは、ユーザーインターフェイスのみなのである。

ミドルウェアの選択と最適な使い方

ミドルウェアとは

クライアントからRDBMSにアクセスするためには、ネットワークを介することになる。
ネットワークにはさまざまなプロトコルがある。インターネットの普及した今でこそTCP/IPが標準となってきたが、従来はネットワークOSによって提供される、SPX/IPXやNamed Pipesといったプロトコルが使われることも多かった。
当然ながら、こういったプロトコルによって、ネットワークを使ったデータのやり取りの方法は異なってしまうし、さらに言うならばクライアントOSによっても方法は異なってしまう。そこで出てきたのがミドルウェアと呼ばれるモノである。
一般に、ミドルウェアは次のような条件を満足する。
つまりは、一定の手続きを踏めば、RDBMSに接続し、SQL文を投げて、結果を得ることができるための仕組みである。
 

ミドルウェアの階層

ミドルウェアは単純なSQLのやり取りができるだけでなく、対応するRDBMSの機能に特化した機能を持っているのがふつうである。というのも、そもそもはRDBMSと同時に配布されるものであったからだ。たとえば、SQL Serverであれば、DB Library、Oracle7であればSQL*Net、Oracle8であれば、Net8がミドルウェアの基本である。
ただし、これらはクライアントアクセスモジュールと呼ばれることが一般的であり、さらにこれらを特定のツールから使いやすく抽象化されたレイヤーを、ミドルウェアと呼ぶのが一般的である。ODBCやDAO、RDO、ADO、OO4O(Oracle Objects for OLE)などがこれに当たる。
DB LibraryやNet8などのクライアントアクセスモジュールとも呼ばれる低レベルミドルウェアでは、接続プロトコル、接続先RDBMSサーバー、ユーザーID、パスワード、デフォルトデータベースなどを指定すれば、目的のサーバーとの接続をする関数が用意されている。さらに、SQL文を発行し、その結果を得るための関数群が用意されている。したがって、こういった数少ない関数さえ呼び出して、SQLを実行できれば、ミドルウェアの役目はおしまいである。
しかし、これらの関数は言語によっては使いにくかったりするし、SELECTした行の前後方向への移動や、行の特定列を、UPDATE文などを使わなくても更新したり、INSERT文を使わなくても追加したりといった、一見ISAM的で使いやすそうに思えるような機能は持っていない。あくまでもSQL文を使うのが前提である。そこで、特定のOSが持つ機能や特定の言語から使いやすいインターフェイスとしたり、一見使いやすげな機能を追加したミドルウェアが作られた。前者がVBSQL、DDE Manager、OracleGlue、後者がSQL DMO、OO4Oなどである。
また、SQL ServerとOracleとでは、関数や使用方法がまったく違うために、それぞれ別々に修得が必要である。そこで、同じ手続きで異なるRDBMSにもアクセスできるようにした「RDBMSを選ばない」ミドルウェアが作られた。ODBCやOLE DBがそれである。さらに、これらを使い、その上に先に述べたような一見使いやすげな機能までも実装したのが、DAO、RDO、ADOである。
さらには、もっと一見使いやすげにするために、データコントロールやRDC、ADC、OraDataコントロールといった、データバインディングを可能とした上に、行の移動をたやすくするインターフェイスを持つカスタムコントロールも用意された。

ダメなミドルウェアばかり?

ミドルウェアの階層図から分かることは、ミドルウェア自体が複数階層になっていることだ。当然ながら、クライアントアプリケーションからサーバーに至る間にあるレイヤーが多いほど、オーバーヘッドは大きくなるということだ。もちろん、それが使いやすさに結びついていることもあるし、特定の使い方をしたときに高速に動作するようになってはいる。
しかし、一般的にはレイヤーが多くなるほどインストールや設定の手間は複雑になるし、パフォーマンスは落ちてしまうと考えるべきである。
しかし、そもそも、ODBCを使うのは問題である。たとえば、DAOであれば、RDBMSに対応したODBCドライバーをインストールし、接続の設定をする必要がある。ところが、ODBCの構造上、その性能はODBCドライバーに依存してしまう。DAOやRDOの新しいバージョンができるたびに、ODBC自体の規格も変更される。ところが、このドライバーはMicrosoftの提供する自社のSQL Server用はともかく、他社向けのたとえばOracle用は機能不足であり、きちんと動作しない。
Visual BasicなどからODBCを使うときには、DAO自体をインストールする手間も必要になるが、これも結構な面倒ものがある。さらに、DAO自体はISAMをベースとしたJETエンジンを使うものであるため、サーバーからインデックスのデータをクライアントにいちいちダウンロードする。このため、著しくパフォーマンスが落ちてしまう。
これを補うためにODBC Directといった機能が追加されていたりするが、一見使いやすげだった機能が使えなくなり、使えるのはSQL文だけとなる。これではDAOのメリットはない。
さらに、実はODBCトライバーは内部的に勝手なSQL文を作って実行するため、思い通りのSQLが実行されるわけではない。たとえば、実際に500Kbytes程度のテーブル初期化SQLをODBC APIを使って実行すると、6Mbtyesほどのデータのやり取りがされてしまったことがあった。このように、ODBC APIが勝手に作り出すSQL文はスマートではない。
また、ODBCやOLE DBといった、「RDBMSを選ばない」ための仕組みでは、RDBMSの持つ固有の機能を活かすことができない、あるいは使いにくくなっているケースが多いという問題がある。また、逆にこれらを提供しているマイクロソフトのRDBMSが持つ弱点を補う機能が追加されている。RDOに実装された複雑なカーソルモデルや排他処理機能は、行レベルロックができないSQL Server 6.5以前の機能不足を補うものである。ところが、Oracleで使うことを考えれば、RDOのデータバインド機能はOracleネイティブな使い方でないためにたいへん使いにくい。
このように、ODBCが間に入るとロクなことがない。あえて使おうというのであれば、ターゲットとなるRDBMSがSQL Server 6.xとなるときと限定すべきだろう。
では、Microsoftの最近提唱しているADOはどうであろうか。
ADOは、OLE DBをVisual Basicなどから使うためのCOMオブジェクトである。OLE DBは、Microsoftの提唱するユニバーサルデータアクセスを実現するための手法である。ユニバーサルデータアクセスとは、RDBMSに限らず、さまざまなデータ形式をサポートするデータソースに対して、共通の手続きでアクセスしようというものである。したがって、実はデータにアクセスする基本的な仕組みのみが定義されており、データソースごとに個別の拡張がなされているのである。このため、DAOからRDOの流れで一時はC/S型RDBMSに特化して充実した機能が、さらに汎用なADOになったために逆に機能は少なくなってしまった。さらに、例によってOracle用のOLE DBプロバイダーなどはきちんと動作しないという問題も指摘されている。
このように、Microsoftの提供する一見汎用な「RDBMSを選ばない」ための仕組みは、実はぜんぜん汎用ではなく、Microsoft以外の製品を使うと問題だらけなのだ。
Oracleにアクセスするのであれば、OO4Oを使うのが一番である。もっとも、OO4Oもオブジェクトの参照方法に少々工夫をしないと高速にアクセスできないといったクセがあるので、OO4Oにすればそのまま速くなるというものではない。このあたりはヘルプにも記述があるので、しっかり読んでいただきたい。
 

ミドルウェアで必要なことはSQLの実行だけなことを改めて思い知った!

さて、このように考えてくると、実はマトモなミドルウェアは少ないということに気づく。すっかり途方にくれてしまいそうだが、ここで、思い出していただきたい。
それは、RDBMSにおけるデータアクセスは、すべてSQL文が基本となっていたということだ。すなわち、適切なSQL文さえ実行できれば、クライアントからのRDBMSへのアクセスは、すべてできてしまうのだった。ところが、ミドルウェアと呼ばれるものたちときたら、ヘタにいろいろなRDBMSに同じ手法でアクセスできるとか、SELECTしたデータをメソッドで修正できるとか、そういう余計な機能がついているばかりで、かえってパフォーマンスは低下していたり、インストールやメンテナンスが面倒になっている。しかもちゃんと動かないケースも多い。
Visual BasicとRDOで開発をしていたある日、私は問題にぶち当たった。それは、すでにデータベースはでき上がっており、動作しているシステムから、週報のようなものを出すプログラムだった。
私は、I/SQLでSQL文を書いてデータアクセスの方法を模索していた。このSQLでは、少々複雑なクエリーを必要とし、さらにテンポラリのテーブルを作成して集計するといった処理が必要だった。何回かのトライのあと、I/SQLでは正しく動作することを確認した。ところが、こうした複雑なSQLを、RDOでは実行できないのだ。すでに動いているSQLがあるにもかかわらず、SQLを分割し、SELECT系とINSERT系で動作を分けて何段階かに書き直す必要があることに気づいたのだ。
しかし、考えてみれば、おかしな話である。すでにI/SQLではSQL文は正しく動いているのだ。このSQLをVisual Basicから実行し、その結果セットを取り出して、印刷するプログラムに渡すことができればよいだけなのに、何でこんな面倒なことをしなくてはならないのだろう。
そこで、ATLを使ってDB Libraryの関数をVisual Basicから使えるようなCOMコンポーネントを作成した。なんと、これだけで問題はすべて解決してしまったのだ。ついでに、長いSQL文を格納するコントロールを作り、これにはバインド変数のようにプロパティで一部の内容を外部から制御できるようにした。これ以来、私の会社では、どんな複雑なデータアクセスでも、I/SQLで動作を確認すれば、すぐにVisual Basicに持ってきて動かすことができるようになったのである。
同様の機能を持つOracle用COMコンポーネントも作った。これには、Oracleのバインド機能を付けた。これもまた便利である。
いずれもATLで作ったコンポーネントなので、小さくて速い。ミドルウェア階層図の一番右にある、Direct SQLとDirect Oraがこれである。
これは、たまたま独自に作ったが、OracleであればOO4O、SQL Serverであれば、SQL DMOで似たようなことはできる。ただ、コンパクトさと問題が発生したときにはソースコードがこちらにあるという点では、Directミドルウェアに分があると思う。いずれにしても、RDBMSへのアクセスには、SQL文さえ使えれば、それだけで必要十分であるというのが、私の結論である。余計な機能は結局マイナスなのだ。
クライアントからのデータアクセスでは、ミドルウェアはSQLが実行でき、結果が得られることさえできれば、他にクライアントに必要となるのは、ユーザーインターフェイスのみなのである。
 

DirectSQLの概要

SQL ServerのDB LibraryにネイティブアクセスするためのDirectSQLコンポーネントを紹介しよう。DirectSQL.Txtに、このコンポーネントのオブジェクトおよびメソッド、プロパティの概要を示す。
このコンポーネントの本体は、DirectSQL.DLLであり、Visual Basicなどからは参照設定をして使う。使い方はいたってカンタンである。次にVisual Basicを使ったときの例を示そう。
  ※サンプルのディレクトリ: \DirectSQL\Samples\Pubs
     
  1. DBLibraryオブジェクトの作成 Dim oDBL As New DBLibrary のようにして、DBLibraryオブジェクトを作成する。ここからすべてが始まる。これ以後は、oDBLをベースにすべての作業を行うので、Visual Basic内でのオブジェクトのアクセスを高速化するために、With〜End Withブロックの中で記述することにする。
    With oDBL
    	コード…
    End With
    
  2. サーバーとの接続 Initializeメソッドで初期化したあと、サーバー名、データベース名、ユ−ザー名、パスワードを指定し、Openメソッドで接続が完了する。
    	'初期化 接続
    	.Initialize
    	.Server = "sqlsvr"
    	.Database = "pubs"
    	.User = "sa"
    	.Password = "passwprd"
    	.Open
    
  3. SQL文の発行 文字列型変数sSQLにSQL文を設定し、CompileメソッドでSQL文をコンパイルし、Executeメソッドで実行する。SQL文は、Transact SQLで記述された複雑なものであっても一回で実行可能である。もちろん、SELECTだけでなくいわゆるアクションクエリーやストアドプロシージャの実行なども可能である。
    ' SQL文の発行
    	.Compile sSql
    	.Execute
    
  4. Resultsetの取得 Transact SQLを実行すると、SQL Serverは1つ以上のResultsetを返す。たとえば、リスト(DirectSQL\Samples\Pubs\rept.sql)のような複雑なSQLをSQL Server付属のISQL/wユーティリティを使って実行すると、図のように複数件の処理がされることが分かるハズだ。この「x件処理されました」の1セットがひとつのResultsetなのである。

    DirectSQLでは、GetNextResultプロパティを参照するごとに、次のResultsetを得ることができるようになっている。select文の結果などをFetchするのは、次のようにループ中で行うことになる。

    Do While .GetNextResult
    	'空のResultsetは読み飛ばす
    	If .Columns.Count <> 0 Then
    		Fetchなどの処理…
    	End If
    	Debug.Print "RowCount:"; .RowCount
    Loop
    

    ISQL/wでも分かるように、「SELECT @pub_id = "%%%%%%%%"」のような文に対してもResultsetが返ってくるわけだが、これは結果セットとして取得する必要はない。そこで、Columnus.Countプロパティが0のときには、Fetchなどはしないで、さらにGetNextResultを実行すればよい。
    FetchによってそのResultsetの結果を取り込んだ後で、RowCountプロパティには処理された行数が返る。
    なお、Resultsetが必要ないときや、必要なResultsetを取り出した後には、Cancelメソッドを使ってそのあとのResultsetを破棄することができる。
     

  5. Fetchとカラム内容の取得 Resultsetから一行を取り出すには、Fetchプロパティを使う。これは、GetNextResultプロパティと同様に、ループの条件として使うことができる。
    カラム数を示すColumnts.Countプロパティが0以外のときには、カラム名やカラムを取得することかできる。
    Columnはコレクションになっているので、Column("ID") のようにカラム名を指定したり、Column(1)のように配列ライクにアクセスすることもできる。また、カラム名は、ColumnName(index)で得ることができる。
    次のコードは、1回目のFetchのときにカラム名を表示してから、カラムの内容を表示する例である。
    	bFirst = True
    	Do While .GetNextResult
    		Do While .Fetch
    			If .Columns.Count <>0 Then
    				If bFirst Then
    					For i = 1 To .Columns.Count
    						Debug.Print .Columns(i).Name;",";
    					Next i
    					Debug.Print
    					bFirst = False
    				End If
    				For i = 1 To .Columns.Count
    					Debug.Print .Columns(i);",";
    					List1.AddItem .Columns(i)
    				Next i
    				Debug.Print
    			End If
    		Loop
    		Debug.Print "RowCount:"; .RowCount
    	Loop
    

  6. 終了 .Closeメソッドで接続を閉じることができる。
     
  以上が、一般的な使い方である。

実際に、先ほど示したリストの複雑なSQL文を実行した結果を図に示す。

この複雑なSQLについて、ちょっと説明しておこう。このSQL文は、MS SQL Serverにインストールされるサンプルデータベースpubsにある11のテーブルのうち、6つにアクセスして(DirectSQL\Samples\Pubs\pubs.xlsのERDシート)、原稿料とロイヤリティ(著作権料金)の計算をするものである。ここで使っているテーブル構造について表(pubs.xlsのテーブルシート)に示す。ここでは、次のようなことを前提に、著者に支払うロイヤリティの計算をしている。

  実際にSQL文をご覧いただければ、テンポラリテーブルを作成して集計するという、実に複雑な作業をしていることが分かるだろう。これを、従来のVisual Basic+RDO/ADOなどでやろうとしたら、一大事である。もちろんできないことではないが、いくつかのクエリーをネストして行い、計算自体はVisual Basicでやることになるだろう。これは考えたくないことではないだろうか?
ところが、SQLさえしっかりマスターしてあれば、このようなSQL文を記述することができ、ISQL/wで実行して動作を確認することができる。ところが、こんな複雑なSQL文をVisual Basic + そのへんの一般的なミドルウェアでは動かすことができないのである。ISQL/wで動作を確認したのに、再び複数のクエリーにバラしてVisual Basicに計算をさせるのは得策だと思うだろうか?
そんな人はいないだろう。
DirectSQLは決して高機能ではないが、こうした複雑なSQLを実行し、その結果のみを自由にVisual BasicやExcelなどのCOMコンポーネントにアクセスできるツールに得ることができるのである。ここでは、たまたまVisual Basicを使ったが、この結果をExcelのシートに貼りつけて帳票として印刷することだってできるのだ。
既存のミドルウェアとクライアントツールが一体化したVisual Basic Enterprise Editionのような製品は、確かに型にハマったアプリケーションをカンタンに作れるような気になるが、現実の開発ではそれだけではない。このように、SQLをうまく活用することを考えれば、RDBMS、ミドルウェア、ツールの役割分担を適切にさせ、SQLを有効活用して効率的に開発ができそうであることは、この例からもお分かりいただけるだろう。
なお、ここでは独自のミドルウェアを使用したが、SQL Server 6.xに付属のSQL DMOやOracle Serverに付属のOracle Objects for OLEなどを使えば、このような複雑なTransact SQL文や、PL/SQLブロックなどを処理することができるだろう。ただし、どの程度まで大丈夫か検証はしていないので、ISQL/wやSQL*Plusと同様に結果が得られるかは保証の限りではない。また、ここで使用した独自ミドルウェアはいたずらに高機能を狙っていないので、軽量コンパクトであることもメリットである。

独自ミドルウェアDirectORAの概要

次に、Oracle8のNet8にネイティブアクセスするためのDirectORAコンポーネントを紹介しよう。DirectORA.Txtに、このコンポーネントのオブジェクトおよびメソッド、プロパティの概要を示す。
このコンポーネントの本体は、DirectORA.DLLであり、DirectSQL同様にVisual Basicなどからは参照設定をして使う。使い方もDirectSQL同様いたってカンタンであるが、DBLibraryとNet8との機能の違いがあるので、多少使い勝手が違う。たとえば、Net8では複数Resultsetという考えがないので、結果セットの取得はカンタンだ。また、Bind変数にも対応している。さらに、ここでは一つのテストケースとして、Visual Basicなどのコントロールにデータバインドする機能もつけてみた。次にVisual Basicを使ったときの例を示そう。
※サンプルのディレクトリ:\DirectORA\Samples\simple
     
  1. OCIオブジェクトの作成 DirectORAでは、サーバーと接続するため、次のようにOCIオブジェクトを作成する。

    Dim oOra As New OCI

    DirectORAは複数階層のオブジェクト構造となっているので、ここでは分かりやすくするためにWithブロックは使わないで説明する。

  2. サーバーとの接続ための前段階 サーバー名、ユ−ザー名、パスワードを指定する。
    oOra.Server = sServer
    oOra.User = sUser
    oOra.Password = sPassword
    

     

  3. OciCursorオブジェクトの作成 Net8のOCIはマルチセッションに対応しているので、一つのOCIオブジェクトから複数のOciCursorオブジェクト(カーソル)を持つことができる。したがって、Direct ORAのオブジェクト構造も複数セッションに対応している。具体的には、次のようにOpenプロパティを呼び出して、OciCursorオブジェクトを作成する。
    Dim oCur As OciCursor
    Set oCur = oOra.Open
    
     

  4. SQL文の実行 CompileメソッドでSQL文をコンパイルし、Executeメソッドで実行する。ここで実行できるSQL文は、PL/SQLブロックである。もちろん、SELECTだけでなくいわゆるアクションクエリーも可能である。Bind変数にも対応しているので、引数や戻り値を返すストアドプロシージャ呼び出しも可能である。
    oCur.Compile = "select * from …"
    oCur.Execute
    

  5. Fetchとカラムの取得 各行は、Fetchプロパティをループ条件として取得することができる。取得した一行は、コレクションとしてOciColumnオブジェクトに入る。For EachでこのオブジェクトのNameプロパティでカラム名を、Valueプロパティで値をすることができる。次に、最初の行のときだけカラム名も表示し、それ以後はカラムの値を表示する例を示す。
    Dim oCol As OciColumn
    bFirstTime = True
    Do While oCur.Fetch
    	If bFirstTime Then
    		sValue = ""
    		For Each oCol In oCur.Columns
    			sValue = sValue & oCol.Name & vbTab
    		Next
    		lstX.AddItem sValue
    		bFirstTime = False
    	End If
    	sValue = ""
    	For Each oCol In oCur.Columns
    		sValue = sValue & oCol.Value & vbTab
    	Next
    	lstX.AddItem sValue
    Loop
    

  6. 処理された行数の取得 実際に処理された行数は、OciCursorオブジェクトのRowAffectedプロパティで得ることができる。
     
    MsgBox CStr(oCur.RowsAffected) & "レコード処理しました。", vbInformation, "DirectOra"
    

  7. セッションの終了 セッションを終了するには、OciCursorオブジェクトをCloseメソッドで閉じる。OCIオブジェクト自体は、プロセス終了時に自動的に解放されるが、NothingをSetして明示して解放することもできる。
    oCur.Close
    Set oOra = Nothing
    

以上がカンタンな使い方であるが、Insert、Update、Deleteなどのときには、Oracleはオートコミットがデフォルトではないので、commitあるいはrollbackなどをしてやる必要がある。これも、SQL文として実行することも可能だが、DirectORAではメソッドとしても実装する。

DirectORAでのBind変数の使い方

DirectORAでは、Bind変数も使うことができるようになっている。
サンプルのディレクトリ: \DirectORA\Samples\bind
  DirectORAのオブジェクトモデルにある、OciFloat、OciInteger、OciStringの各オブジェクトのValueプロパティは、それぞれがFloat、Integer、Stringのバインド変数として、Visual Basicなどのツール側からアクセスすることができる。
たとえば、次のように各オブジェクトを作成したとしよう。
Dim fval As New OciFloat
Dim fval2 As New OciFloat
Dim ival As New OciInteger
Dim sval As New OciString

次のPL/SQLブロックで、:f、:f3、:s、:I、:cはそれぞれPL/SQLブロック側、すなわちOracleサーバーが解釈するバインド変数である。

begin
	:f := 3.14;
	:f3 := :f * :f2;
	:s := 'hello, world';
	:i := 123;
	:c := 'Edit!';
end;

このPL/SQLブロックをコンパイルする。

oCur.Compile txtSQLBind.Text

Visual Basic側では、次のようにBindメソッドを使ってVisual Basic側でアクセスできる変数としてバインドする。たとえば、PL/SQLブロック中の「:f」はfvalにバインドされる。また、ここでは文字列型をTextBoxコントロールのデフォルト値であるTextプロパティにバインドすることもできる。

oCur.Bind "f", fval
fval2 = txtF2.Text
oCur.Bind "f2", fval2
oCur.Bind "f3", txtF3
oCur.Bind "i", ival
oCur.Bind "s", sval
oCur.Bind "c", txtReturn

Executeメソッドで実行すれば、Bindされていることが確認できる。

oCur.Execute
Debug.Print fval
Debug.Print ival
Debug.Print sval

このプログラムでおもしろいのは、Visual Basicのように Signed型の整数値しか扱えない言語ではオーバーフローしてしまうような計算を、Oracleにやらせてしまうことができることだ。もっとも、計算だけさせるのはナンセンスであるから、なんらかのクエリーと共に実行すべきだろう。
また、DirectORAでは、このようなPL/SQLブロックを実行できるから、ストアードプロシージャやストアードファンクションを実行することもできる。もちろん、引数や戻り値の受け渡しは上記のようにバインド変数を使えばカンタンである。

DirectORAでのデータバインディングコントロールへの対応実験

OracleのBind変数は、クエリー時に変化する変数をやり取りするものである。これは便利なものだが、Visual Basicなどのデータバインディングコントロールとは違う。つまり、複数のレコードセットが返されたときには、Bind変数がそのままコントロールの値にはならないのである。
データバインディングコントロールは、クエリーした結果が自動的にカラムと結びつけられたコントロールに表示され、コントロールの内容をユ−ザーが変更すれば、自動的にUpdateもされる。これは確かに用途を限定したメンテナンスプログラムでは便利である。しかし、多少複雑なクエリーであっても、SELECTだけでなくUPDATE文も自動的に発行しなくてはならず、そのための構文解釈やバッファなどが必要になるため、ミドルウェアとしては重いものにならざるを得ないと言えるだろう。しかし、その便利さは素敵だ。
そこで、DirectORAでは、Fetchしてきた特定カラムと結びつけるLinkプロパティを実験的につけてみた。
次のようなPL/SQL文があったとしよう。
select empno,ename from emp where empno > to_number(:empno)

このSQL文を実行し、WHERE節の条件中にある:empnoはテキストボックスにBindされるとしよう。

oCur2.Compile txtSQLBind2.Text
oCur2.Bind "empno", txtMinEmpNo
oCur2.Execute

Executeメソッド発行後に、Linkメソッドでempnoとenameを、それぞれテキストボックスtxtEmpNoとtxtEnameにデータバインディングすることができる。

oCur2.Columns.Item("empno").Link = txtEmpNo
oCur2.Columns.Item("ename").Link = txtEname

一度、データバインディングしてしまえば、別のボタンなどでFetch していくと、次の行の内容も自動的に表示される。
確かに便利である。ここまでできたらUPDATEまで自動にしたいような気もするが、DirectORAを名乗るシンプルなミドルウェアという思想からは少々離れてしまう。まあ、更新くらいはUPDATE文をBind変数を使って記述してもよいだろう。確かに少々SQLなどを書く手間はあるが、その分軽くなるのである。
この機能については、本来の軽いダイレクトなミドルウェアという趣旨から外れるので、将来的にはサポートしなくなる可能性がある。

MovePreviousを実現するには

DirectSQLとDirectORAについて説明してきた。シンプルな構成ながら、SQL文をうまく活かせば、既存の開発環境より効率が上げられることは容易に予想できるだろう。
しかし、セッションを張り、SQLを実行し、Fetchするというネイティブなやり方では、Visual BasicやAccessで作成されたメンテナンスプログラムでは当たり前のようにできる「MovePrevious」の機能、すなわち一行前に戻ることができない。これは、仕様からして当然のことである。逆に、それができるほどにバッファリングなどを内部的にやっている既存のツール&ミドルウェアでは、いかに無駄にリソースを使っているか分かろうというものだ。
それでも、MovePreviousをしたいというのであれば、話はカンタンである。現在の行の位置を記憶しておき、もう一度クエリーをしなおして、その直前行まで空読みすればよいのだ。確かに効率はあまり良くないかもしれないが、読み飛ばしてしまえば、意外に速いものではある。これについては、各ミドルウェアのSamples\Editディレクトリにあるサンプルプログラムで実現している。
それよりも、本当にMovePreviousが必要なケースはそんなにあるのかを考えてほしい。できる限り、クエリーして得る結果セットは少ない方が効率よくアクセスできるし、サーバーへの負担やネットワークの負荷も軽いのである。
確かに、何でもデータが自由にいじれるようにしたいとユ−ザーが要求することは往々にしてある。MovePreviousだけではなく、制約までかけた複数のテーブルに自由にアクセスしたいなどということすらある。そのときには、「Accessでアタッチしてください」と言ってしまった方がよいだろう。そうでないと、スキーマの整合性までも壊された責任を負わされかねないのだから。

Directミドルウェアを使った効率的なシステム開発例

DirectSQLやDirectORAは、COMコンポーネントとして実現されている。これは、RDOやADOと同様であるが、大きく違うのは、ISQL/wやSQL*PlusといったダイレクトにTransact SQLあるいはPL/SQLブロックを実行できるツールと同じように、SQLを実行できることだ。すなわち、SELECTなどのクエリーやアクションクエリーといった区別なくRDBMSネイティブなSQL文をそのまま実行できるのである。さらに言えば、CREATE TABLEなどのDDL文すら実行することができる。すなわち、何でもアリなのだ。
それというのも、これらのコンポーネントは、DB LibraryやOCIにネイティブに接続しているからである。SQL ServerやOracleのEnterprise Managerは、クライアントサイドからサーバーを管理しているが、これらもネイティブ接続だからできるのである。つまり、Directコンポーネントのようなものを使えば、サーバーの管理ツールと同等以上のものを作ることもできるわけだ。
そこで、Directコンポーネントを使った例として、現実の開発を踏まえたツールを作ってみた。ここで紹介するものは、あくまでもサンプルとしての簡易版であるが、私たちの会社では、これらを発展させたようなツールを使って現実に開発をしている。
サンプルプログラムのディレクトリ
SQL Server版: DirectSQL\Samples\Edit\
Oracle版: DirectORA\Samples\Edit\
  1. スキーマ作成& 初期データ投入ツール
    ※サンプルプログラム
    SQL Server版: CREATE_SQL.XLS
    Oracle版: CREATE_SQL.XLS

    ExcelからDirectコンポーネントを使い、シート上に記述されたテーブル定義を元にして、テーブルやインデックス(複数のカラムを組み合わせたものも可)を作成することができるツールである。ここでは、シート名=テーブル名としてテーブルの定義がされている。

    さらに、.dateがついたシートには、初期データが入力されており、テーブル作成後に投入することができる。データには、SQLの関数を記述して投入日時が自動的に入るようにすることもできる。

    本来はあまりよくないことではあるが、ユ−ザーの要求や開発中に発覚した事情のためスキーマの設計が変更せざるを得ないことがある。このようなときに、スキーマ作成をExcelのシートに記述しなおすだけで、スグにスキーマを再作成できるのは、実に効率がよい。
    また、デバッグ時にデータを初期化したいときや、実際に運用に入ってからもマスターのデータを入れ換えたりしなくてはならないこともある。こういったときにも、このツールは威力を発揮する。
    同じように考えていくと、年度末などにデータをExcelに取得して統計を取ったり、別のバックアップテーブルに移行させることも、カンタンである。

  2. データ入力ツール
    ※サンプルプログラム:Edit.Vbp
    ここでは、Visual BasicとDirectコンポーネントを使って、1.で入力したマスターデータを元にして、蓄積データを作成するアプリケーションを作った。

    このツールでは、商品マスターITEMと担当者マスターPERSONを参照しながら、販売蓄積テーブルSALEにデータを追加、修正、削除ができるようになっている。消費税などの金額計算は自動で行われる。
    検索では、入力された販売日付をキーにして、複数のレコードを移動して表示できるようになっている。MovePreviousは、先に述べたような手法で実現している。

  3. 帳票印刷ツール
    ※サンプルプログラム
    SQL Server版:REPORT_SQL.XLS
    Oracle版: REPORT_ORA.XLS

    帳票印刷ツールでは、またまたExcelを使っている。販売蓄積テーブルと各マスターにアクセスし、売り上げの一覧をシートに取得している。
    このツールではあまり手をかけていないが、Excelの表形式には限界はあるものの、それなりにきれいにフォーマットして印刷をすることができる上、集計などもすることができる。さらに、グラフ化などもできる。
    さらによいことは、このシートごと別名で保存しておけば、日報や月報などを、紙だけでなくそのときのExcel形式のデータとして蓄積することができる。
    このように、あまり凝ったレイアウトが要求されないのであれば、Excelはかなり使い勝手がよく、応用も効く帳票ツールとして使うことができるのである。

    データアクセス開発は効率化できる

    今回は、私たちの会社で作成したDirectコンポーネントという一般的ではないミドルウェアを使った例を紹介した。確かにミドルウェアとしては一般的なものではあるがDB LibraryやOCIの機能にかなり近く、データアクセスの基本的な部分を実感していただけたことだろう。
    繰り返し述べているように、RDBMS、ミドルウェア、クライアントツールの役割を適切に割り振って、それぞれの特徴をうまく活かすことができれば、データアクセスの開発は効率化できる。特に重要なのは、ネイティブなSQLの理解とその使いこなしである。ベンダーが押しつけている一件イージーな開発環境は、必ずしも最良のものではないどころか、開発現場を混乱させている元なのである。
    今回のDirectコンポーネントやそれをベースにしたツールは、手前味噌でいささか恐縮ではあるが、実際に我々が効率的な開発をしようと努力してきた過程で生まれてきたものであり、それなりによい結果をもたらしてきた。
    しかし、これらはまだ完璧ではない。今後、ミドルウェア部分のブラッシュアップはもちろん、テーブル定義だけでなく、実際にアプリケーションの動作の定義を元にしてユ−ザーインターフェイスを構築するようなツールも作っていきたいと考えている。
    今後、このようなコンポーネントやツールのサンプルは、私たちの会社のWWWサイト(http://www.int21.co.jp/directmw/)からダウンロードできるようにする。また、ある程度の形になったところで、市販することも検討したいと考えている。興味のある方は、ぜひアクセスしていただき、ご意見をいただければ幸いである。

    最後に、開発者の皆様に今一度申し上げたい。
    与えられたものや市販されている一般的なモノの使い方をマスターするのではなく、基本に立ち返り、構造や動作原理を理解しよう。そうすれば、おのずと最良の方針が見えてくるハズだ。その方針にあったものがあれば購入してもよいだろうし、なかったら、作るなり作らせるなりすることもできるのだ。
    「とにかくスグに始めなくてはならないから、そんなヒマはない。ツールありきなのだ」とおっしゃる気持ちも分かる。しかし、昔の人はこんなよい格言を残している。
    「急がば回れ」
    である。


    int21 Home | Directミドルウェアシリーズ



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