ラベル JSON の投稿を表示しています。 すべての投稿を表示
ラベル JSON の投稿を表示しています。 すべての投稿を表示

2025年11月20日木曜日

JSON Regionプラグインを使用して東京都こどもDX子育て支援制度レジストリのJSONを編集する

Oracle AI Database 26aiには、JSONをそのままJSONとして保存できるJSONコレクション表というオブジェクトがあります。このJSONコレクション表に東京都のオープンデータ「東京都こどもDX_子育て支援制度レジストリ」を保存し、簡単なレポートとフォームのページを作成してみます。

保存先がリレーショナル表の場合は、APEXの標準コンポーネントを使うことができます。今回はJSON形式のデータをJSONコレクション表に保存し、JSONのまま操作します。JSONのまま操作するために、Uwe Simonさんが作成されたJSON Regionプラグインを使用します。

Oracle APEXでは共有コンポーネントとしてJSONソースがあります。JSONソースを作成することにより、JSONコレクション表からAPEXのデータ・ソースを定義できます。ただし、JSONソースはデータ・プロファイル(JSONの属性を列として見せる定義)が静的に定義されています。JSONデータの属性の追加や削除に対応するには、データ・プロファイルを更新する必要があります。この作業はアプリケーションの開発者が実施し、APEXアプリケーションの更新を伴います。

リージョン・プラグインのJSON Regionは、JSONデータとJSONスキーマの定義から、動的にフォームを生成します。フォームに表示するアイテムやレイアウトは、JSONスキーマによって決まります。これは、アプリケーションに静的に記述したり、JSONデータから動的に生成したり、SELECT文を実行してデータベースから取り出したり、色々な方法で決めることができます。フォームのアイテムやレイアウトの自由度には制限がありますが、表示するデータに依存したフォームの生成が可能です。

作成するAPEXアプリケーションは以下のように動作します。アプリケーションの動作や見かけは、データソースがリレーショナル表のときと違いはありません。


以下よりAPEXアプリケーションの作成手順を紹介します。

最初に東京都の子育て支援制度をJSONのまま保存する、JSONコレクション表TOKYO_KOSODATESHIENSEIDOを作成します。

create json collection table tokyo_kosodateshienseido with etag;

APEXのSQLコマンドから実行します。


東京都オープンデータカタログサイトの東京都こどもDX_子育て支援制度レジストリのページを開き、JSON形式のデータのURLを確認します。本記事では2025年8月20日時点のデータを参照しています。


SQLコマンドで以下のコードを実行し、JSONデータのダウンロードとJSONコレクション表への投入を実施します。

実行結果には1行が挿入されました。と表示されています。PL/SQLブロックを実行したときは、このように表示されます。表に挿入された行数ではありません。


データの準備ができたので、APEXアプリケーションの作成に移ります。

空のAPEXアプリケーションを作成します。名前東京都子育て支援制度とします。


APEXアプリケーションが作成されます。JSON Regionプラグインの導入から始めます。


APEX Worldのサイトを開き、Plug-insのタブを選択します。Plug-in typeRegionを選択し、Search KeywordJSONを入れてプラグインを検索します。

JSON-Region 0.9.8.0(バージョンは変わるかもしれません)が見つかるので、それを開きます。


Downloadをクリックし、プラグインやサンプル・アプリケーションを含むZIPファイルをダウンロードします。


2025年11月20日時点では、simonuwe-oracle-apex-json-region-rel-0.9.8.0-0-g67958cf.zipというファイル名で、ファイルがダウンロードされました。

このファイルを解凍しておきます。

APEXに戻り、共有コンポーネントプラグインを開きます。


インポートをクリックします。


インポートするファイルとして、ダウンロードしたZIPファイルに含まれているplug-in/region_type_plugin_json_region_uwesimon_selfhost_e.sqlを選択します。ファイル・タイププラグインです。

へ進みます。


ファイルのインポートが完了しました。へ進みます。


プラグインをインストールします。


コンポーネント設定SQL-Query for referenced JSON-schemaのデフォルト値の設定を求められます。必須ではなく、後からでも変更できるので空白のままとします。

変更の適用をクリックします。


コンポーネント設定のページに移ります。

すでにJSON-Regionプラグインは作成されています。


JSONコレクション表TOKYO_KOSODATESHIENSEIDOをソースとした、対話モード・レポートとフォームのページを作成します。

ページの作成をクリックします。


対話モード・レポートを選択します。


レポート定義名前支援制度一覧フォーム・ページを含めるオンにし、フォーム・ページ名として支援制度を設定します。ページ番号はそれぞれ、とします。

データ・ソースソース・タイプSQL問合せを選択し、SQL SELECT文を入力に以下を記述します。

