3回目に入ります。
前回までの話で、いろんなことを解決してきました。ちょっと整理してみましょう。
1. SQLGetData()呼び出しにおけるオーバヘッドの改善
2. SQLBindCol()利用によるフェッチ後のデータ取得の改善
3. SQLSetPos()利用による、カーソルに対する追加・更新・削除・行ロック
最後の課題は、既存のコードを書き換えないで高速化するというものです。
既存のコードというのは、顧客がすでにプログラミングを完了しているコードのことです。つまり、再コンパイルだけで新しい動作に変更できるようにするということです。
結論から先に述べれば、顧客がすでに作っているクラスと等価なクラスを提供できれば解決します。運が良いことに、その顧客には、事前にインターフェイスについて決め事を作っていました。製品が持っているクラスを直接使うのではなく、サブクラス化して使う、ということをお願いしていたので、問題となっている「実行速度の遅い」クラスとの結合度は密ではないようにしてありました。
ここからは、オブジェクト指向によるソリューションです。
まず、改善点をすべて網羅した基本クラスを作りました。斜体部分のメソッドは抽象メソッドで実体がないものです。
+---------------------------+
| SQLRowset |抽象クラス名
+---------------------------+
| EOF |EOFかどうかの判定
| FCount |列の数の総数
| Status |ODBCのエラーステータス
+---------------------------+
| Axit() |オブジェクトの破棄メソッド(デストラクタ)
| BindDataColumns() |列のバインド
| ClearCurrentRowBuffer() |行バッファのクリア
| Close() |ステートメントの解放
| Commit() |トランザクションのコミット
| Delete() |行削除
| Fetch() |フェッチ実行
| Fin() |バッファを解放し、終了
| Init() |オブジェクトの初期化メソッド(コンストラクタ)
| Insert() |行挿入
| Log() |ログ記録
| ODBC2VO() |ODBCデータ型を利用している言語データ型へ変換
| Open() |SELECTステートメントの実行
| RaiseFatalError() |致命的エラーの通知
| RestoreBuffer() |保存している行バッファからのリストア
| RLock() |行ロック
| Rollback() |トランザクションのロールバック
| SaveBuffer() |行バッファの保存
| SetRowsetOption() |行セットオプションの設定
| ShowErrorMsg() |エラーメッセージの表示
| Update() |行更新
| VO2ODBC() |利用している言語データ型からODBCデータ型への変換
+---------------------------+
△
|
|
+---------------------------+
| ConcreteSubClass |サブクラス名
+---------------------------+
| a<テーブル名> |行セット配列へのプロパティ(読み書き)
+---------------------------+
| BindDataColumns() |具体的な列のバインド
| Init() |オーバライドされたコンストラクタ
| ODBC2VO() |具体的なODBCデータ型->言語データ型へ変換
| VO2ODBC() |具体的な言語データ型->ODBCデータ型への変換
+---------------------------+
このデザインは、抽象クラスであるSQLRowsetそのものをインスタンス化して実行することはできません。サブクラスによって、具体的な列のバインドやデータ変換が実装されることによってはじめて動作可能になります。限りなくクラスに近いインターフェイスとでもいいましょうか。
このために、コードジェネレータを作り、具体的に対象となるテーブルの列がバインドされるようなサブクラスを自動生成しました。このとき、サブクラス名は、顧客がすでに使っているものとまったく同じです。インターフェイスが変わらなければ実装が変わってもオブジェクトのクライアントに影響がない、というのはオブジェクト指向ならではの強みです。
最後に生成されたサブクラス群をライブラリとして、既存のデータベースアクセス用に使われているクラス群と置き換えました。顧客からすれば再コンパイルだけで、既存のコードに一切手を加えることなく移行が完了できたわけです。もちろん、動作速度は改善されました。
参考のために、当時のコードを公開します。これは、CA-Visual Objectsというオブジェクト指向言語で書かれています。文法的には、PascalとCとXbaseの良いところをあわせたものです。ODBC2.xレベルなので、ODBC3.5に移植するには、廃止された関数を考慮する必要がありますが、雰囲気だけでも味わっていただければと思います。
TEXTBLOCK SQLROW.PRG
インポート日付: 1997/01/08 5:42:06
最終更新日: 1997/02/20 17:30 by Akira Onishi
※ SQLRowset:Fetch()において、
SQLExtendedFetch()関数の呼び出し前に、
データバッファをゼロクリアするように変更した
FUNCTION RC_NOTSUCCESSFUL( rc AS ShortInt ) AS LOGIC
//s ODBC関数の戻り値チェック
//l ODBC関数が失敗したかどうかを調べる
//p
//r ODBC関数の戻り値がSQL_SUCCESSまたはSQL_SUCCESS_WITH_INFOでなければ、Trueを
//a RC_SUCCESSFUL()
if rc = SQL_SUCCESS .OR. rc = SQL_SUCCESS_WITH_INFO
return False
else
return True
end
FUNCTION RC_SUCCESSFUL( rc AS ShortInt ) AS LOGIC
//s ODBC関数が成功したかどうかを調べる
if rc = SQL_SUCCESS .OR. rc = SQL_SUCCESS_WITH_INFO
return True
else
return False
end
_DLL FUNC SQLSetPos( hstmt AS long, iRow as short, fRefresh as word, fLock as word ) as short pascal:odbc.SQLSetPos
//s SQLSetPos()関数のプロトタイプ
CLASS SQLRowset
//s SQLRowset抽象クラス
//l ODBCのダイナセットを作成し、ダイナセット上からのレコード追加、更新、削除を実現するクラス
PROTECT cTableName AS String // Table name
PROTECT oConn AS SQLConnection // SQLConnection object
PROTECT hEnv AS LongInt // Environment handle
PROTECT hDbc AS LongInt // Database connection handle
PROTECT hStmt AS LongInt // Statement handle
PROTECT rc AS ShortInt // return value of ODBC API functions
PROTECT dwFetched AS longInt // Number of fetched rows
PROTECT ptrRowset AS PTR // Pointer to the row-set buffer
PROTECT ptrSave AS PTR // Pointer to the saving/restoring buffer
PROTECT ptrStatus AS PTR // Cursor status in the fetched rowset
PROTECT siColumns as Shortint // Number of columns in the row
PROTECT wRecSize AS Word // Record size
PROTECT dwKeysetRows AS LongInt // Number of rows in the key-set
PROTECT dwRowsetRows AS LongInt // Number of rows in the row-set buffer
PROTECT dwSizeofRowset AS LongInt // Size of the row-set buffer
PROTECT siLocalRow AS ShortInt // Local row of the rowset buffer
PROTECT siRetrieved AS Shortint // Number of rows retrieved from the rowset buffer
PROTECT lIsFirstFetch as Logic // Is the first fetching or not?
PROTECT aRowsetPointers as array // The pointers for each row
PROTECT lDestroyed as logic
PROTECT lEOF as logic // EOF indicater
PROTECT oSQLError as SQLErrorInfo
METHOD Axit() CLASS SQLRowset
//s 破棄メソッド
//l ODBCデータソースから切断し、ダイナセット用に利用していたバッファを解放する
if ! lDestroyed
self:Fin()
end
METHOD BindDataColumns() CLASS SQLRowset
//s データバッファをバインドする
//l データバッファのバインド用の抽象メソッド。サブクラス側で実装する。
// please override
METHOD ClearCurrentRowBuffer() CLASS SQLRowset
MemSet( ptrRowset, 0, wRecsize )
METHOD Close() CLASS SQLRowset
rc := SQLFreeStmt(hStmt, SQL_DROP)
return RC_SUCCESSFUL( rc )
METHOD Commit() CLASS SQLRowset
if SQLTransact( hEnv, hDbc, SQL_COMMIT ) = SQL_SUCCESS
return True
else
return False
end
METHOD Delete() CLASS SQLRowset
if self:RLock()
rc := SQLSetPos(hStmt, 1, SQL_DELETE, SQL_LOCK_NO_CHANGE)
if RC_SUCCESSFUL( rc )
return True
else
return False
end
else
return False
end
return True
ACCESS EOF() CLASS SQLRowset
return lEOF
ACCESS FCount() CLASS SQLRowset
return siColumns
METHOD Fetch() CLASS SQLRowset
local lSuccess as logic
if lIsFirstFetch
// 最初のFETCH
self:ClearCurrentRowBuffer()
rc := SQLExtendedFetch( hStmt, SQL_FETCH_NEXT, 0, @dwFetched, ptrStatus )
lIsFirstFetch := False
siLocalRow := 1
if rc != SQL_SUCCESS
self:ShowErrorMsg(#FetchFirst)
end
elseif siLocalRow >= dwFetched
if dwFetched > 0
// バッファから取り出した行数が、FETCHした行数と同じ場合
self:ClearCurrentRowBuffer()
rc := SQLExtendedFetch( hStmt, SQL_FETCH_NEXT, 0, @dwFetched, ptrStatus )
siRetrieved := 0
siLocalRow := 1
end
else
// バッファから行を取り出す
siLocalRow += 1
rc := 0
end
lSuccess := RC_SUCCESSFUL( rc )
if ! lSuccess
lEOF := True
else
lEOF := False
self:ODBC2VO()
siRetrieved += 1
end
return lSuccess
METHOD Fin() CLASS SQLRowset
// Free allocated buffers
MemFree( ptrRowset )
MemFree( ptrStatus )
aRowsetPointers := NIL
lDestroyed := True
METHOD Init(cTable, oConn, nRecSize, nKeySetRows, nRowsetRows, nColumns) CLASS SQLRowset
local dwPtr, dwPtrTop as DWORD
local siRow as shortint
// Initialize the database connection
self:oConn := oConn
hEnv := oConn:EnvHandle
hDbc := oConn:ConnHandle
Default( @cTable, DEFAULT_TABLE )
Default( @nRecSize, DEFAULT_SIZE )
Default( @nKeySetRows, DEFAULT_KEYSET )
Default( @nRowSetRows, DEFAULT_ROWSET )
lIsFirstFetch := True
// Table name
cTableName := cTable
// A size of record
wRecSize := nRecSize
dwKeysetRows := nKeySetRows
dwRowsetRows := nRowsetRows
dwSizeofRowset := dwRowsetRows * wRecSize
siColumns := nColumns
ptrRowset := MemCAlloc( dwRowsetRows+1, wRecSize )
if ptrRowset == NULL_PTR
self:RaiseFatalError("メモリを確保できません")
return self
end
// 行単位のポインタ配列を作成する
aRowsetPointers := ArrayCreate( dwRowsetRows+1 )
AFill( aRowsetPointers, 0L )
dwPtr := DWORD( _CAST, ptrRowset )
dwPtrTop := dwPtr
for siRow := 1 upto dwRowsetRows+1
dwPtr := dwPtrTop + (siRow-1)*wRecSize
aRowsetPointers[siRow] := PTR( _CAST, dwPtr )
next
ptrSave := aRowsetPointers[dwRowsetRows+1]
ptrStatus := MemCAlloc( dwRowsetRows, _sizeof(ShortInt) )
if ptrStatus == NULL_PTR
self:RaiseFatalError("メモリを確保できません")
return self
end
// avoid NO_DATA_FOUND
siLocalRow := 1
return self
METHOD Insert() CLASS SQLRowset
self:SaveBuffer()
self:VO2ODBC()
rc := SQLSetPos(hStmt, 1, SQL_ADD, SQL_LOCK_NO_CHANGE)
self:RestoreBuffer()
if RC_SUCCESSFUL( rc )
return True
else
self:ShowErrorMsg(#Insert)
return False
end
METHOD Log(cMsg) CLASS SQLRowset
METHOD ODBC2VO(isAppend) CLASS SQLRowset
// Abstract method
// In your subclass, you have to override this method
return True
METHOD Open(cCondition) CLASS SQLRowset
local cStatement as string
local pszStatement as psz
// Allocate the statement handle
rc := SQLAllocStmt( hDbc, @hStmt )
if rc != SQL_SUCCESS
self:ShowErrorMsg(#Open)
end
// Set options for rowset binding
self:SetRowsetOption()
// Bind data columns
self:BindDataColumns()
cStatement := "SELECT * FROM "+cTableName
if ! IsNil( cCondition )
cStatement += " WHERE "+ cCondition
end
// Convert String to PSZ
pszStatement := String2Psz( cStatement )
// Execute the statement
rc := SQLExecDirect( hStmt, pszStatement, SQL_NTS )
if rc != SQL_SUCCESS
self:ShowErrorMsg(#Open)
end
return RC_SUCCESSFUL(rc)
METHOD RaiseFatalError( cMsg ) CLASS SQLRowset
local oD as ErrorBox
oD := ErrorBox{ ,cMsg}
oD:Show()
oD:Axit()
return NIL
METHOD RestoreBuffer() CLASS SQLRowset
local p as ptr
p := aRowsetPointers[siLocalRow]
MemCopy( p, ptrSave, wRecSize )
METHOD RLock() CLASS SQLRowset
rc := SQLSetPos(hStmt, siLocalRow, SQL_POSITION, SQL_LOCK_EXCLUSIVE)
if RC_SUCCESSFUL( rc )
return True
else
return False
end
METHOD Rollback() CLASS SQLRowset
if SQLTransact( hEnv, hDbc, SQL_ROLLBACK ) = SQL_SUCCESS
return True
else
self:ShowErrorMsg(#RollBack)
return False
end
METHOD SaveBuffer() CLASS SQLRowset
local p as ptr
p := aRowsetPointers[siLocalRow]
MemCopy( ptrSave, p, wRecSize )
METHOD SetRowsetOption() CLASS SQLRowset
rc := SQLSetStmtOption( hStmt, SQL_CONCURRENCY, SQL_CONCUR_ROWVER )
if ! RC_SUCCESSFUL( rc )
self:ShowErrorMsg(#SetRowsetOption)
end
SQLSetStmtOption( hStmt, SQL_KEYSET_SIZE, dwKeysetRows )
rc := SQLSetStmtOption( hStmt, SQL_CURSOR_TYPE, SQL_CURSOR_DYNAMIC )
if RC_SUCCESSFUL( rc )
rc := SQLSetStmtOption( hStmt, SQL_ROWSET_SIZE, dwRowsetRows )
if RC_SUCCESSFUL( rc )
rc := SQLSetStmtOption( hStmt, SQL_BIND_TYPE, wRecSize )
else
self:ShowErrorMsg(#SetRowsetOption)
end
else
self:ShowErrorMsg(#SetRowsetOption)
end
if RC_NOTSUCCESSFUL( rc )
self:ShowErrorMsg(#SetRowsetOption)
end
RETURN RC_SUCCESSFUL( rc )
METHOD ShowErrorMsg(symMethod) CLASS SQLRowset
oSQLError := SQLErrorInfo{self,symMethod , hEnv, hDbc, hStmt }
oSQLError:ShowErrorMsg()
oSQLError := NIL
ACCESS Status() CLASS SQLRowset
return oSQLError
METHOD Update() CLASS SQLRowset
if self:RLock()
self:SaveBuffer()
self:VO2ODBC()
rc := SQLSetPos(hStmt, 1, SQL_UPDATE, SQL_LOCK_NO_CHANGE)
self:RestoreBuffer()
if RC_SUCCESSFUL( rc )
return True
else
self:ShowErrorMsg(#Update_SQLSetPos )
return False
end
else
self:ShowErrorMsg(#Update_RLock)
return False
end
METHOD VO2ODBC() CLASS SQLRowset
// Abstract method
// In your subclass, you have to override this method
return True
DEFINE DEFAULT_KEYSET := 10
DEFINE DEFAULT_ROWSET := 10
DEFINE DEFAULT_SIZE := 10 // Don't care of this value
DEFINE DEFAULT_TABLE := "Unknown"
//具象クラスの例
TEXTBLOCK MKTB1031.PRG
インポート日付: 1997/02/20 10:52:19
TEXTBLOCK mktb1031クラス定義
作成日付:97/02/20 10:17:04
CLASS mktb1031 INHERIT SQLRowset
PROTECT aR[4] AS Array
PROTECT p AS PTR
ACCESS amktb1031() CLASS mktb1031
return aR
ASSIGN amktb1031(aN) CLASS mktb1031
ACopy( aR, aN )
return aR
METHOD BindDataColumns() CLASS mktb1031
local D as STRU_mktb1031
D := p
D.c_paydate:= SQL_NTS
SQLBindCol( hStmt, 1, SQL_CHAR,@D.s_paydate[1], 3,@D.c_paydate)
D.c_dvnnm:= SQL_NTS
SQLBindCol( hStmt, 2, SQL_CHAR,@D.s_dvnnm[1], 5,@D.c_dvnnm)
D.c_explntn:= SQL_NTS
SQLBindCol( hStmt, 3, SQL_CHAR,@D.s_explntn[1], 21,@D.c_explntn)
D.c_seqno:= SQL_NTS
SQLBindCol( hStmt, 4, SQL_CHAR,@D.s_seqno[1], 6,@D.c_seqno)
METHOD Init(oConn) CLASS mktb1031
local S as word
S := ROWSET_ROWS_OF_mktb1031
Super:Init('mktb1031', oConn,_sizeof(STRU_mktb1031), S, S, 4)
p := ptrRowset
METHOD ODBC2VO() CLASS mktb1031
local z as PSZ
local c as string
local fV as float
local b as STRU_mktb1031
b := aRowsetPointers[siLocalRow]
c := Psz2String( PSZ(_CAST, @b.s_paydate) )
aR[ 1] := c
c := Psz2String( PSZ(_CAST, @b.s_dvnnm) )
aR[ 2] := c
c := Psz2String( PSZ(_CAST, @b.s_explntn) )
aR[ 3] := c
c := Mem2String( @b.s_seqno[1], 5)
fV := Val(c)
aR[ 4] := fV
METHOD VO2ODBC() CLASS mktb1031
local z as PSZ
local c as string
local b as STRU_mktb1031
b := aRowsetPointers[siLocalRow]
z := String2Psz( MBRTrim(aR[ 1]) )
MemSet( @b.s_paydate, 0, 3)
MemCopy( @b.s_paydate, z, pszLen(z))
b.c_paydate := SQL_NTS
z := String2Psz( MBRTrim(aR[ 2]) )
MemSet( @b.s_dvnnm, 0, 5)
MemCopy( @b.s_dvnnm, z, pszLen(z))
if b.s_dvnnm[1] = 0x00
b.c_dvnnm := SQL_NULL_DATA
else
b.c_dvnnm := SQL_NTS
end
z := String2Psz( MBRTrim(aR[ 3]) )
MemSet( @b.s_explntn, 0, 21)
MemCopy( @b.s_explntn, z, pszLen(z))
if b.s_explntn[1] = 0x00
b.c_explntn := SQL_NULL_DATA
else
b.c_explntn := SQL_NTS
end
c := LTrim(STR( aR[ 4], 5, 0))
z := String2Psz( c )
MemSet( @b.s_seqno, 0, 6)
MemCopy( @b.s_seqno, z, pszLen(z))
if b.s_seqno[1] = 0x00
b.c_seqno := SQL_NULL_DATA
else
b.c_seqno := SQL_NTS
end
STATIC DEFINE ROWSET_ROWS_OF_mktb1031 := 1
// 上の値は、1回のフェッチで取得する行数を示します。
STRUCTURE STRU_mktb1031
member dim s_paydate[3] AS BYTE
member c_paydate AS LONGINT
member dim s_dvnnm[5] AS BYTE
member c_dvnnm AS LONGINT
member dim s_explntn[21] AS BYTE
member c_explntn AS LONGINT
member dim s_seqno[6] AS BYTE
member c_seqno AS LONGINT
ここまでお付き合いいただきありがとうございます。
今後余力があれば、ADODB vs ODBCパフォーマンス対決、みたいなコードを書いてみると面白いかと考えていますが、時代が.NET FrameworkのADO.NETに流れている今、そんなニッチなことに興味を持つ人は少ないだろうと思います。
私がODBCと闘った経験は、私がODBCによるデータベースアクセス技術に対して無知であることを知ることからはじまりました。しかし、未経験でもドキュメントを読み、仮説を立て、実験を行い、結果を考察し、・・・と繰り返すことで、解決にいたりました。これは技術者としてよい経験でした。Inside ODBC、ODBC Programmer's referenceなどの洋書を自腹で買って(!)、今から思うとすごいバイタリティだったなぁと思います。
今回の記事で理解していただきたいことは、「限られた制約の中で窮地に陥っても頭を使えば、最適なソリューションを生み出すことができる」ということです。当時の私の経験においては、結果として、実行可能性の証明・アプリケーションパフォーマンスの改善・開発生産性の改善を与えられたODBCドライバと開発ツールのみで実現できました。時間というコストはかかりましたが、新しいものは何も買わずに済んだのです。目先の道具に文句を言わず、皆さんも頭を働かせて見ましょう。現在の開発ツールの進化はすさまじいものがあります。ただし道具は道具。いい道具を持っていることが優秀なエンジニアの条件ではありません。与えられた道具をいかに活用するか、与えられた制約の中でいかに実現するか、これは、今後も未来永劫続くことだと思います。
ODBCは遅くありません。SQL ServerのEnterprise ManagerやMSDEのOsqlがODBCアプリケーションであることがそれを証明しています。また、ODBCはISOの規格であり、もはやマイクロソフトやWindowsだけで動くテクノロジーではありません。UnixでもODBCは使えます。ISOレベルのデータベースのCLIであるODBC、まだまだ生き残っていくと思います。
今後、データベースアクセス技術は、オブジェクト指向系だと特に.NET FrameworkネイティブなアクセスメソッドとJavaのJDBCの2極に分かれるかと思います。プロセッサは早くなり、ディスクI/Oも早くなり、メモリも潤沢にある、そんな時代だからといってルーズなデータアクセスを行うのはどうかと思います。データベースアクセス技術は地味ですが、アプリケーション開発の要です。今は書店にいけば、ADO.NETやJDBCの参考書籍は簡単に手に入ります。読者の皆さんはとても恵まれている環境下にあることを忘れないでください。インターネットを使えば、MSDNライブラリにもアクセスできます。皆様が開発されるデータベースアプリケーションがよいものとなりますように。
[EOF]