ナッキーの「Turbo Delphiはじめて奮戦記」 - 第20回 アンケートプログラムでトランザクション処理

By: Hitoshi Fujii

Abstract: SQL、参照データとデータベースの機能を使った処理をご紹介してきました。今回は、アンケートプログラムを正しく更新するために、トランザクション処理を取り入れましょう。

Hide image
nacky75

ナッキー

データベースの機能はいろいろ紹介していただきましたが、奥が深いんだなぁって実感しました。まだまだ知らないことがあるんだろうなぁ。

 

Hide image
takahashi75b

高橋先生

データベースにはいろいろな機能があって、使いやすく管理しやすくなっているんだ。今回は確実にデータを登録する方法を紹介するよ。

    データの更新

いくつかデータを更新してみると、時々変になるんですよね。これって私がドジなせい?教えて、高橋先生!

ナッキー:ときどき登録したはずのデータが、なくなっちゃうんですよね。[保存]ボタンもちゃんと押したのにぃ~。

高橋先生:え?普通に登録すれば問題ないはずだけど、どうやったのかな?

ナッキー:うーん?1つのレコードを編集して、1つのレコードを追加しました。あ、IDがダブってたのかも!

高橋先生:もう1回同じことをやってみてもらえるかな?


では、皆さんも一緒にやってみて、うまくいかないかどうか確認してみてください。InterBaseサーバーを起動します。Windows OSのスタートメニューで「Borland InterBase 7.5 Developer [instance = gds_db]」の「InterBase サービスマネージャー [instance = gds_db]」をクリックします。「ステータス(T)」欄に、「InterBaseをWindowsサービスとして起動する」にチェックが付いていないことを確認して、「InterBaseサーバーは停止中」となっていれば、[起動(S)]ボタンをクリックして起動します。「InterBaseサーバーは稼動中」と変化したのを確認して[閉じる](×)ボタンで終了します。

次にプロジェクトを開きます。Turbo Delphiを起動して、画面中央の「ホームページ」で「ProfileDB.bdsproj」を選択。もし一覧に表示されていなければ、ツールバーの[プロジェクトを開く(Ctrl+F11)]ボタンをクリックします。「プロジェクトを開く」ダイアログボックスから「ProfileDB.bdsproj」を探します。

そのまま、ツールバーの[実行]ボタンをクリックして、プログラムを起動します。DBグリッドコンポーネントからどれかひとつレコード(今回はIDが7のレコード)を選択して「名前」フィールドと「誕生日」フィールドを変更します。変更できたら、[追加]ボタンをクリックして、新しいレコードを作ります。表を参考にして入力します。

ID

名前

ADDR_ID

住所

誕生日

性別

ペットの有無

1

鈴木 太郎

6

東京

1985/06/06

男性


「ID」フィールドが既存のものとダブっているけど、そのまま入力します。実際にはうっかり間違って、IDがダブってしまったんです。だけど、今回は同じ現象を確認していただくために、わざと同じIDを使います。このように編集と追加ができたら[保存(S)]ボタンをクリックします。

Hide image
01データの加工

図01 データの加工

ボタンをクリックするとすぐにエラーメッセージが表示されます。設定によってはエラーメッセージを表示しないようになっている場合があります。表示されなくても問題はありません。これはメッセージを読んで[継続(C)]ボタンをクリックすればいいんですって。英語のところは読めないから読み飛ばして、「メッセージ'データベースエラー:~~'」って書いてあるわね。データベースが悪いんだ。

Hide image
02エラーメッセージ1

図02 エラーメッセージ1

よし、[継続(C)]ボタンをクリック。またメッセージが出ちゃいました。今度は日本語多くてよかった。「メッセージ'レコードが見つかりませんでした。キーを指定されていません'」ってことね。

Hide image
03エラーメッセージ2

図03 エラーメッセージ2

日本語だけどよくわかりませんね。「レコードが見つかりません」って出てくるけど検索なんかしていないのになぁ。[継続(C)]ボタンで続けます。画面上ではちゃんと変更と追加がされていますよね。確認できたら、このままフォームの[閉じる](×)ボタンで終了します。そして、もう1度ツールバーの[実行]ボタンをクリックしてみます。あー、名前などを変更したレコード(今回はIDが7のレコード)は有効なんですが、追加したデータがなくなっちゃっているんです。

Hide image
04再実行時のデータ

図04 再実行時のデータ