select json_value(data,'$._id') "_id", data from tokyo_kosodateshienseido

へ進みます。


主キー列_id (Varchar2)を選択します。MongoDBと互換性を持つためだと思われますが、JSONコレクション表は属性"_id"として、一意キーが設定されています。

ページの作成をクリックします。


対話モード・レポートとフォームのページが作成されます。


APEXアプリケーションを実行します。

対話モード・レポートにはJSONがそのまま表示されています。任意の行の編集アイコンをクリックし、ドロワーを開きます。


ドロワーが開いたら、JSONは変更せずに、変更の適用をクリックします。


以下のエラーが発生します。これは、更新するJSONに"_id"属性が含まれていることが原因です。

ORA-54059: 不変列("WKSP_APEXDEV"."TOKYO_KOSODATESHIENSEIDO"."RESID")は別の値に更新できません


JSONコレクション表を操作するプロセス・フォーム支援制度の前に、プロセスRemove _idを作成します。タイプコードを実行です。

ソースPL/SQLコードに以下を記述し、受信したデータより"_id"属性を削除します。

select json_transform(:P3_DATA, remove '$._id') into :P3_DATA;

サーバー側の条件ボタン押下時SAVEを指定します。


プロセスRemove _idを追加することで先ほどのエラーは発生しなくなり、フォームでJSONデータを更新できるようになります。

これから、ページ・アイテムP3_DATAに含まれるJSONデータからフォームを生成します。

ページ・アイテムP3_DATAの下にリージョンを作成します。

識別名前JSON Regionタイプに先ほど導入したプラグインであるJson-Regionを選択します。


Json-Regionの属性を開きます。

設定JSON-itemにJSONデータが含まれるページ・アイテムP3_DATAを選択します。JSONスキーマのSourceとして、Generate Schemaを選択します。この設定では、JSONスキーマはP3_DATAに保存されているデータから生成されます。

その他は用途によって変更します。以下ではHide JSON-itemオンにしているので、JSONデータを保持しているP3_DATA非表示に変わります。


先ほどと同様の手順でフォームを開くと、JSONデータの代わりに自動的に生成されたフォームが開きます。


このままでは見辛いので、JSONスキーマを定義して表示されるページ・アイテムを限定します。

設定SourceStaticに変更し、Static Schemaに以下を記述します。

Column Width12を指定します。Universal Themeは横幅を12分割して配置を決めています。列幅に12を設定すると、1列すべてを1つのページ・アイテムが占有します。つまり、1列に1つだけページ・アイテムが配置されます。


先ほどと同様の手順でフォームを開きます。今度はJSONスキーマの定義に従ったフォームが開かれます。


編集も可能です。変更の適用をクリックします。


フォームのデータに一切変更を加えていないくても、JSON Regionの設定にあるKeep additional attributesRemove NULLS from JSONといった設定により、属性が削除されることがあります。また、JSON Regionが更新するのはページ・アイテムP3_DATAに読み込まれたJSONデータであり、データベースはP3_DATAの値で更新されます。JSON_TRANSFORMを呼び出して更新する場合と異なり、JSONデータは全体が更新されます。


本記事のテーマであるJSON Regionの組み込みは以上で完了です。

対話モード・レポートの表示がJSONそのままではいけないので、属性ごとに列として表示されるように変更します。

JSONコレクション表からJSONデータガイドを生成し、それを元にビューを作成します。

SQLコマンドで以下のスクリプトを実行し、ビューTOKYO_KOSODATESHIENSEIDO_Vを作成します。



対話モード・レポートのソースSQL問い合わせを、作成したビューTOKYO_KOSODATESHIENSEIDO_Vを参照するように変更します。

select * from tokyo_kosodateshienseido_v


対話モード・レポートで新しく認識された列は、デフォルトでは表示されません。

アクション・メニューを開き、新しく認識された列をレポートに表示します。


選択した列が対話モード・レポートに表示されます。これでJSONの属性がそれぞれ列として表示されるようになりました。

編集アイコンをクリックすると、ORA-1403が発生します。


これは対話モード・レポートでは主キーの値(属性"_id"の値)が大文字なのに対して(JSONデータガイドを元に作成したビューが列"_id"を大文字で返す)、フォームでは小文字で扱うためです。

フォームが"_id”の値を小文字で受け取るように、プロセス初期化フォーム支援制度の前にP3__IDの値を小文字に変換するプロセスを作成します。

識別名前Lower P3__IDとします。ソースPL/SQLコードに以下を記述します。

:P3__ID := lower(:P3__ID);

サーバー側の条件タイプアイテムはNULLではないを選択し、アイテムとしてP3__IDを設定します。


