
- 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構文でJSON Duality Viewを作り、文書を読み書きする
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が表示されました。
文書を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が入りました。
文書からの更新と表からの更新を比べる
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も別の値になりました。
配列から外した子の行を確かめる
部門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人になりました。
etagによる同時更新の検出と、エラーになる操作
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に更新されました。
次に、_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に更新されました。
主キーの変更と定義にないキーを試す
部門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は定義されていない)になりました。
文書を削除して子の行を確かめる
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を作り、更新を制限する
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が追加されました。
@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が許可されていない)になりました。
ディクショナリで定義を確かめる
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列は、どれも空です。この列は文書には出てこず、親の主キーから自動で設定される列だと分かります。
まとめ
JSON Relational Duality Viewは、表のデータをそのままJSON文書として読み書きでき、文書の更新が元の表に反映される機能になります。
JSON Relational Duality Viewは、以下の手順で作成と動作の確認ができます。
- 親子の表(DEPT・EMP)を作り、SQL構文で二面性ビューを作る
- 文書をINSERTして、親と子の表に行が入ることを確かめる
- 文書からの更新と表からの更新が、互いに反映されることを確かめる
- 配列から外した子の行が削除されることを確かめる
- etagを付けた更新と付けない更新の違いを確かめる
- _idの変更と定義にないキーがエラーになることを確かめる
- 文書のDELETEで親子の行が削除されることを確かめる
- GraphQL構文でビューを作り、@noupdateで更新を制限する
- DBA_JSON_DUALITY_VIEWSなどのディクショナリで定義を確かめる
表の正規化を保ったまま、アプリにはJSON文書の形でデータを渡せるため、変換処理や同期の仕組みを持たずに済みます。
最後までご愛読ありがとうございました。
