【Oracle26ai新機能】JSON Duality Viewの使い方|配列から外した子の行は表から削除される【実行ログ付き】

  • URLをコピーしました!
Oracle AI Database
僕の作業環境
  • OS:Oracle Linux 9.7
  • 導入製品:Oracle AI Database 26ai Enterprise Edition Release 23.26.1.0.0
  • 構成:CDB=orcl/PDB=ORCLPDB1

アプリの開発者は「部門と、その部門の社員一覧」を1つのJSONで受け取りたい。一方、DBAとしては、データは正規化した表で管理したい。この2つの要望がぶつかる場面はよくあります。

これまでは、表とJSONの変換をアプリ側で書くか、JSONを別に持って表と同期する必要がありました。変換処理の不具合や、同期のずれが起きやすい部分です。

Oracle Database 23ai以降では、表のデータをJSON文書として読み書きできるJSON Relational Duality View(JSONリレーショナル二面性ビュー)が使えます。本記事では、「JSON Duality Viewの使い方」について、二面性ビューの作り方と、文書の更新が表にどう反映されるかを、僕の環境で実際に動かした結果をもとに解説します。

この記事でわかること
  • SQL構文とGraphQL構文での二面性ビューの作り方
  • 文書のINSERT・UPDATE・DELETEで表がどう変わるか
  • etagによる同時更新の検出
  • 更新を禁止する指定と、ディクショナリ・ビューでの確認方法
目次

JSON Relational Duality View(二面性ビュー)とは

二面性ビューは、親子関係にある表(本記事では部門表DEPTと社員表EMP)を、1件のJSON文書(部門の中に社員の配列を持つ形)として見せるビューです。

データの実体は表にあり、ビューに文書をINSERT・UPDATE・DELETEすると、その内容が元の表に反映されます。逆に、表を直接更新すると、ビューから読む文書も変わります。

作り方は2通りあります。SQL構文では、SELECT JSON {‘キー’ : 列, …} FROM 表 WITH INSERT UPDATE DELETEのように、JSONのキーと列の対応を書き、WITHで許可する操作を指定します。GraphQL構文では、表 @insert @update @delete { キー : 列, … }のように書きます。どちらの構文でも、ルート表の主キーを_idキーに対応させます。

キー・指定意味
_idルート表の主キー。変更・省略・NULLはできない
_metadata.etag文書の内容から計算される値。更新時に渡すと、読んだ後に変更されていないかが確認される
_metadata.asof文書を読んだ時点のSCN
WITH INSERT UPDATE DELETE/@insert @update @delete表ごとに、文書から許可する操作
@noupdateそのキー(列)は文書から更新できない

公式ドキュメントでは、二面性ビューはSQLだけでなく、ORDSのREST APIや、Oracle Database API for MongoDBからも読み書きできると説明されています。アプリはJSON文書のAPIとして扱い、DBAは表として管理する、という役割分担がしやすい機能です。

SQL構文でJSON Duality Viewを作り、文書を読み書きする

STEP

SQL構文で二面性ビューを作る

部門表DEPTと、DEPTを参照する社員表EMPを作り、部門10・20と社員3人を登録します。続けて、SQL構文で、部門の中に社員の配列(emps)を持つ二面性ビューDEPT_DVを作り、部門10の文書を読みます。文書の表示には、JSON型の値を文字列にするJSON_SERIALIZE関数を使い、PRETTYで改行を入れています。

SQL> CREATE TABLE V26_APP.dept (deptno NUMBER PRIMARY KEY, dname VARCHAR2(20));

Table created.

SQL> CREATE TABLE V26_APP.emp (empno NUMBER PRIMARY KEY, ename VARCHAR2(20), sal NUMBER, deptno NUMBER REFERENCES V26_APP.dept);

Table created.

SQL> INSERT INTO V26_APP.dept VALUES (10,'SALES'), (20,'DEV');

2 rows created.