以上でAPEXアプリケーションは完成です。アプリケーションを実行すると、記事の先頭のGIF動画のように動作します。

今回作成したAPEXアプリケーションのエクスポートを以下に置きました。
https://github.com/ujnak/apexapps/blob/master/exports/sample-json-region-plugin.zip

JSON RegionのプラグインとしてダウンローしたZIPには、プラグインの他にもサンプル・プログラムなどが含まれています。便利なプラグインなので、使い方を学ぶことをお勧めします。

今回の記事は以上になります。

Oracle APEXのアプリケーション作成の参考になれば幸いです。

2025年2月28日金曜日

APEXの楽観的同時実行制御とJSON二面性ビューおよびJSONコレクション表のETAGの扱いについて

Oracle APEXは自前の楽観的同時実行制御(Optimistic Concurrency Control)の仕組みを持っています。JSON二面性ビューおよびJSONコレクション表はそれぞれの行、つまりJSONオブジェクトにメタデータとしてETAGを含みます。そのため、APEXのデータ・ソースとしてJSON二面性ビューまたはJSONコレクション表を指定すると、同時実行制御が二重になる可能性があります。

JSON二面性ビューでは、更新するJSONオブジェクトのETAGと表に保存されているJSONオブジェクトのETAGが一致しているときに更新が実行されます。一致していないと、ORA-42699が発生します。

JSONコレクション表はJSON二面性ビューとは異なり、JSONオブジェクトにETAGが含まれますが、楽観的同時実行制御は実装されていません。開発者がETAGを使って、同時実行制御を実装する必要があります。

Oracle APEXのデータ・ソースとして二面性ビューおよびJSONコレクション表を指定したときに、同時実行制御がどのように動作するか確認してみます。

最初にJSON二面性ビューEMPDEPT_DVを作成します。サンプル・データセットEMP/DEPTに含まれる表EMPとDEPTを元にしています。



作成された二面性ビューEMPDEPT_DVを検索し、内容を確認します。

select data from empdept_dv;

すべてのオブジェクトに"_metadata": { "etag": "....", "asof": "...." }が含まれています。


JSONコレクション表EMPDEPT_Jを作成します。WITH ETAGを指定します。

create json collection table empdept_j with etag;


二面性ビューEMPDEPT_DVの内容をJSONコレクション表EMPDEPT_Jにコピーします。二面性ビューEMPDEPT_DVに含まれるメタデータ_metadataは、コピーするJSONオブジェクトから除きます。

insert into empdept_j select json_transform(d.data, remove '$._metadata') data from empdept_dv d;


JSONコレクション表EMPDEPT_Jを検索し、内容を確認します。

select data from empdept_j;

JSONコレクション表EMPDEPT_Jを作成する際にWITH ETAGを指定しているため、それぞれのJSONオブジェクトにETAGが含まれています。JSONコレクション表のETAGは行のチェックサムではなく行バージョンが生成されているため、異なる行で同じETAGが割り当たることもあります。JSONコレクション表には属性asofはありません。


以上でデータベース側の準備ができました。


コマンドラインでの楽観的同時実行制御の確認



コマンドラインから、同時実行制御の動作を確認します。JSON二面性ビュー、JSONコレクション表の両方とも検索対象の列はDATAのみなのでheadingoff、楽観的同時実行制御の確認なのでautocommitonにします。出力に改行が含まれるとうまくコピペができないので、pages、lines、trims、trimoなどのオプションも指定します。

SQL> set heading off

SQL> set autocommit on

SQL> set pages 1000 lines 1000 trims on trimo on



JSON二面性ビュー


従業員番号7788のSCOTTのデータを取り出します。

SQL> select d.data from empdept_dv d where d.data."_id"."empno" = 7788;


{"_id":{"empno":7788},"ename":"SCOTT","job":"ANALYST","sal":3000,"mgr":7566,"comm":null,"deptno":20,"dept":{"deptno":20,"dname":"RESEARCH","loc":"DALLAS"},"_metadata":{"etag":"F8C263E0EF750556C959750077B31838","asof":"000000000129DECB"}}


SQL> 


給与を変更したJSONドキュメントに置き換えます。この例では3000を4000に変更しています。データベースに保存されているETAGと更新するJSONドキュメントのETAGが同じ値なので、更新は成功します。

SQL> update empdept_dv d set d.data = '{"_id":{"empno":7788},"ename":"SCOTT","job":"ANALYST","sal":4000,"mgr":7566,"comm":null,"deptno":20,"dept":{"deptno":20,"dname":"RESEARCH","loc":"DALLAS"},"_metadata":{"etag":"F8C263E0EF750556C959750077B31838","asof":"000000000129DECB"}}' where d.data."_id"."empno" = 7788;