新たに追加したレコードがありませんね。「ID」フィールドを間違えたのがまずかったわね。IDがダブらないように、気をつけていても間違うことってありますよね。そうするとレコードがなくなっちゃうかもしれないってことかな?名前などを変更したレコードが有効になっていて、追加のレコードが無くなっていることを確認できたらアンケートプログラムを終了します。

高橋先生:メッセージの英語の部分を読み飛ばしては、もったいないな。よく読めばもう少しどういうことかわかるよ。まず、「例外」は覚えているかな?以前にやったけど忘れているかもしれないね。プログラムを動かす上でのエラーをTurbo Delphiでは「例外」と呼んでいる。例外をコード上で扱うために例外オブジェクトが作られるんだったね。もう一度読み返してみたい時は「第8回の計算機でエラーチェック」を参照してみよう。メッセージから「EDatabaseError」例外ができたことがわかる。例外クラスの種類からどんなエラーなのかがわかるよ。今回はデータベースのエラーということが既にわかっているけれど、皆目見当がつかないときには有効だ。

ナッキー:( ) の中の英語は難しくて全然読めません。

高橋先生:おおまかに訳すと「PROFILEテーブルのINTEG_15フィールドでプライマリ・キーかユニーク・キーに違反がある」ということだね。

ナッキー:プライマリ・キーがダブっていて違反があるのはわかっていますけど、「INTEG_15」って何ですか?

高橋先生:「ID」フィールドのことだよ。IBConsoleでプライマリ・キーの設定をしたあと名前が自動でつけられていたんだ。今、IBConsoleで確認すれば、ついている名前がわかるよ。気になる場合はIBConsoleを起動して、「Local Server」、「PROFILE.DB」とダブルクリックしていってデータベースを開く。一覧の「Tables」項目をクリックしてから「PROFILE」テーブルをダブルクリックしてプロパティを開く。「Properties for: PROFILE」画面が表示されたら[選択された項目の変更]ボタンで「テーブルエディタ」ダイアログボックスを表示すると、付いている名前が確認できるよ。

Hide image
05INTEG_15

図05 INTEG_15

ナッキー:へぇ、勝手に名前が付いちゃうんだ。じゃあ、2番目のメッセージはどういうことですか?

高橋先生:データベースにレコードが見つからないというメッセージだったね。1番目のメッセージは値をチェックしているときのエラーメッセージで、2番目のメッセージは実際に登録しようとしているときのメッセージだ。プライマリ・キーが「1」のレコードを登録しようとしていたけれど正しいIDではないからエラーが表示されたんだ。「ApplyUpdates」メソッドの中でほかの手続きなどを呼んでいるので、複数のメッセージを表示しているよ。

ナッキー:どちらも追加がうまくできなかったことを表してたんですね。でも、どうして最初はフォーム上に追加したレコードが表示されたんだろう?

高橋先生:DBグリッドに表示されているデータはデータベースを直接表示しているわけじゃないんだ。「ローカルバッファ」とか「ローカルキャッシュ」と呼ばれるデータベースのコピーのようなものを参照している。ローカルバッファはこれから追加するデータを格納する場所でもあるから、画面上には追加した後のレコードのように見えるんだ。重複したIDフィールドを持つレコードがローカルバッファに残っている限り、[保存(S)]ボタンをクリックするたびにエラーが表示されるよ。

ナッキー:ローカルバッファって場所に登録されている内容がDBグリッドに表示されていたのね。でもデータベースには間違ったレコードは登録しないようにしてくれているんだ。

高橋先生:エラーが起こるかどうか監視しておいて、エラーが起こったらデータベースは元の状態に戻すという機能が働いているんだ。

ナッキー:へぇ。うまくできなかった理由はよくわかりました。


    暗黙的なトランザクションと明示的なトランザクション

エラーが起こるかどうか見張っておいてくれるなんて安心。これもデータベースの機能の1つなのかな?

高橋先生:そうだね。レコードを追加したり、変更したり、削除してデータベースに変化が起こる処理を「更新」と呼ぶ。1つの更新処理を見張ってくれているんだ。失敗したら更新前の状態に戻してくれる。

ナッキー:あれ?住所の変更は設定できたみたいだけど、IDフィールドのダブっちゃった追加のレコードはなくなっていますね。半分は更新が成功しているから、画面は更新前の状態じゃありませんでしたよ。

