ここではCachedUpdatesについて説明します。

Delphi Professional 以上のユーザであれば、BDE(Borland Database
Engine)で「キャッシュアップデート」という機能が使えるということを聞いたことがあるはずです。しかし、Delphiのマニュアルの説明不足のせいで、よく知らないために使わないでいる方も多いと思います。

通常は、データセット上のレコードを更新して、Postメソッドで更新を確定させると、その場でOracleサーバに対しUPDATE/INSERT/DELETE文が発行されますが、

TOracleDataSetがCachedUpdatesモードに設定されている場合は、Postメソッドによる更新の確定は、クライアントアプリケーションの内部に設けられたローカルの更新キャッシュに対して行われ、SQLの発行が(後回しに)遅延されます。

実際にOracleサーバ上のデータを更新するには、TOracleDataSetのApplyUpdatesメソッドを呼び出します。このとき、ローカルの更新キャッシュに蓄積されていた変更が、一括してOracleサーバに受け渡されます。

  • TOracleDataSet

    • property
      CachedUpdates:
      boolean:
      Trueにセットすると、データセットの更新をローカルにキャッシュします。
  • TOracleSession * procedure
    ApplyUpdates(const
    DataSets: array of TOracleDataSet;
    Commit: Boolean) :

    • データセットのローカル更新キャッシュに行われた更新内容を、SQLサーバに反映させます
    • master/detail
      関係が定義されている場合、masterデータセットについてApplyUpdatesを行うことで、detailデータセットの更新キャッシュも反映されます
    • さらに、Commit=Trueの場合は、トランザクションをコミットします
    • procedure
      CommitUpdates(DataSets): ApplyUpdates(DataSets, True)と同じです
    • procedure
      CancelUpdates(const DataSets: array of TOracleDataSet):

      • データセットのローカル更新キャッシュをクリアします
      • 変更されていたレコードは元に戻り、挿入されていたレコードは消え、削除されていたレコードは復活します

CachedUpdatesのメリット

  • サーバに負荷をかけないトランザクション制御
    • CachedUpdatesモードでは、CancelUpdatesによって変更を取り消すことが可能です。そのため、ApplyUpdates/CancelUpdatesを使ってトランザクション制御が実現できます。しかも、Commit/Rollbackと異なり、CachedUpdates/CancelUpdatesは、SQLサーバの資源を全く消費しない点ですぐれています
  • バッチ:パフォーマンスの向上
    • 一度に多数のレコードを更新する場合には、クライアントアプリケーションと、Oracleサーバの間の通信の発生回数を抑えることで、通信のオーバヘッドにかかる時間を節約することができます
  • オンライン:トランザクション継続時間の短縮
    • 一般に、「長い」トランザクションによって、長時間にわたりレコードをロックすることは、レコードロックの競合を起こりやすくするため、デッドロックや、応答時間低下の原因となり得ます。CachedUpdatesを使うと、トランザクションの開始がApplyUpdatesが行われるまで遅延されるため、「長い」トランザクションが発生しにくくなります

CachedUpdatesのデメリット

  • バッチ:ローカルシステム資源の消費
    • ローカルに更新キャッシュを保持するためのメモリが必要なため、あまりに大量のレコードを一度に更新すると、メモリが不足する恐れがあります。一般的に言って、CachedUpdatesでキャッシュするレコード件数が、数十万件になるようなら、途中で更新を分割してApplyUpdatesするべきでしょう
  • オンライン:最新レコードの反映遅れ
    • CachedUpdatesの性質上、どこかの端末で更新した最新データは、すぐにOracleのデータベースに反映されず、しばらくはその端末のローカルキャッシュに留まります。したがって、不特定多数のユーザによって更新され、つねに最新データの参照が必要であるようなデータを扱う場合には、CachedUpdatesによって更新を長時間キャッシュするのは避けるべきです。

CachedUpdatesの使用例: master/detail関係にあるテーブルの挿入

例えば、受注(Order)テーブルと、受注詳細(OrderDetail)テーブルという二つのテーブルに、注文書の内容を挿入するとします

  1. 注文番号を新たに採番する
  2. 受注テーブルに1レコードを挿入し、(注文番号、顧客番号と、注文日付)をセット
  3. 受注詳細テーブルに、受注した各製品ごとにレコードを挿入し(注文番号、注文書行番号、製品番号、受注数量)をセット

というシナリオを処理するオンラインアプリケーションを考えてみて下さい。注文詳細テーブルを1レコード更新するごとに、Oracleに更新を反映する必要は全くありません。データの更新は、注文書作成が完了した時に1回でいいはずです。

  • 二つのテーブルそれぞれに挿入するデータを編集するために二つのデータセットを用意します
  //OrderQuery.SQL =       'SELECT Order.ROWID, Order.* FROM Order WHERE 0=1'
  //OrderDetailQuery.SQL = 'SELECT OrderDetail.ROWID, OrderDetail.* FROM OrderDetail WHERE 0=1'

注文入力フォームを開いたらまず上記の例のようにWHERE条件をFalseのままにOrderQueryを開き、即座にAppendして、空のレコードを作ります。これをデータベース対応コンポーネントでエディットするとよいでしょう。

顧客番号を入力するEditBox(実際の業務の場合は50音順検索やリストからの選択機能が必要でしょう)があり、DetailQueryはDBGridコンポーネントに関連付けられていて、製品番号と受注数量の一覧編集を行うことでしょう。

…と、前置きが長くなりましたが、「注文入力」ボタンのコードは例えばこんな感じです。

  //OrderQuery.CachedUpdates = True
  //OrderDetailQuery.CachedUpdates = True
  procedure SubmitButtonClick(Sender: TObject);
  var
    NewOrderNumber: integer;
    i: integer;
  begin
    //まず、注文番号を採番
    SequenceQuery.SQL = 'SELECT OrderNumSeq.NextVal NewOrderNumber FROM DUAL';
    SequenceQuery.Open;
    NewOrderNumber := SequenceQuery.Field('NewOrderNumber').AsInteger;

    //受注テーブルにキー項目をセット
    with OrderQuery do begin
      Edit;
      Field('OrderNumber').AsInteger := NewOrderNumber;
      Post;
    end;

    //受注詳細テーブルのキー項目をセット
    with OrderDetailQuery do begin
      i := 0;
      First;
      while not Eof do begin
        Edit;
        Field('OrderNumber').AsInteger := NewOrderNumber;
        Field('OrderDetailNumber').AsInteger := i;
        Post;
        Next;
        Inc(i);
      end;
    end;
    //ここまでの更新は、ローカルにキャッシュされる

    //ここでまとめて更新をサーバに反映する
    OracleSession.ApplyUpdates([OrderQuery, OrderDetailQuery], True);
    //(...以下省略...たとえばフォームを閉じたり、クリアしたり…)
  end;