1行更新しました。


コミットが完了しました。

SQL> 


更新された内容を確認します。_metadataのetagが異なる値に変わっています。

SQL> select d.data from empdept_dv d where d.data."_id"."empno" = 7788;


{"_id":{"empno":7788},"ename":"SCOTT","job":"ANALYST","sal":4000,"mgr":7566,"comm":null,"deptno":20,"dept":{"deptno":20,"dname":"RESEARCH","loc":"DALLAS"},"_metadata":{"etag":"6907379BED6ABD00E3381F8A7E6C5068","asof":"000000000129DF3A"}}


SQL> 


再度、更新された内容を確認します。データに変更は無いため_metadataのetagの値は同じですが、asofは検索される度に変わります。

SQL> select d.data from empdept_dv d where d.data."_id"."empno" = 7788;


{"_id":{"empno":7788},"ename":"SCOTT","job":"ANALYST","sal":4000,"mgr":7566,"comm":null,"deptno":20,"dept":{"deptno":20,"dname":"RESEARCH","loc":"DALLAS"},"_metadata":{"etag":"6907379BED6ABD00E3381F8A7E6C5068","asof":"000000000129DFE9"}}


SQL> 


最初に検索したJSONオブジェクトで、SCOTTのデータを置き換えます。dataに渡しているETAGと現状のETAGが一致していないため、ORA-42699が発生します。

SQL> update empdept_dv d set d.data = '{"_id":{"empno":7788},"ename":"SCOTT","job":"ANALYST","sal":3000,"mgr":7566,"comm":null,"deptno":20,"dept":{"deptno":20,"dname":"RESEARCH","loc":"DALLAS"},"_metadata":{"etag":"F8C263E0EF750556C959750077B31838","asof":"000000000129DE86"}}' where d.data."_id"."empno" = 7788;


次のコマンドの開始中にエラーが発生しました : 行 1 -

update empdept_dv d set d.data = '{"_id":{"empno":7788},"ename":"SCOTT","job":"ANALYST","sal":3000,"mgr":7566,"comm":null,"deptno":20,"dept":{"deptno":20,"dname":"RESEARCH","loc":"DALLAS"},"_metadata":{"etag":"F8C263E0EF750556C959750077B31838","asof":"000000000129DE86"}}' where d.data."_id"."empno" = 7788

エラー・レポート -

ORA-42699: updateを実行できません(JSONリレーショナル二面性ビュー'EMPDEPT_DV'): データベースのID 'FB03C24E5900'のドキュメントのETAGが、渡されたETAGと一致しませんでした。


https://docs.oracle.com/error-help/db/ora-42699/


More Details :

https://docs.oracle.com/error-help/db/ora-42699/

SQL> 


更新するJSONオブジェクトに_metadata(etag含む)が含まれていないと、データが更新されます。

SQL> update empdept_dv d set d.data = '{"_id":{"empno":7788},"ename":"SCOTT","job":"ANALYST","sal":3000,"mgr":7566,"comm":null,"deptno":20,"dept":{"deptno":20,"dname":"RESEARCH","loc":"DALLAS"}}' where d.data."_id"."empno" = 7788;


1行更新しました。


コミットが完了しました。

SQL> 


更新された結果を確認します。ETAGはチェックサムです。最初の検索結果とデータが同じなので、ETAGも同じ値になっています。ASOFは検索ごとに更新されています。

SQL> select d.data from empdept_dv d where d.data."_id"."empno" = 7788;


{"_id":{"empno":7788},"ename":"SCOTT","job":"ANALYST","sal":3000,"mgr":7566,"comm":null,"deptno":20,"dept":{"deptno":20,"dname":"RESEARCH","loc":"DALLAS"},"_metadata":{"etag":"F8C263E0EF750556C959750077B31838","asof":"000000000129E350"}}


SQL> 


更新するJSONドキュメントから_metadataを除くと更新されることより、JSON二面性ビューの楽観的ロックは強制されるものではなく、開発者が意識して実装する必要がありそうです。


JSONコレクション表


JSONコレクション表EMPDEPT_Jを対象として、JSON二面性ビューEMPDEPT_DVで実施した操作を行います。

従業員SCOTTの情報を取り出します。JSONコレクション表なので_metadataのasofはありません。

SQL> select d.data from empdept_j d where d.data."_id"."empno" = 7788;


{"_id":{"empno":7788},"ename":"SCOTT","job":"ANALYST","sal":3000,"mgr":7566,"comm":null,"deptno":20,"dept":{"deptno":20,"dname":"RESEARCH","loc":"DALLAS"},"_metadata":{"etag":"2F19B4FCB1D5A2B9E0630500590AAC77"}}