高橋先生:今回の場合、処理グループの単位が1レコードずつなんだ。だから[保存(S)]ボタンは1度をクリックしたけれど処理グループは2つあったということだね。1つの処理グループを「トランザクション」というんだ。このような動作は、自動的に設定されるので「暗黙的なトランザクション」と呼ばれている。

ナッキー:トランザクションって、なんだかカッコいい名前ですね。住所を変更したトランザクションは成功して、追加したトランザクションは失敗だったんだ。

高橋先生:例えば、暗黙のトランザクションは1つのレコードに対して1つ用意される。だから一度に100件のレコードを変更したり追加したりした場合、100個のトランザクションが作られる。そのつどエラーがないかチェックするし、エラーがあった場合はその処理はデータベースには反映しない。

ナッキー:100個もあると、なんだか忙しそう。失敗したレコードがどれか、すぐにわかるんですか?

高橋先生:残念ながら、100個もあるとすぐにはわからないな。エラーのレコードがデータセットになって残るから、そこからたどることもできる。わかりにくい、というだけなら問題にはならないけれど、出金と入金など両方が成立しないと困る場合もあるよね。

例えば、Aという銀行からBという銀行にお金を振替える場合を説明しよう。Aから100のお金を出金するのは成功して、Bにお金を入金するときデータベースでエラーが発生してしまったとする。この場合、出した100のお金はAに戻されるべきだよね。でも1レコードずつ処理していたら、Aからは100が出金されて、Bには入金されないという状況になるんだ。

Hide image
06出金と入金の処理

図06 入金と出金の処理

ナッキー:それじゃあ、エラーを処理できても困っちゃうんですね。

高橋先生:だから、明示的に更新処理グループの範囲を指定するんだ。それが「明示的なトランザクション」というものだ。処理グループはここから始めます、とコードに記述するんだ。

ナッキー:自動で設定されたトランザクションは使わないんですか。

高橋先生:さっきの銀行の例だと、A銀行の出金からB銀行の入金までを1つのトランザクションとすればいいんだ。Aの出金で失敗しても、Bの入金で失敗しても、出金も入金もしていない元の状態に戻すことになる。

ナッキー:じゃあ、明示的なトランザクションで100個の更新を1つの処理グループとすると、そのうち1つでもエラーがあったら、100個のレコード全部が元の状態に戻っちゃうんですね。

高橋先生:そうすれば、やり直すときも全部をやり直せばいいから処理が難しくないよ。更新するレコードの件数がすごく多いとそうはいかないから、全体の処理時間との相談だね。アンケートプログラムは全部やり直しても、それほど多くはないと予想できる。エラーが起きたら、[保存(S)]ボタンをクリックするまでの更新を全部やり直すことにしよう。

ナッキー:やった!アンケートプログラムにもトランザクションを使おうっと。


    StartTransaction・Commit・Rollback

トランザクションはエラーがないかチェックする処理グループということまでは、わかってきました。今度はコードに記述する番ね。どのように書いたらいいのかな?

高橋先生:トランザクションのはじまりを示すには、TSQLConnectionコンポーネントの「StartTransaction」メソッドを使うよ。トランザクションを複数持つことができるので、それぞれを区別するために、どんなトランザクションかをパラメータにセットする。トランザクションの情報はTTransactionDesc型で持つことができるんだ。StartTransactionメソッドを処理する前にTTransactionDesc型の変数を用意して、プロパティをセットしてね。

ナッキー:なんだか難しい型がでてきましたね。トランザクションの情報を入れておくのね。

高橋先生:用意した変数にセットしてほしいプロパティは「TransactionID」プロパティと「IsolationLevel」プロパティ。TransactionIDプロパティは固有の番号を表す。今回は複数のトランザクションは使用しないから、適当に番号をつけていいけれど、複数ある場合は重複しない番号にしてね。IsolationLevelプロパティは、1つのテーブルに複数のトランザクションがあったとき、ほかのトランザクションのデータをどのように読み込むかを決めている。定数には「xilDIRTYREAD」「xilREADCOMMITTED」「xilREPEATABLEREAD」などがある。それぞれの詳細については、ヘルプのキーワード検索で「トランザクション,排他レベル」を参照してみよう。ここではよく使われる「xilREADCOMMITTED」を設定しておく。

パラメータ用の「td」変数の書き方はこんな感じ

var
  td : TTransactionDesc;