SQL> INSERT INTO V26_APP.emp VALUES (1,'A',100,10), (2,'B',200,10), (3,'C',300,20);

3 rows created.

SQL> CREATE JSON RELATIONAL DUALITY VIEW V26_APP.dept_dv AS
    SELECT JSON {'_id'   : d.deptno,
                 'dname' : d.dname,
                 'emps'  : [ SELECT JSON {'empno' : e.empno, 'ename' : e.ename, 'sal' : e.sal}
                             FROM V26_APP.emp e WITH INSERT UPDATE DELETE
                             WHERE e.deptno = d.deptno ]}
    FROM V26_APP.dept d WITH INSERT UPDATE DELETE;

View created.

SQL> SELECT JSON_SERIALIZE(data PRETTY) AS doc FROM V26_APP.dept_dv d WHERE d.data."_id" = 10;

DOC
----------------------------------------
{
  "_id" : 10,
  "_metadata" :
  {
    "etag" : "A55E3080E3707ABEBCDFDABF3E4D5A8F",
    "asof" : "000000000308933B"
  },
  "dname" : "SALES",
  "emps" :
  [
    {
      "empno" : 1,
      "ename" : "A",
      "sal" : 100
    },
    {
      "empno" : 2,
      "ename" : "B",
      "sal" : 200
    }
  ]
}


1 row selected.

結果は、部門10の文書に_id・dname・社員2人の配列が入り、_metadataにetagとasofが表示されました。

_metadataは、ビューの定義に書かなくても自動で付きます。アプリはこのetagを保持しておき、更新時に渡すことで同時更新を検出できます。

STEP

文書をINSERTして表に行を入れる

DEPT_DVに、部門30と社員2人(4・5)を持つ文書をINSERTし、DEPT表とEMP表を確認します。

SQL> INSERT INTO V26_APP.dept_dv VALUES ('{"_id":30,"dname":"OPS","emps":[{"empno":4,"ename":"D","sal":400},{"empno":5,"ename":"E","sal":500}]}');

1 row created.

SQL> SELECT * FROM V26_APP.dept ORDER BY deptno;

    DEPTNO DNAME
---------- --------------------
        10 SALES
        20 DEV
        30 OPS

3 rows selected.

SQL> SELECT * FROM V26_APP.emp ORDER BY empno;

     EMPNO ENAME                       SAL     DEPTNO
---------- -------------------- ---------- ----------
         1 A                           100         10
         2 B                           200         10
         3 C                           300         20
         4 D                           400         30
         5 E                           500         30

5 rows selected.

結果は、DEPT表に部門30、EMP表に社員4・5が追加され、社員のDEPTNOには文書の親である30が入りました。

1件の文書のINSERTで、親の表と子の表の両方に行が入ります。子の表の外部キー列は文書に書かなくても、親の主キーの値が設定されます。

STEP

文書からの更新と表からの更新を比べる

DEPT_DVの部門30の文書のdnameを、JSON_TRANSFORMでOPS2に変え、DEPT表を確認します。JSON_TRANSFORMは、JSON文書の一部のキーだけを書き換える関数です。

SQL> UPDATE V26_APP.dept_dv d SET data = JSON_TRANSFORM(data, SET '$.dname' = 'OPS2') WHERE d.data."_id" = 30;

1 row updated.

SQL> SELECT deptno, dname FROM V26_APP.dept WHERE deptno = 30;

    DEPTNO DNAME
---------- --------------------
        30 OPS2

1 row selected.

結果は、DEPT表のDNAMEがOPS2に変わりました。

次は逆方向です。EMP表の社員3の給与(SAL)を300から350に直接UPDATEし、部門20の文書を前後で読みます。この確認と、後のetagの確認(STEP5)・ディクショナリでの確認(STEP10)は、同じ構成の表と二面性ビューをV26_DVスキーマに用意して行っています。

SQL> SELECT JSON_SERIALIZE(d.data) AS doc FROM V26_DV.dept_dv d WHERE d.data."_id" = 20;

