この特集のトップページへ
Chapter 6:ビジネスロジックの設計

6.2.2 顧客情報の編集
●データオブジェクトの構築

○履歴の処理
 以上でデータオブジェクトであるDataObj.Customerコンポーネントを通じて,既存の顧客情報の取得ならびに変更ができるようになったが,Chapter3で説明したように,今回のサンプルではデータが更新されたときには,その履歴をすべて履歴テーブルに記録するような機構を設けることにする。

 DataObj.Customerコンポーネントにおいて,データを更新するのはList 6-17に示したUpdateRecordメソッドとList 6-18に示したUpdateRecord_BILLDAYメソッドである。この2つのメソッドに対し,履歴を追加する機構を備えるようプログラムを拡張してゆく。

 履歴テーブルは,Chapter3においてTable 6-6のように定義した。

Table 6-6 履歴テーブル
フィールド名 サイズ Null 解説
ID 数値型(オートナンバー) 不可 レコードに対して唯一無二の値を割り当てる
DATE 日付型 不可 変更が生じた日時
TABLENAME 文字型 32 不可 変更が生じたデータベーステーブル名
FIELDNAME 文字型 32 不可 変更が生じたデータベーステーブルのフィールド名

RECORDID

数値型

不可

変更が生じたレコードのレコードID(そのデータベーステーブルにおいて主キーとなっているフィールドの値)

OLDDATA 文字型 256 更新前のデータ
NEWDATA 文字型 256 更新後のデータ
UPDATEUSER 文字型 256 不可 更新したユーザーのアカウント名

 UpdateRecordメソッドとUpdateRecord_BILLDAYメソッドに対し,直接履歴テーブルも書き加えるようにプログラムを変更してもかまわないが,履歴テーブルを書き換えるという操作は,のちに実装してゆく製品情報テーブルや伝票テーブルの更新などにも必要となってくる。そこで,履歴テーブルを操作するDataObj.Historyというコンポーネントを新たに導入し,履歴テーブルを操作するメソッドをそこにまとめてしまうことにしよう。

 DataObj.Historyコンポーネントを作るためには,まずDataObjプロジェクトにおいてHistoryクラスを追加する。そして,Historyクラスのトランザクションの状態を「トランザクションが必要」にするため,HistoryクラスのMTSTransactionModeプロパティの値を[RequiresTransaction]に変更する(Fig.6-41)。

Fig.6-41 Historyクラスのプロパティ
fig6_41

 クラスモジュールの設定が終わったならば,作成したHistoryクラスにList 6-20に示すプログラムを記述する。

 List 6-20の29行目で記したAddHistoryメソッドは,データベーステーブル名,フィールド名,更新されたレコードのレコードID,更新前の値,更新後の値を与えると,それを履歴テーブルに追加する。29行目よりまえの部分は,コンストラクタ文字列を取得してそれをデータベース接続文字列として内部的に利用するための処理である(「データベース接続文字列を動的に設定できるようにする」を参照)。

 DataObj.Historyコンポーネントを実装したならば,List 6-17UpdateRecordメソッドとList 6-18UpdateRecord_BILLDAYメソッドをそれぞれList 6-21List 6-22に示すように変更する。

 List 6-21List 6-22に追加された処理は,「DataObj.Historyコンポーネントを実体化しておき,データを設定するまえの値と設定しようとしている値が違った場合には,そのAddHistoryメソッドを呼び出して履歴テーブルに追加する」というものである。Fig.6-41に示したように,DataObj.Historyコンポーネントは[トランザクションが必要]に設定されているため,DataObj.Customerオブジェクトから実体化したときには,同じトランザクション範囲内に属することになる。したがって,DataObj.HistoryコンポーネントのAddHistoryメソッドの処理に失敗したら,トランザクションはアボートとなる。つまり,履歴テーブルへの書き込みに失敗したにもかかわらず,顧客情報テーブルが書き換えられてしまうことはない。

注意List 6-21の26行目とList 6-22の20行目では,DataObj.Historyコンポーネントを実体化する際にCreateObject文を使っている。これを,Newキーワードに置き換えてはならない。List 6-21ならびにList 6-22は,DataObj.Customerコンポーネントに実装するものである。つまり,DataObj.CustomerコンポーネントもDataObj.Historyコンポーネントも同じDataObjという名前のプロジェクトに備わるクラスモジュールに記述するわけだ。Visual Basicにおいて,同一のプロジェクトに備わるクラスモジュールをNewキーワードで実体化したときには,COMやCOM+の機構を使って実体化されるのではなく,Visual Basic内部で実体化される。つまり,COM+のアクティベータを通らないことになり,インタセプタなどは設定されないことになる。その結果,実体化されたオブジェクト(この実体化されたオブジェクトはCOMやCOM+の機構を通っていないので,COMオブジェクトではなく,単なるVisual Basicのオブジェクトである)は,同じトランザクション範囲に入らなくなる。

 List 6-21List 6-22では,設定するまえの値と設定しようとしている値が異なるかどうかを調べるのに,あまり一般的に使われないXor演算子を使っている。ここで,その点について補足説明をしておく。

 たとえば,List 6-21の50〜53行目は次のようにした。


 50:     If objRec.Fields("NAME").Value <> NAME Or _
              (IsNull(objRec.Fields("NAME").Value) Xor IsNull(NAME)) Then
 51:         objHistory.AddHistory "顧客情報", "NAME", _
                                   CUSTOMERID, objRec.Fields("NAME").Value, NAME
 52:         objRec.Fields("NAME").Value = NAME
 53:     End If

 50行目は,objFields("NAME").ValueNAME変数の内容が等しくないことを確認している文である。仮に,50行目を次のようにしたとしよう。


 50:     If objRec.Fields("NAME").Value <> NAME Then

 こうした場合,objRec.Fields("NAME").ValueもしくはNAME変数の内容がNullであると,“objRec.Fields("NAME").Value <> NAME”の条件式の評価はTrueにもFalseにもならず,Nullとなる。つまり,objRec.Fields("NAME").ValueもしくはNAME変数の内容がNullの場合,51行目と52行目は実行されないことになる。そこで次のように,Xor演算子とIsNull関数を使っているのだ。


 50:     If objRec.Fields("NAME").Value <> NAME Or _
              (IsNull(objRec.Fields("NAME").Value) Xor IsNull(NAME)) Then