begin
  td.TransactionID := 1;
  td.IsolationLevel := xilREADCOMMITTED;
|
end;

次は、念のためStartTransactionメソッドを記述する前に、トランザクションがすでに始まっていないかチェックしてみよう。トランザクション中かどうかは、TSQLConnectionコンポーネントの「InTransaction」プロパティに入っている。「Ture」だったらトランザクションは始まっている。「False」のときだけStartTransactionメソッドを実行しよう。パラメータには、さっき用意したtd変数を使ってね。

if not SQLConnection1.InTransaction then
   SQLConnection1.StartTransaction(td);


さっそく、ここまで記述してみましょう。画面をコードエディタに変更します。次にbtnSaveClickイベントハンドラを探します。見つかったら「begin」の前にパラメータ用のtd変数を宣言します。処理は「sdsProfile.ApplyUpdates(-1);」の前に記述します。空行を挿入して、太字部分を追加します。

procedure TfrmProfileDB.btnSaveClick(Sender: TObject);
var
  td : TTransactionDesc;
begin
  td.TransactionID := 1;
  td.IsolationLevel := xilREADCOMMITTED;
  if not SQLConnection1.InTransaction then
     SQLConnection1.StartTransaction(td);
  sdsProfile.ApplyUpdates(-1);
end;

ナッキー:StartTransactionメソッドを実行した直後からトランザクションが始まるんですね。ひとまとまりの処理として認識されるんだ。あ!どこまでいったら終わりになるんですか?

高橋先生:トランザクションの終わりには2種類あるんだ。まず、更新が成功して、ローカルバッファの内容をデータベースに書き写して終わる場合。次に、更新の処理が失敗して、データベースをトランザクションが始まる前の状態に戻して終わる場合。成功したときは「Commit」メソッドを使う。失敗したら「Rollback」メソッドを使おう。どちらもパラメータはStartTransactionメソッドのときと同様、TransactionDesc型の変数を代入する。

ナッキー:成功したかどうかは、どうすればわかるんですか?

高橋先生:SQLなどを使って更新する場合は例外処理ブロックを使うのだけれど、アンケートプログラムでは「ApplyUpdates」メソッドを使って更新しているから例外処理ブロックは使えない。ApplyUpdatesメソッドの内部でエラーの処理をしてしまうから例外処理ブロックでは受け取れないんだ。エラーがあったとき、ApplyUpdatesメソッドにはエラーの個数が戻り値となる。だから戻り値が0より大きいときエラーがあった、とわかるんだよ。

ナッキー:じゃあ、if文を使えばわかるんですね。そういえば、戻り値は使わないといけないんじゃなかったっけ?今までは戻り値を受け取らなくても平気だったのね。

高橋先生:特殊な関数になっているものは戻り値を無視してもエラーにならないようになっているんだ。このApplyUpdatesメソッドもそうだよ。


では、コードの続きを記述してみます。btnSaveClickイベントハンドラでは、いったん「sdsProfile.ApplyUpdates(-1);」の行を削除します。戻り値が0のとき「Commit」メソッドを使います。そうでないとき「Rollback」メソッドを使います。太字部分を追加します。

procedure TfrmProfileDB.btnSaveClick(Sender: TObject);
var
  td : TTransactionDesc;
begin
  td.TransactionID := 1;
  td.IsolationLevel := xilREADCOMMITTED;
  if not SQLConnection1.InTransaction then
     SQLConnection1.StartTransaction(td);
  if sdsProfile.ApplyUpdates(-1) = 0 then
    SQLConnection1.Commit(td)
  else
    SQLConnection1.Rollback(td);
end;

ここまでできたら、保存して実行してみますね。ツールバーの[すべて保存]ボタンをクリックし、次に[実行]ボタンをクリックしてプログラムを実行します。まずは、レコードをどれか選択して名前フィールドだけ書き換えます。次に[追加(A)]ボタンをクリックして、最初に追加したレコードをもう一度入力してみます。住所フィールドまででかまいません。

ID

名前

ADDR_ID

住所

1

鈴木 太郎

6

東京


Hide image
07トランザクションを使った

図07 トランザクションを使った更新

[保存(S)]をクリックすると、エラーメッセージが表示されます。メッセージボックスは[継続(C)]ボタンで進めます。

    表示も更新する

エラーになる更新なので、Rollbackメソッドが働いて元に戻るはずよね。って、アレ画面上は何も変わらないみたい。教えて、高橋先生!