DOC
----------------------------------------
{"_id":20,"_metadata":{"etag":"A487F1FBC6B0AE562ACE0E274AB57502","asof":"00000000036BDBB5"
},"dname":"DEV","emps":[{"empno":3,"ename":"C","sal":300}]}


1 row selected.

SQL> UPDATE V26_DV.emp SET sal = 350 WHERE empno = 3;

1 row updated.

SQL> SELECT JSON_SERIALIZE(d.data) AS doc FROM V26_DV.dept_dv d WHERE d.data."_id" = 20;

DOC
----------------------------------------
{"_id":20,"_metadata":{"etag":"554DC6A115DE94C74E81CDC885E121AE","asof":"00000000036BDBBA"
},"dname":"DEV","emps":[{"empno":3,"ename":"C","sal":350}]}


1 row selected.

結果は、文書の社員3のsalが350に変わり、etagも別の値になりました。

データの実体は表なので、文書から更新しても表から更新しても、同じデータが変わります。

STEP

配列から外した子の行を確かめる

部門30の文書を、社員4だけを持つ配列に置き換えて(社員5を配列から外して)UPDATEし、EMP表を確認します。

SQL> UPDATE V26_APP.dept_dv d SET data = ('{"_id":30,"dname":"OPS2","emps":[{"empno":4,"ename":"D","sal":400}]}') WHERE d.data."_id" = 30;

1 row updated.

SQL> SELECT empno, deptno FROM V26_APP.emp ORDER BY empno;

     EMPNO     DEPTNO
---------- ----------
         1         10
         2         10
         3         20
         4         30

4 rows selected.

結果は、EMP表から社員5の行が削除され、社員は4人になりました。

文書を丸ごと置き換えると、配列からなくなった子の行は表からも削除されます。本記事では子の表EMPにWITH DELETEを許可しています。アプリが一部の社員だけを入れた文書で更新すると、ほかの社員が消えるので注意が必要です。

etagによる同時更新の検出と、エラーになる操作

STEP

etagで同時更新を検出する

部門20の文書を読んだ時点のetagを保存し、そのあとDEPT表を直接更新して文書を変えます。続けて、保存した古いetagを_metadataに入れて文書を置き換え、次に最新のetagを読み直して置き換えます。

SQL> SELECT JSON_VALUE(d.data, '$._metadata.etag') AS saved_etag FROM V26_DV.dept_dv d WHERE d.data."_id" = 20;

SAVED_ETAG
----------------------------------
A487F1FBC6B0AE562ACE0E274AB57502

1 row selected.

SQL> UPDATE V26_DV.dept SET dname = 'DEV2' WHERE deptno = 20;

1 row updated.

SQL> SELECT JSON_VALUE(d.data, '$._metadata.etag') AS current_etag FROM V26_DV.dept_dv d WHERE d.data."_id" = 20;

CURRENT_ETAG
----------------------------------
2F9A71BFD3FA21634D98A8072C95B67C

1 row selected.

SQL> PROMPT OLD_ETAG = A487F1FBC6B0AE562ACE0E274AB57502

OLD_ETAG = A487F1FBC6B0AE562ACE0E274AB57502

SQL> UPDATE V26_DV.dept_dv d SET data = '{"_id":20,"_metadata":{"etag":"A487F1FBC6B0AE562ACE0E274AB57502"},"dname":"DEV3","emps":[{"empno":3,"ename":"C","sal":300}]}' WHERE d.data."_id" = 20;

UPDATE V26_DV.dept_dv d SET data = '{"_id":20,"_metadata":{"etag":"A487F1FBC6B0AE562ACE0E274AB57502"},"dname":"DEV3","emps":[{"empno":3,"ename":"C","sal":300}]}' WHERE d.data."_id" = 20
*
ERROR at line 1:
ORA-42699: Cannot update JSON Relational Duality View 'DEPT_DV': The ETAG of document with ID 'FB03C11500' in the database did not match the ETAG passed in.