SQL> 


給与を更新します。

SQL> update empdept_j d set d.data = '{"_id":{"empno":7788},"ename":"SCOTT","job":"ANALYST","sal":4000,"mgr":7566,"comm":null,"deptno":20,"dept":{"deptno":20,"dname":"RESEARCH","loc":"DALLAS"},"_metadata":{"etag":"2F19B4FCB1D5A2B9E0630500590AAC77"}}' where d.data."_id"."empno" = 7788;


1行更新しました。


コミットが完了しました。

SQL> 


再度、検索します。salの値およびETAGの値が更新されていることが確認できます。

SQL> select d.data from empdept_j d where d.data."_id"."empno" = 7788;


{"_id":{"empno":7788},"ename":"SCOTT","job":"ANALYST","sal":4000,"mgr":7566,"comm":null,"deptno":20,"dept":{"deptno":20,"dname":"RESEARCH","loc":"DALLAS"},"_metadata":{"etag":"2F2ABCBB18716754E0630500590A1DD3"}}


SQL> 


最初に検索したJSONオブジェクトで、SCOTTのデータを置き換えます。ETAGは異なっていますが、そのまま更新されます。

SQL> update empdept_j d set d.data = '{"_id":{"empno":7788},"ename":"SCOTT","job":"ANALYST","sal":3000,"mgr":7566,"comm":null,"deptno":20,"dept":{"deptno":20,"dname":"RESEARCH","loc":"DALLAS"},"_metadata":{"etag":"2F19B4FCB1D5A2B9E0630500590AAC77"}}' where d.data."_id"."empno" = 7788;


1行更新しました。


コミットが完了しました。

SQL> 


更新結果を確認します。JSONコレクション表のETAGはチェックサムではなく行バージョンであるため、データが同じでも最初の検索結果とETAGは異なります。JSON二面性ビューのETAGとは異なります。

SQL> select d.data from empdept_j d where d.data."_id"."empno" = 7788;


{"_id":{"empno":7788},"ename":"SCOTT","job":"ANALYST","sal":3000,"mgr":7566,"comm":null,"deptno":20,"dept":{"deptno":20,"dname":"RESEARCH","loc":"DALLAS"},"_metadata":{"etag":"2F2ABCBB18736754E0630500590A1DD3"}}


SQL> 


JSONコレクション表ではETAGは生成されますが、JSON二面性ビューとは異なり、それ自体では同時実行制御は行わないことがわかります。

JSONコレクション表のETAGを使って同時実行制御を行う例として、以下のプロシージャupdate_jct_with_etagを作成してみました。


先ほどの手順のupdate文の実行をupdate_jct_with_etagの呼び出しに置き換えます。

最初に従業員SCOTTのデータを取得します。

SQL> select d.data from empdept_j d where d.data."_id"."empno" = 7788;


{"_id":{"empno":7788},"ename":"SCOTT","job":"ANALYST","sal":3000,"mgr":7566,"comm":null,"deptno":20,"dept":{"deptno":20,"dname":"RESEARCH","loc":"DALLAS"},"_metadata":{"etag":"2F2ABCBB18736754E0630500590A1DD3"}}


SQL> 


給与を更新します。

SQL> exec update_jct_with_etag('{"_id":{"empno":7788},"ename":"SCOTT","job":"ANALYST","sal":4000,"mgr":7566,"comm":null,"deptno":20,"dept":{"deptno":20,"dname":"RESEARCH","loc":"DALLAS"},"_metadata":{"etag":"2F2ABCBB18736754E0630500590A1DD3"}}');


PL/SQLプロシージャが正常に完了しました。


コミットが完了しました。

SQL> 


再度、検索します。

SQL> select d.data from empdept_j d where d.data."_id"."empno" = 7788;


{"_id":{"empno":7788},"ename":"SCOTT","job":"ANALYST","sal":4000,"mgr":7566,"comm":null,"deptno":20,"dept":{"deptno":20,"dname":"RESEARCH","loc":"DALLAS"},"_metadata":{"etag":"2F2ABCBB18766754E0630500590A1DD3"}}


SQL> 


最初に検索したJSONオブジェクトで、SCOTTのデータを置き換えます。プロシージャupdate_jct_with_etagは現行のETAGと更新するデータに含まれるETAGを比較して、不一致を検出して例外を上げています。

SQL> exec update_jct_with_etag('{"_id":{"empno":7788},"ename":"SCOTT","job":"ANALYST","sal":3000,"mgr":7566,"comm":null,"deptno":20,"dept":{"deptno":20,"dname":"RESEARCH","loc":"DALLAS"},"_metadata":{"etag":"2F2ABCBB18736754E0630500590A1DD3"}}');