高橋先生:エラーではない更新はローカルバッファには残っていないから表示の更新をすればデータベースにある情報と同じようになる。このように異なる内容だったものを同じ内容にすることを「同期を取る」というんだ。更新するのはTSimpleDataSetコンポーネントの「Refresh」メソッドだ。だけどエラーの場合はレコードが、ローカルバッファにデータが残っているんだ。ローカルバッファにデータが残っていては表示の更新もできない。だから、まずローカルバッファにある更新をキャンセルしてから、Refreshメソッドでデータベースと同期を取ろう。

ナッキー:データベースのほうは、名前の変更もレコードの追加もRollbackメソッドで元に戻っているけれど、表示が元に戻っていないだけなんだぁ。もう1度実行したら元に戻ってるかな?よーしやってみよう。わぁ、元に戻ってる。再起動も「同期を取る」操作の1つなのね。じゃあ、同期を取る処理を追加する場所はどこにしましょうか?

高橋先生:Rollbackメソッドの次に記述すればいいよ。if文の中にコードを入れたいから、ブロック化しておいてね。

ナッキー:これで、Rollbackした状態をすぐにみることができますね。


実行中の場合は終了しておきます。表示をコードエディタにして、btnSaveClickイベントハンドラを探します。以下のように太字の部分を追加します。

procedure TfrmProfileDB.btnSaveClick(Sender: TObject);
var
  td : TTransactionDesc;
begin
  td.TransactionID := 1;
  td.IsolationLevel := xilREADCOMMITTED;
  if not SQLConnection1.InTransaction then
     SQLConnection1.StartTransaction(td);
  if sdsProfile.ApplyUpdates(-1) = 0 then
    SQLConnection1.Commit(td)
  else
  begin
    SQLConnection1.Rollback(td);
    sdsProfile.CancelUpdates;
    sdsProfile.Refresh;
  end;
end;

追加できたら、保存して実行します。ツールバーの[すべて保存]ボタンをクリックしたら、そのまま[実行]ボタンをクリックします。先ほどと同じように、レコードを1つ選択して名前を変更します。[追加(A)]ボタンをクリックして、IDフィールドを「1」にしたデータを追加します。入力できたら[保存(S)]ボタンをクリックします。エラーメッセージが表示されますので、[継続(C)]ボタンをクリックして処理を進めます。プログラムを起動しなおさなくても、画面は元の状態に戻ります。

    OnReconcileErrorイベント

ちゃんとトランザクションで[保存(S)]ボタンでの更新処理が、全部元に戻りました。でもレコードを登録するときに、何がいけなかったのかよくわからないわね。変更したレコードと追加したレコードのどっちが悪かったか、わかるといいのに。

高橋先生:エラーのとき起こる「OnReconcileError」イベントに処理を記述すればいいよ。これは、ApplyUpdatesメソッドなどでエラーがあったとき起こるイベントだ。エラーごとに起こるから、複数のエラーがあったとしても、それぞれでメッセージを出すことができるよ。

ナッキー:へぇ。でもif文の中で処理してもいいんじゃないですか?

高橋先生:イベントにはパラメータが入ってくる。OnReconcileErrorイベントにはたくさんのエラー情報がパラメータに入ってくるんだ。せっかくだから使ったほうがいいね。

まずはエラーのレコードの内容が入っている「DataSet」パラメータ。エラーがあったレコードがデータセットの形になって入っているよ。エラーは1レコードごとだから、データセットになっていても1レコード分しか入っていない。

ナッキー:パラメータに入ってくるから調べやすい!DataSetパラメータからレコードを取り出すのってコンポーネントを使うんですか?

高橋先生:あぁ、そうか。これまで項目コンポーネントを使って値を取り出していたんだよね。このままでは値が参照しにくいから、項目コンポーネントをつくろう。データセットから項目コンポーネント取り出す「FieldByName」というメソッドがあるんだ。パラメータはフィールド名。そして項目コンポーネントが取り出せたら、値を取り出す「AsInteger」プロパティや「AsString」プロパティなどを使ってフィールドの値を調べよう。型に合ったプロパティを使ってね。たとえば「ID」フィールドの値を調べるときは

i := Dataset.FieldByName('ID').AsInteger;

このようになる。

ナッキー:これで、エラーになったレコードがどれだかわかりますね。でも、そのレコードが追加だったか、変更だったか思い出せるかな?