SQL> SELECT JSON_VALUE(d.data, '$._metadata.etag') AS current_etag FROM V26_DV.dept_dv d WHERE d.data."_id" = 20;

CURRENT_ETAG
----------------------------------
2F9A71BFD3FA21634D98A8072C95B67C

1 row selected.

SQL> UPDATE V26_DV.dept_dv d SET data = '{"_id":20,"_metadata":{"etag":"2F9A71BFD3FA21634D98A8072C95B67C"},"dname":"DEV3","emps":[{"empno":3,"ename":"C","sal":300}]}' WHERE d.data."_id" = 20;

1 row updated.

SQL> SELECT deptno, dname FROM V26_DV.dept WHERE deptno = 20;

    DEPTNO DNAME
---------- --------------------
        20 DEV3

1 row selected.

結果は、古いetagではORA-42699(etagが一致しない)になり、DNAMEはDEV2のままでした。最新のetagを付けると、DEV3に更新されました。

これは楽観的ロックの仕組みです。読んだ後に別の更新が入ると、古いetagでの更新は失敗します。

次に、_metadataのetagを付けずに文書を置き換えます。

SQL> UPDATE V26_DV.dept_dv d SET data = '{"_id":20,"dname":"DEV4","emps":[{"empno":3,"ename":"C","sal":350}]}' WHERE d.data."_id" = 20;

1 row updated.

SQL> SELECT deptno, dname FROM V26_DV.dept WHERE deptno = 20;

    DEPTNO DNAME
---------- --------------------
        20 DEV4

1 row selected.

結果は、etagの確認は行われず、DNAMEはDEV4に更新されました。

etagを付けない更新は、ほかの更新を確認なしで上書きします。同時に更新される可能性がある文書では、必ず読んだときのetagを付けて更新します。

STEP

主キーの変更と定義にないキーを試す

部門20の文書の_idを99に変えるUPDATEと、ビューの定義にないキーunknownを含む文書のINSERTを実行します。

SQL> UPDATE V26_APP.dept_dv d SET data = JSON_TRANSFORM(data, SET '$._id' = 99) WHERE d.data."_id" = 20;

UPDATE V26_APP.dept_dv d SET data = JSON_TRANSFORM(data, SET '$._id' = 99) WHERE d.data."_id" = 20
*
ERROR at line 1:
ORA-42603: Cannot update JSON Relational Duality View 'DEPT_DV': The Primary Key column(s) of the root table 'DEPT' cannot be updated, omitted, or set to NULL.

SQL> INSERT INTO V26_APP.dept_dv VALUES ('{"_id":40,"dname":"X","unknown":1,"emps":[]}');

INSERT INTO V26_APP.dept_dv VALUES ('{"_id":40,"dname":"X","unknown":1,"emps":[]}')
*
ERROR at line 1:
ORA-40944: Cannot insert into JSON Relational Duality View 'DEPT_DV': The input JSON document is invalid.
JZN-00651: field 'unknown' is unknown or undefined

結果は、_idの変更はORA-42603(ルート表の主キーは更新できない)、unknownを含む文書はORA-40944(JZN-00651:unknownは定義されていない)になりました。

_idに対応する主キーは文書から変更できません。また、定義にないキーを含む文書は登録できないため、アプリが余計な項目を送ってきた場合はエラーで気づけます。

STEP

文書を削除して子の行を確かめる

DEPT_DVから部門30の文書をDELETEし、DEPT表の部門30とEMP表の社員4・5の件数を確認します。

SQL> DELETE FROM V26_APP.dept_dv d WHERE d.data."_id" = 30;

1 row deleted.

SQL> SELECT COUNT(*) AS dept30 FROM V26_APP.dept WHERE deptno = 30;

    DEPT30
----------
         0

1 row selected.

