2020年6月23日火曜日

Oracle APEXにおける楽観的並行性制御の実装について

Oracle APEXでは、データベースに保存されているデータを更新するための標準コンポーネントとして、対話グリッドフォームが提供されています。これらの標準コンポーネントに組み込まれている楽観的並行性制御(Optimistic Concurrency Control)について説明します。

難しい話に聞こえるかもしれませんが、この機能はデフォルトで有効になっています。そのため、Oracle APEXのアプリケーションでは、楽観的並行性制御を意識して実装する必要はありません。

今回はフォームを例にとって説明します。対話グリッドにも同様の設定があります。正確には対話グリッドに実装されていた機能が、その後に開発されたフォーム・リージョンにも含まれました。

フォームまたは対話グリッドの操作を実行するプロセス、行の自動処理(DML)設定にある失われた更新の防止ONにすることで楽観的並行性制御が有効になります。先ほども説明しましたが、これはデフォルトでONになっています。


アプリケーションを作って動作を確認してみます。SQLワークショップSQLコマンドから以下のDDLを実行して、表TEST_EMPを作成します。
create table test_emp (
id number generated by default on null as identity
constraint test_emp_id_pk primary key,
ename varchar2(80),
job varchar2(40),
sal number
);
表を作成した後、オブジェクト・ブラウザから作成した表TEST_EMPを選択し、アプリケーションの作成を実行します。


アプリケーションの作成画面では、任意の名前を設定し、それ以外はウィザードに任せて、アプリケーションの作成を実行します。今回の確認で使用するのは、Empレポート(フォーム付き対話モード・レポート)のみです。


アプリケーションが作成されますが、実行する前に少々調整をします。最初に、アプリケーション・プロパティの編集を開いて、アプリケーションの別名を変更します。次に、共有コンポーネント認証スキームを開いて、認証スキーム公開資格証明にします。


アプリケーションの別名は英数字と一部の記号に限定します。


認証スキームは検証作業を楽にするために、ユーザー名のみで認証できる公開資格証明をカレント・スキームにします。


対話モード・レポートから開かれるDMLフォームをページ・デザイナで開き、失われた更新の防止の設定を確認します。


左ペインにてプロセス・ビューを表示させ、フォームのプロセスを選択します。失われた更新の防止ONになっています。


準備は以上で完了です。これからアプリケーションを実行し、動作の確認をしていきます。

最初にアプリケーションを実行し、ユーザーaliceとしてログインします。


Empレポートを開きます。


作成をクリックして、確認に使用するデータを登録します。


EnameCarolJob営業Sal(Salary)を1000として、一行登録します。


AliceとBobがそれぞれ、CarolのSal+100することを依頼されたとします。それぞれ+100ですので、両人が処理を実行した結果は1200になることが想定されます。

AliceのアカウントでCarolのデータの編集画面を開きます。Sal1100に変更しますが、まだ、変更の適用は行いません。


別のブラウザで同じアプリケーションにBobのアカウントでアクセスします。こちらは、1000
と表示されているSal1100に変更し、変更の適用を実行します。


変更は適用されて、Salは1100として保存されます。


Aliceの画面に戻って、変更の適用をクリックします。データベースから行を読み出した後に、保存されている行が変更されたため、エラーが発生して変更の適用が出来ません。エラー・メッセージは「ユーザーが更新処理を開始してから、データベース内の現行バージョンのデータが変更されています。」です。


一旦このフォームを取消をクリックして閉じ、再度、更新処理をやり直す必要があります。次にフォームを開いた時は、Salは1100になるため、+100して1200に更新することになり、想定した結果になります。

失われた更新の防止をOFFにして同様の操作を行うと、エラーにならずにSalが保存されることが確認できますので、試してみてください。これは大抵の場合で問題の起こる動作ですので、この設定をOFFにすることはないでしょう。Webアプリケーションを一から作成する場合は、このようなコーディングを自分で行う必要があります。

失われた更新の防止の仕組みですが、2種類の方法から選ぶことができます。フォーム・リージョンの属性に、その切り替えがあります。失われた更新タイプがその設定です。


それぞれについて、ヘルプでは以下のように説明されています。