高橋先生:どんな種類の更新だったかは「UpdateKind」パラメータに入っているよ。それぞれの定数は以下を参照。

編集:ukModify

追加:ukInsert

削除:ukDelete

では、これらを使って、それぞれ「編集エラー」などのメッセージを出すことにしよう。case文で分岐できるよ。そのあと、DataSetパラメータを使ってIDフィールドと名前をメッセージボックスに出せばいいね。


では、エラーメッセージを出すコードを記述します。表示をフォームデザイナに変更してsdsProfileコンポーネントを選択します。オブジェクトインスペクタ、イベントページの「OnReconcileError」を探してダブルクリックします。イベントハンドラが作成できたら、UpdateKindパラメータを使ったメッセージから作成します。case文を使うんだったわね。「case UpdateKind of」のあと[Enter]キーを押すと自動的に分岐の値が入力されます。

procedure TfrmProfileDB.sdsProfileReconcileError(DataSet: TCustomClientDataSet;
  E: EReconcileError; UpdateKind: TUpdateKind; var Action: TReconcileAction);
begin
  case UpdateKind of  //ここで[Enter]キー
    ukModify: ;
    ukInsert: ;
    ukDelete: ;
  end;
end;

続いてメッセージボックスを表示します。太字部分を記述します。

procedure TfrmProfileDB.sdsProfileReconcileError(DataSet: TCustomClientDataSet;
  E: EReconcileError; UpdateKind: TUpdateKind; var Action: TReconcileAction);
begin
  case UpdateKind of
    ukModify: ShowMessage('変更エラー');
    ukInsert: ShowMessage('追加エラー');
    ukDelete: ShowMessage('削除エラー');
  end;
end;

次に、レコードの内容をメッセージボックスに表示します。メッセージの内容は変数に代入しておきます。Messageの文字から数文字とって「msg」変数とします。値だけが表示されてもわかりにくいので「ID:」と「名前:」のように見出しをつけたほうが親切ですね。「ID:」のうしろと「名前:」の前後には空白があったほうが読みやすくなります。IDフィールドはInteger型なので型変換関数でstring型にします。太字部分を追加します。

unit FormProfileDB;

interface

(中略)

implementation

uses FormSelect;

{$R *.dfm}

procedure TfrmProfileDB.btnAddDataClick(Sender: TObject);
begin
  sdsProfile.Append;
end;

procedure TfrmProfileDB.btnDeleteClick(Sender: TObject);
begin
  if MessageDlg('削除してもいいですか?',mtConfirmation,
    [mbYes,mbNo],0) = mrYes  then
    sdsProfile.Delete;
end;

procedure TfrmProfileDB.btnNextClick(Sender: TObject);
begin
  sdsProfile.Next;
end;

procedure TfrmProfileDB.btnPriorClick(Sender: TObject);
begin
  sdsProfile.Prior;
end;

procedure TfrmProfileDB.btnSaveClick(Sender: TObject);
var
  td : TTransactionDesc;
begin
  td.TransactionID := 1;
  td.IsolationLevel := xilREADCOMMITTED;
  if not SQLConnection1.InTransaction then
    SQLConnection1.StartTransaction(td);
  if sdsProfile.ApplyUpdates(-1) = 0 then
    SQLConnection1.Commit(td)
  else
  begin
    SQLConnection1.Rollback(td);
    sdsProfile.CancelUpdates;
    sdsProfile.Refresh;
  end;
end;


procedure TfrmProfileDB.btnSelectClick(Sender: TObject);
var
  s : string;