SQL> SELECT COUNT(*) AS emp_of_30 FROM V26_APP.emp WHERE empno IN (4,5);

 EMP_OF_30
----------
         0

1 row selected.

結果は、どちらも0件になりました。文書のDELETEで、親の行と配列に入っていた子の行がまとめて削除されます。

GraphQL構文でJSON Duality Viewを作り、更新を制限する

STEP

GraphQL構文で二面性ビューを作る

DEPT表とEMP表から、GraphQL構文で二面性ビューDEPT_GQLを作ります。キーの名前は、部門名をname、社員の配列をstaff、社員番号をnoにしています。部門10だけが入った状態で文書を読み、部門20と社員3を持つ文書をINSERTします。

SQL> CREATE JSON RELATIONAL DUALITY VIEW V26_APP.dept_gql AS
    V26_APP.dept @insert @update @delete {
      _id : deptno,
      name : dname,
      staff : V26_APP.emp @insert @update @delete { no : empno, name : ename }
    };

View created.

SQL> SELECT JSON_SERIALIZE(data PRETTY) AS doc FROM V26_APP.dept_gql;

DOC
----------------------------------------
{
  "_id" : 10,
  "_metadata" :
  {
    "etag" : "EBE1761F4289D258F77FBA8C1E963C85",
    "asof" : "0000000003093984"
  },
  "name" : "SALES",
  "staff" :
  [
    {
      "no" : 1,
      "name" : "A"
    },
    {
      "no" : 2,
      "name" : "B"
    }
  ]
}


1 row selected.

SQL> INSERT INTO V26_APP.dept_gql VALUES ('{"_id":20,"name":"DEV","staff":[{"no":3,"name":"C"}]}');

1 row created.

SQL> SELECT e.empno, e.deptno FROM V26_APP.emp e ORDER BY 1;

     EMPNO     DEPTNO
---------- ----------
         1         10
         2         10
         3         20

3 rows selected.

結果は、SQL構文と同じ形の文書が表示され、文書のINSERTでEMP表に部門20の社員3が追加されました。

GraphQL構文では、表名の後に@insert・@update・@deleteを付け、{ キー : 列 }で対応を書きます。EMP表はDEPT表を参照する外部キーを持っているため、結合条件を書かなくても部門ごとの社員が配列に入りました。SQL構文より短く書けるのが利点です。

STEP

@noupdateで更新を禁止する

DEPT表に@updateだけを付け、nameキー(DNAME列)に@noupdateを付けた二面性ビューDEPT_ROを作ります。nameを変えるUPDATEと、文書のINSERTを実行します。

SQL> CREATE JSON RELATIONAL DUALITY VIEW V26_APP.dept_ro AS V26_APP.dept @update { _id : deptno, name : dname @noupdate };

View created.

SQL> UPDATE V26_APP.dept_ro d SET data = JSON_TRANSFORM(data, SET '$.name' = 'X') WHERE d.data."_id" = 10;

UPDATE V26_APP.dept_ro d SET data = JSON_TRANSFORM(data, SET '$.name' = 'X') WHERE d.data."_id" = 10
*
ERROR at line 1:
ORA-40940: Cannot update field 'name' corresponding to column 'DNAME' of table 'DEPT' in JSON Relational Duality View 'DEPT_RO': Missing UPDATE annotation or NOUPDATE annotation specified.

SQL> INSERT INTO V26_APP.dept_ro VALUES ('{"_id":30,"name":"OPS"}');

INSERT INTO V26_APP.dept_ro VALUES ('{"_id":30,"name":"OPS"}')
*
ERROR at line 1:
ORA-40937: Cannot insert into table 'DEPT' in JSON Relational Duality View 'DEPT_RO': Missing INSERT annotation or NOINSERT annotation specified.

結果は、UPDATEはORA-40940(nameは更新できない)、INSERTはORA-40937(DEPT表へのINSERTが許可されていない)になりました。

表ごとの@insert・@update・@deleteと、キーごとの@noupdateで、文書から変更できる範囲を絞れます。