BEGIN update_jct_with_etag('{"_id":{"empno":7788},"ename":"SCOTT","job":"ANALYST","sal":3000,"mgr":7566,"comm":null,"deptno":20,"dept":{"deptno":20,"dname":"RESEARCH","loc":"DALLAS"},"_metadata":{"etag":"2F2ABCBB18736754E0630500590A1DD3"}}'); END;

*

行でエラーが発生しました 1:

ORA-06510: PL/SQL: ユーザー定義の例外が発生しましたが、処理されませんでした

ORA-06512: "WKSP_APEXDEV.UPDATE_JCT_WITH_ETAG", 行27

ORA-06512: 行1


https://docs.oracle.com/error-help/db/ora-06510/



More Details :

https://docs.oracle.com/error-help/db/ora-06510/

https://docs.oracle.com/error-help/db/ora-06512/

SQL> 


JSONコレクション表ではETAGは生成しますが、同時実行制御の実装は開発者に任されています。


APEXアプリケーションの作成



二面性ビューEMPDEPT_DVおよびJSONコレクション表EMPDEPT_Jを操作するAPEXアプリケーションを作成します。

作成するアプリケーションの名前JSON ETAG OCCとします。


アプリケーションが作成されたら、共有コンポーネントJSONソースを開き、二面性ビューEMPDEPT_DVとJSONコレクション表EMPDEPT_Jを、JSONソースとして作成します。

Oracle APEX 24.2.2では不具合のため、データソース二面性ビューを作成できません。ワークアラウンドとして、二面性ビューをJSONソースとして作成します。


JSONソースを開いたら、作成をクリックします。


最初に二面性ビューEMPDEPT_DVを元にしたJSONソースを作成します。

名前EMPDEPT_DVJSONソース・タイプJSON列のある表を選択します。JSON列のある表としてEMPDEPT_DVを選択します。

へ進みます。


JSON列1DATA(Json)になります。

へ進みます。


C_ID_EMPNO主キーチェックを入れます。今回は属性_idにe.empnoの代わりに{ "empno": e.empno }を設定しているため、主キーが自動検出されていません。


C_METADATA_ETAG(セレクタ_metadata.etag)およびC_METADATA_ASOF(セレクタ_metadata.asof)のデータ型Varchar2として検出されていることを確認します。異なる場合は、作成されたJSONソースのデータ・プロファイルを更新します。

作成をクリックします。


JSONソースとしてEMPDEPT_DVが作成されます。


次にJSONコレクション表EMPDEPT_Jを元にしたJSONソースを作成します。

名前EMPDEPT_JJSONソース・タイプJSONコレクション表を選択します。JSONコレクション表としてEMPDEPT_Jを選択します。

へ進みます。


EMPDEPT_DVと同様に、C_ID_EMPNO主キーチェックを入れます。


C_METADATA_ETAGデータ型Varchar2として検知されていることを確認します。JSONコレクション表にはC_METADATA_ASOFはありません。

作成をクリックします。


JSONソースとしてEMPDEPT_DVおよびEMPDEPT_Jが作成されました。


以上でJSONソースの準備は完了です。

それぞれのデータ・ソースを元にフォーム付き対話モード・レポートのページを作成します。

ページの作成をクリックします。


対話モード・レポートを選択します。


最初に二面性ビューEMPDEPT_DVをデータ・ソースとしたページを作成します。

ページ定義名前二面性ビューとし、フォーム・ページを含めるオンにします。フォーム・ページ名EMPDEPT_DVとします。

データ・ソースJSONソースを選び、JSONソースEMPDEPT_DVを選択します。

へ進みます。


主キー列1C_ID_EMPNO (Number)です。

ページの作成を実行します。


同様の手順でJSONコレクション表EMPDEPT_Jをデータ・ソースとしたページを作成します。

ページ定義名前JSONコレクション表フォーム・ページ名EMPDEPT_JJSONソースとしてEMPDEPT_Jを指定し、ページを作成します。


二面性ビューEMPDEPT_DVおよびJSONコレクション表EMPDEPT_Jをデータ・ソースとした対話モード・レポートとフォームのページが作成されました。


以上でAPEXアプリケーションの準備は完了です。


APEXアプリケーションでの楽観的同時実行制御の確認



作成したAPEXアプリケーションを使って、従業員の給与を同時に更新してみます。


JSON二面性ビュー


作成したAPEXアプリケーションを実行し、二面性ビューの対話モード・レポートを開きます。