行の値
データを最初に問い合せる場合、チェックサム値が各行に計算されます。 チェックサムは、すべての更新可能な列を文字列に連結し、一意の値を生成して計算されます。 更新されたレコードをコミットすると、このチェックサムが現在のデータベース・レコードのチェックサム値と比較されます。 同じでない場合、エラーが発生します。
行バージョン列
データベース表にデータベース・トリガー(可能な場合)によってレコードが更新されるたびに増える列が含まれている場合、チェックサムを計算するかわりにこの列を使用できます。 対話グリッドで複数の表のデータを更新する場合、このオプションを使用しないでください。

注意 - 行バージョン列をリージョンのSQLソースに含める必要があります。
失われた更新タイプ行の値のときは、 データベースからそれぞれの列の情報を取り出してフォームに表示する際に、それらのすべての列を連結して生成したチェックサムも同時にフォームに含めます。このチェックサムは画面には表示されませんが、フォームがサブミットされる際にページ・アイテムと一緒にサーバーへ送信されます。サブミットされたフォームを処理するプロセスは、データを更新する前に変更の対象となっている行をデータベースから読み出し、チェックサムを計算します。受信したチェックサムと一致している場合(つまり行が変更されていない)のみ、受け取ったページ・アイテムの情報で既存の行を更新します。

行バージョン列はちょっと馴染みがないかもしれません。クイックSQLを使って表のDDLを生成するときに指定可能なオプションに行バージョン番号というものがあります。行バージョン列というのはそれを指します。

SQLワークショップユーティリティからクイックSQLを実行します。左の画面には以下の定義を指定します。
test_emp
  ename vc80
  job       vc40
  sal        number
設定を開いて、追加列行バージョン番号にチェックを入れ、変更の保存を行います。


結果として以下のようなDDLが生成されます。

-- create tables
create table test_emp (
    id                             number generated by default on null as identity  
                                   constraint test_emp_id_pk primary key,
    row_version                    integer not null,
    ename                          varchar2(80),
    job                            varchar2(40),
    sal                            number
)
;

-- triggers
create or replace trigger test_emp_biu
    before insert or update 
    on test_emp
    for each row
begin
    if inserting then
        :new.row_version := 1;
    elsif updating then
        :new.row_version := nvl(:old.row_version,0) + 1;
    end if;
end test_emp_biu;
/
追加されたrow_version列はトリガーによってアップデートが実行されるたびに、1づつ数値がインクリメントされます。そのためチェックサムの代わりにrow_versionを読み出し、データの更新時にはrow_versionを比較することで行の変更を検知することができます。

Oracle APEXのアプリケーションではチェックサムを生成するコードを記述する必要はないため、デフォルトの設定である行の値を使用するケースがほとんどでしょう。行バージョン列はチェックサムより実装が容易なので、Oracle APEX以外の処理がある場合は採用を検討することになるかと思います。例えば、Oracle REST Data Servicesを使ったRESTサービスによる更新などです。

最後に行のロックについて説明します。失われた更新の防止の下にあり、ヘルプの記載だけではその意味が非常に掴みにくい設定です。


先ほど、チェックサムの一致を確認する手順を記述しました。フォームの送信に含まれるチェックサムをプロセスが受け取ると、
  1. 更新対象の行を読み出して、チェックサムを計算する。
  2. 受信したチェックサムと計算したチェックサムを比較する。
  3. 一致していれば、受信したページ・アイテムで行を更新する。
という順番で処理が行われます。行のロックYesである場合、1の処理でチェックサムを計算するためにデータベースからデータを取り出す際に"SELECT FOR UPDATE"を実行し、行を排他ロックします。非常に短い瞬間ですが、チェックサムを計算してから実際に行のアップデートを行う間に行が更新されないように保護しています。ですので、できる限りYesにします。

フォームのソースが単純な表やSQLではなく、SELECTは実行できるがFOR UPDATEをつけるとエラーが発生する(例えばソースとなるSQLが複数の表をJOINしている)場合、またはデータ・ソースがリモート・データベースなどの場合にNoを設定します。Noにする弊害が明確な場合は、PL/SQL Codeを選択し、排他制御を行うためのコードを記述することができます。

Oracle APEXの楽観的並行性制御の実装についての説明は以上です。Oracle APEXの良いところは、Oracle APEXでアプリケーションを作成すると、意識しなくても、今まで説明してきた機能が実装されている、というところでしょう。