One Point! ただし,Table 6-4に示した顧客情報テーブルを見るとわかるように,NAMEフィールドはNull値を不許可にしてある。よって,objRec.Fields("NAME").ValueNAME変数もNullになることはないため(NAME変数がNullである場合にはList 6-21の33行目にあるChk_NAMEメソッドの呼び出しで実行時エラーが発生する(List 6-2を参照),NAMEフィールドに限っては,上記のOr演算子以降は不要である。しかし,ここではほかのフィールドとの表記の統一化のためにOr以降の条件を付与した。

 Xor演算子は,演算対象のどちらかがTrueであればTrueとなる演算子である(Table 6-7)。

Table 6-7 Xor演算子
A B A Xor B
True True False
True False True
False True True
False False False

 そしてIsNull関数は,引数に与えられた値がNullであればTrueを,そうでなければFalseを返す関数である。よって,“IsNull(objRec.Fields("NAME").Value) Xor IsNull(NAME)”の部分は,objRec.Fields("NAME").ValueNullであるかNAME変数の内容がNullであれば,全体としてTrueとなる条件式である。つまり,条件式“objRec.Fields("NAME").Value<> NAME Or (IsNull(objRec.Fields("NAME").Value) Xor IsNull(NAME))”は,次のいずれかの条件のときにTrueとなる。

(1)objRec.Fields("NAME").ValueNAME変数の内容がともにNullでなく,かつobjRec.Fields("NAME").ValueNAME変数の内容が異なるとき

(2)objRec.Fields("NAME").ValueNAME変数の内容のいずれかがNullのとき

 (2)の条件は少々わかりにくいが,データベースに元から格納されている値(objRec.Fields("NAME").Value)がNullで,データベースに格納しようとしている値(NAME)がNull以外,もしくは,その逆という条件を示している。つまり,レコードのフィールド値がNullのものをNullでないものに変更しようとしている場合か,NullでないものをNullに変更しようとしている場合の条件式を示す。もし,条件式“objRec.Fields("NAME").Value<> NAME Or (IsNull(objRec.Fields("NAME").Value) Xor IsNull(NAME))”のOr以降の部分を省略してしまうと,(2)の条件により,この条件式がTrueになることはない。すなわち,NullのものをNullでないものに変更しようとしたり,NullでないものをNullに変更しようとしている場合には,51行目にある履歴の追加と,52行目にあるレコードの値の変更は,実行されなくなってしまう。

 データベーステーブルのフィールドを扱う場合には,Null値を扱うことが比較的多い。そのため,条件式を使う際にNull値の判断を適切に処理しないと,思わぬ結果を招くことになるので注意したい。


One Point!Or演算子は,条件式のいずれかがTrueであればTrueを返す演算式である。たとえば,objRec.Fields("NAME").ValueNullNAME変数の内容がNullでないとしよう。このとき,条件式“objRec.Fields("NAME").Value<> NAME Or (IsNull(objRec.Fields("NAME").Value) Xor IsNull(NAME))”について考えてみよう。まず,先頭の式“objRec.Fields("NAME").Value <> NAME)”はobjRec.Fields("NAME").ValueNullであるため,評価後の結果はNullとなる。そして,“IsNull(objRec.Fields("NAME").Value) Xor IsNull(NAME))”は,objRec.Fields("NAME").ValueNullであり,NAME変数の内容がNullでないため,全体としてTrueになる。よって,“objRec.Fields("NAME").Value<> NAME Or (IsNull(objRec.Fields("NAME").Value) Xor IsNull(NAME))”は,“Null Or True”となる。この式はNullを含むことになるのだが,Or演算子は条件式のいずれかがTrueであれば,もう片方がたとえNullであってもTrueを返すため,全体としてはTrueとなる。このようにOr演算子は,演算内容のどこかにNullが含まれていたとしても,全体としてNull値になることはない。ただし,片方がFalseで片方がNullの場合,もしくは,両者がNullの場合には,全体としてNullになる。詳しくは,Visual Basicのオンラインヘルプの「Or演算子」「And演算子」「Xor演算子」などの項目を参照してほしい。
prevpg.gif Chapter 6 21/92 nextpg.gif