列C Metadata Etag(セレクタ_metadata.etag)の値は列のデータが同じ値であれば、同じ値になります。列C Metadata Asof(セレクタ_metadata.asof)の値は、二面性ビューEMPDEPT_DVを検索した時点の値になります。結果として、レポートがリフレッシュされるたびに変更されます。


従業員SCOTTのフォームを開きます。このとき、レポート上の列C Metadata Asofの値を覚えておきます。


従業員SCOTTの編集フォームが開きます。

ページ・アイテムのC Metadata Asofの値は、レポートに表示されている値から変わっています。


元々、レポートからフォームが開かれるときは、主キーの値のみがフォームに渡されます。フォームのページに作成されている初期化プロセスにより、渡された主キーの値からデータ・ソースを検索し、その時点での値がページ・アイテムに設定されます。

対話モード・レポートでは、属性ターゲットで、フォームに渡される値が設定されています。


ターゲットアイテムの設定に、ページ遷移時に渡される値が設定されています。レポート列C_ID_EMPNOの値がフォームのページ・アイテムP3_C_ID_EMPNOに渡されます。

主キーの値のみで、列の他の値はフォームには渡されません。アイテムの設定に他の列も含めることはできますが、通常はフォームの初期化プロセスにより、データベースに保存されている現行の値に置き換えられるため、効果はありません。


フォームのページでは、レンダリング前タイプフォーム - 初期化のプロセスが実行されます。このプロセスでは、識別フォーム・リージョンに設定されたリージョンのソースよりデータを取り出し、リージョンに含まれているページ・アイテムに値を設定します。


リージョンEMPDEPT_DVソース位置JSONソースJSONソースEMPDEPT_DVとなっています。


リージョンに含まれるページ・アイテムには、それぞれリージョンのソースに含まれるが設定されています。主キーとして扱われるページ・アイテム(今回のソースではC_ID_EMPNO)は、主キーオンになります。


APEXはフォームを開いた時点の更新可能なページ・アイテムの値から、楽観的ロックに使用するチェックサムを生成します。二面性ビューEMPDEPT_DVでは、主キーP1_C_ID_EMPNO以外の列がチェックサム生成の対象となります。この中に検索するたびに値が変わるC_METADATA_ASOFも含まれています。


フォームのページにコードを追加して、計算されたチェックサムを表示させてみます。

フォームのページに静的コンテンツのリージョンを作成します。ソースHTMLコードとして以下を記述します。

<div id="showChecksum"></div>