STEP

ディクショナリで定義を確かめる

V26_DVスキーマのDEPT_DVを、DBA_JSON_DUALITY_VIEWS・DBA_JSON_DUALITY_VIEW_TABS・DBA_JSON_DUALITY_VIEW_TAB_COLSで確認します。

SQL> SELECT view_name, root_table_name, allow_insert, allow_update, allow_delete, read_only, status FROM dba_json_duality_views WHERE view_owner = 'V26_DV';

VIEW_NAME ROOT_TABLE_NAME ALLOW_INSER ALLOW_UPDAT ALLOW_DELET READ_ONLY STATUS
--------- --------------- ----------- ----------- ----------- --------- ------
DEPT_DV   DEPT            TRUE        TRUE        TRUE        FALSE     VALID

1 row selected.

SQL> SELECT table_name, root_table, allow_insert, allow_update, allow_delete FROM dba_json_duality_view_tabs WHERE view_owner = 'V26_DV' ORDER BY table_number;

TABLE_NAME ROOT_TABLE  ALLOW_INSER ALLOW_UPDAT ALLOW_DELET
---------- ----------- ----------- ----------- -----------
DEPT       TRUE        TRUE        TRUE        TRUE
EMP        FALSE       TRUE        TRUE        TRUE

2 rows selected.

SQL> SELECT table_name, column_name, json_key_name, primary_key_pos, etag_pos FROM dba_json_duality_view_tab_cols WHERE view_owner = 'V26_DV' ORDER BY table_number, column_name;

TABLE_NAME COLUMN_N JSON_KEY_NAME PRIMARY_KEY_POS   ETAG_POS
---------- -------- ------------- --------------- ----------
DEPT       DEPTNO   _id                         1          1
DEPT       DNAME    dname                                  2
EMP        DEPTNO
EMP        EMPNO    empno                       1          1
EMP        ENAME    ename                                  2
EMP        SAL      sal                                    3

6 rows selected.

結果は、ビューのルート表がDEPTで、DEPT表・EMP表ともにINSERT・UPDATE・DELETEがTRUEでした。列ごとには、対応するJSONのキー名(JSON_KEY_NAME)、主キーの位置(PRIMARY_KEY_POS)、ETAG_POSが表示されました。

文書のキーに対応していないEMP表のDEPTNO列は、どれも空です。この列は文書には出てこず、親の主キーから自動で設定される列だと分かります。

DBA_JSON_DUALITY_VIEWSなどで所有者を絞り込むときは、VIEW_OWNER列を使います。

まとめ

JSON Relational Duality Viewは、表のデータをそのままJSON文書として読み書きでき、文書の更新が元の表に反映される機能になります。

JSON Relational Duality Viewは、以下の手順で作成と動作の確認ができます。

JSON Relational Duality Viewの手順
  • 親子の表(DEPT・EMP)を作り、SQL構文で二面性ビューを作る
  • 文書をINSERTして、親と子の表に行が入ることを確かめる
  • 文書からの更新と表からの更新が、互いに反映されることを確かめる
  • 配列から外した子の行が削除されることを確かめる
  • etagを付けた更新と付けない更新の違いを確かめる
  • _idの変更と定義にないキーがエラーになることを確かめる
  • 文書のDELETEで親子の行が削除されることを確かめる
  • GraphQL構文でビューを作り、@noupdateで更新を制限する
  • DBA_JSON_DUALITY_VIEWSなどのディクショナリで定義を確かめる

表の正規化を保ったまま、アプリにはJSON文書の形でデータを渡せるため、変換処理や同期の仕組みを持たずに済みます。

注意点は、配列から外した子の行が削除されることと、etagを付けない更新は確認なしで上書きされることです。アプリには、読んだときのetagを付けて更新し、ORA-42699なら読み直す処理を入れておくのがおすすめです。

最後までご愛読ありがとうございました。

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!
目次