begin
  s := '';
  frmSelect.ShowModal;
  if frmSelect.ModalResult = mrOk then
  begin
    if frmSelect.edtID.Text <> '' then
      s := '(ID = ' + frmSelect.edtID.Text + ')';
    if frmSelect.edtFullName.Text <> '' then
    begin
      if s <> '' then
        s := s + ' AND ';
      s :=s +  '(FULLNAME LIKE ''' + frmSelect.edtFullName.Text + '%'')';
    end;
    if frmSelect.cmbAddress.Text <> '' then
    begin
      if s <> '' then
        s := s + ' AND ';
      s := s + '(ADDRESS = ''' + frmSelect.cmbAddress.Text + ''')';
    end;
    if frmSelect.medtBirthday.Text <> '' then
    begin
      if s <> '' then
        s := s + ' AND ';
      s := s + '(BIRTHDAY >= ''' + frmSelect. medtBirthday.Text + ''')';
    end;
    if frmSelect.cmbMale.Text <> '' then
    begin
      if s <> '' then
        s := s + ' AND ';
      if frmSelect.cmbMale.Text = '男性' then
        s := s + '(MALE = TRUE)'
      else if frmSelect.cmbMale.Text = '女性' then
        s := s + '(MALE = FALSE)';
    end;
    if frmSelect.cmbPet.Text <> '' then
    begin
      if s <> '' then
        s := s + ' AND ';
      if frmSelect.cmbPet.Text = '飼っている' then
        s := s + '(PET = TRUE)'
     else if frmSelect.cmbPet.Text = '飼っていない' then
       s := s + '(PET = FALSE)';
    end;
    sdsProfile.Active := False;
    sdsProfile.DataSet.CommandText :=
     'SELECT * FROM PROFILE WHERE ' + s;
    sdsProfile.Active := True;
  end;

end;

procedure TfrmProfileDB.cmbSortChange(Sender: TObject);
begin
  sdsProfile.Active := False;
  sdsProfile.DataSet.CommandText :=
    'SELECT * FROM PROFILE ORDER BY ' + cmbSort.Text;
  sdsProfile.Active := True;
end;

procedure TfrmProfileDB.sdsProfileNewRecord(DataSet: TDataSet);
begin
  sdsProfileMALE.Value := True;
  sdsProfilePET.Value := False;
end;

procedure TfrmProfileDB.sdsProfileReconcileError(DataSet: TCustomClientDataSet;
  E: EReconcileError; UpdateKind: TUpdateKind; var Action: TReconcileAction);
var
  msg : string;
begin
  case UpdateKind of
    ukModify: ShowMessage('変更エラー');
    ukInsert: ShowMessage('追加エラー');
    ukDelete: ShowMessage('削除エラー');
  end;
  msg := 'ID: ' +
    IntToStr(DataSet.FieldByName('ID').AsInteger) +
    ' 名前: ' +
    DataSet.FieldByName('FULLNAME').AsString;
  ShowMessage(msg);
end;

procedure TfrmProfileDB.btnCancelClick(Sender: TObject);
begin
  sdsProfile.Cancel;
end;


end.

記述できたら、保存して実行します。ツールバーの[すべて保存]ボタンをクリックしたら、そのまま[実行]ボタンをクリックします。先ほどと同じように、レコードを1つ選択して名前を変更します。もう1つ異なるレコードを選択して、IDフィールドを「1」に変更します。さらに[追加(A)]ボタンをクリックして、IDフィールドを「1」にしたデータを追加します。

ID

名前

ADDR_ID

住所

どれか(今回はIDが7のレコード)

名前を変更

どれかを1に変更(今回はIDが6のレコード)

1

鈴木 太郎

6

東京


これで、2つのエラーが発生します。入力できたら[保存(S)]ボタンをクリックします。エラーメッセージが表示されますので、[継続(C)]ボタンをクリックして処理を進めます。そうすると「変更エラー」のメッセージボックスと、IDフィールドを「1」に変更したレコード情報の入ったメッセージボックスが表示されます。次に「追加エラー」のメッセージボックスと「ID: 1名前: 鈴木」のメッセージボックスが表示されて、画面は元の状態に戻ります。エラーの起こらない編集や追加も試してみましょう。

Hide image
08実行テスト

図08 実行テスト

Hide image
09エラーメッセージ

図09 エラーメッセージ

ナッキー:トランザクションの、処理を1まとまりのグループにして考えるのが、ちょっと難しかったです。でもいろいろ試してみると、なんとなくつかめた感じがします。

高橋先生:なんとなくでも、十分だよ。今回は深刻な問題が起きないから、必然性が薄くて余計にわかりにくいかもしれない。だけど入金出金処理や、在庫管理などテーブルは分かれているけれど密接にかかわっている場合は、トランザクションを使った処理が必要なんだ。

ナッキー:でも、はっきりわかったことがあります。

高橋先生:へぇすごいじゃない。どんなこと?

ナッキー:データベースは奥が深い。

高橋先生:ははは。確かにそうだね。少しはデータベースに触れられたっていう実感が持てればいいんじゃないかな。じっくり復習してみてね。

ナッキー:はーい。


ナッキーの「Turbo Delphiはじめて奮戦記」

Prev | Next | Index


Server Response from: ETNASC04