ページ・プロパティJavaScriptページ・ロード時に実行に以下を記述します。
const v = document.getElementById("pPageFormRegionChecksums");
document.getElementById("showChecksum").innerText = v.value;
idがpPageFormRegionChecksums、タイプがhiddenのINPUT要素のvalueを、静的コンテンツのリージョンにコピーしています。(APEX 24.2.2での実装です。内部の仕組みなので、APEXのバージョンごとに変わることがあります。

この値はINPUT要素として設定されているため、ページの送信時にページ・アイテムの値と共に、サーバーに送信されます。


以上の変更を加えてフォームを開きます。

フォームが開いたときに計算されたチェックサムが表示されます。


そのまま変更の適用をクリックします。ページ・アイテムの値は変更していないにも関わらず、同時実行制御に関するエラーが発生します。

  • ユーザーが更新処理を開始してから、データベース内の現行バージョンのデータが変更されています。


フォームから送信されたデータは、(デフォルトでは)タイプフォーム - 行の自動処理(DML)のプロセスで処理されます。


設定失われた更新の防止オンのときは、データ・ソースを更新する前に、送信されたデータに含まれる主キーC_ID_EMPNOの値を条件として、リージョンのデータ・ソースよりJOB、MGR、SAL、COMM、DEPT_LOC、DEPT_DNAME、DEPT_DEPTNO、DNAME、DEPTNO、C_METADATA_ASOF、C_METADATA_ETAGを取り出し、チェックサムを再計算します。行のロックはいのときは、この検索時に、選択した列の排他ロックを取得します。チェックサムの再計算と一致を確認してから実際にデータを更新するまでのわずかな間ですが、その間に他のセッションからデータが更新されないようにしています。

チェックサムが一致していないと、先ほどのエラーが発生します。

チェックサムの再計算時にC_METADATA_ASOFを取得し直しているため、フォームを開いた時のチェックサムと、再計算されたチェックサムは必ず異なります。結果として、APEXによる同時実行制御のエラーが発生します。

以下の2通りの対策が考えられます。
  • 失われた更新の防止オフにして、APEXによる同時実行制御を無効にする。JSON二面性ビューによる同時実行制御は有効です。
  • C_METADATA_ASOFをチェックサムの対象から外す。
C_METADATA_ASOFをチェックサムの対象から外す方法はいくつかあります。

単純に、ページ・アイテムP3_C_METADATA_ASOFコメント・アウトすると、ページ・アイテムとして表示もされなくなり、チェックサムの対象からも外れます。


タイプ表示のみに変更し、設定ページの送信時に送信オフにしても、チェックサムの対象から外れます。


ソース問合せのみオンにしても、チェックサムの対象から外れます。


C_METADATA_ASOFをチェックサムの計算の対象から外して、フォームによる更新を確認します。

従業員SCOTTのフォームを開きます。給与を変更して変更の適用をクリックします。


今度はエラーは発生せず、給与も指定した通りに更新されます。

再度、従業員SCOTTのフォームを開きます。まだ、変更の確定はクリックしません。


別のセッションより、従業員SCOTTの給与を更新します。

update emp set sal = 10000 where ename = 'SCOTT';

SQL> update emp set sal = 10000 where ename = 'SCOTT';


1行更新しました。


コミットが完了しました。

SQL> 


開いている従業員SCOTTのフォームの給与を変更し、変更の適用をクリックします。

データベースに保存されている給与の値はフォームを開いた時点での給与の値と異なるため、再計算されたチェックサムが一致せず、APEXによる同時実行制御のエラーが発生します。


プロセスプロセス・フォームEMPDEPT_DV設定失われた更新の防止オフにして、APEXによる同時実行制御を無効化します。


先ほどと同じ操作を行います。

APEXによる同時実行制御は行われていませんが、送信データにETAGが含まれているため、JSON二面性ビューによる同時実行制御は有効です。

そのため、ORA-42699のエラー(ETAGの不一致)が発生し更新が拒否されます。


APEXアプリケーションよりETAG(ページ・アイテムC_METADATA_ETAGの値)を送信対象から外すと、二面性ビューの更新データにETAGが含まれなくなります。

更新データのETAG(_metadata.etagの値)が含まれていない場合、そのままデータが更新されます。

ページ・アイテムP3_C_METADATA_ETAGコメント・アウトし、同じ操作を行います。


従業員SCOTTのフォームを開きます。


別のセッションより、従業員SCOTTの給与を更新します。

update emp set sal = 2000 where ename = 'SCOTT';

SQL> update emp set sal = 2000 where ename = 'SCOTT';


1行更新しました。


コミットが完了しました。

SQL> 


開いている従業員SCOTTのフォームの給与を変更し、変更の適用をクリックします。


今度はデータが更新されます。


この状態は、APEXおよび二面性ビューの双方の、楽観的同時実行制御が適用されていない状態です。

基本的にAPEXアプリケーションでは、APEXの同時実行制御が有効であれば十分です。APEXの同時実行制御が適用できない場合に、JSON二面性ビューの同時実行制御の活用を検討することになるでしょう。


JSONコレクション表


JSONコレクション表はETAGは生成しますが、楽観的同時実行制御は実装されていません。_metadataにasofも含まれていないため、APEXの楽観的同時実行制御はそのまま適用できます。

JSONコレクション表での同時実行制御は、生成されたETAGを使って開発者が実装する必要があります。

試しにJSONコレクション表のETAGを活用して、APEXに同時実行制御を組み込んでみます。

フォームの更新を行うプロセスの直前に、ETAGをチェックするプロセスを作成します。ソースPL/SQLコードとして以下を記述します。送信されたフォームに含まれるETAGと、データベースに保存されているETAGを比較し、一致しない場合に例外を上げています。


サーバー側の条件タイプアイテムはNULLではないを選択し、アイテムとして主キーであるP5_C_EMPNOを指定します。


動作を確認します。

JSONコレクション表のレポートを開き、従業員SCOTTのフォームを開きます。


別ウィンドウより、従業員SCOTTの給与を更新します。

update empdept_j d set d.data = json_transform(d.data, set '$.sal' = 10000) where d.data."_id"."empno".number() = 7788;

SQL> update empdept_j d set d.data = json_transform(d.data, set '$.sal' = 10000) where d.data."_id"."empno".number() = 7788;


1行更新しました。


コミットが完了しました。

SQL> 


フォームの変更の確定をクリックすると、raise_application_errorで定義しているORA-20001: ETAG不一致によるトランザクション失敗が発生します。


JSONコレクション表でも、APEXの同時実行制御が有効であれば十分です。APEXの同時実行制御が適用できない場合に、JSONコレクション表のETAGを使った同時実行制御の実装を検討することになるでしょう。

今回の記事は以上になります。

今回の検証に使用したAPEXアプリケーションのエクスポートを以下に置きました。
https://github.com/ujnak/apexapps/blob/master/exports/json-etag-occ.zip

Oracle APEXのアプリケーション作成の参考になれば幸いです。