技術ブログ
目次
この記事は JPOUG Advent Calendar 2017 の16日目のエントリです。
前からすこし気になっていたOracle Database 12c JSON機能を使ってみました。
使うに当たっての手頃なターゲットとして、JMESPATH式のチュートリアルの内容を「なぞる」ことにします。
(ただし、Functionsは割愛・・・)
なお、JMESPATH式は、AWS CLIなどで使用されているJSONパス式です。
キー名"a"を指定して、JSONオブジェクトからキーに対応する値を取得します。
{
"a" : "foo", ←★★
"b" : "bar",
"c" : "baz"
}
JSONオブジェクトはあらかじめテーブルjson_dataのdata列に格納しておきます。
まず、テーブルjson_dataを作成します。(データ登録および更新をUPDATE文で統一したいため、1行ダミーのデータを入れています)
SQL> DROP TABLE json_data;
Table dropped.
SQL> CREATE TABLE json_data (
2 data CLOB CHECK (data IS JSON (STRICT))
3 );
Table created.
SQL> INSERT INTO json_data (data) VALUES('{}');
1 row created.
data列にIS JSON制約を設定していることに注意してください。この制約がないと、ドット表記法が使えません。
列にはis jsonチェック制約が設定されている必要があり、これにより、整形式のJSONデータが含まれるようにします。これらの規則のどちらも順守されない場合、問合せのコンパイル時にエラーが発生します。
次にデータを登録します。
SQL> UPDATE json_data SET data ='
2 {"a": "foo", "b": "bar", "c": "baz"}
3 ';
1 row updated.
Oracle Database JSONに用意されているドット表記法アクセスでアクセスします。
.
SQL> SELECT j.data.a FROM json_data j;
A
--------------------------------------------------------------------------------
foo
別の方法として、JSON_xxxxファンクションを使う方法があります。
.
SQL> SELECT JSON_VALUE(j.data, '$.a') FROM json_data j;
JSON_VALUE(J.DATA,'$.A')
--------------------------------------------------------------------------------
foo
"a.b.c.d"のように、キー名を連ねる形で指定して、入れ子になっているJSONオブジェクトから値を取得します。
{
"a" :
{
"b" :
{
"c" :
{
"d" : "value"←★
}
}
}
│
│a.b.c.d
↓
"value"
JSONオブジェクトはあらかじめテーブルjson_dataの列dataに格納しておきます。
SQL> UPDATE json_data SET data ='
2 {"a": {"b": {"c": {"d": "value"}}}}
3 ';
1 row updated.
ドット表記法アクセスを用いて、入れ子になっているJSONオブジェクト( a → b → c → d)から値を取得できます。
SQL> SELECT j.data.a.b.c.d FROM json_data j;
A
--------------------------------------------------------------------------------
value
さっきの例と同様にJSON_VALUE()ファンクションを使った例も示します。
.
SQL> SELECT JSON_VALUE(j.data, '$.a.b.c.d') FROM json_data j;
JSON_VALUE(J.DATA,'$.A.B.C.D')
--------------------------------------------------------------------------------
value
配列オブジェクトから2番目の値を取得します。
.
[
"a",
"b",←★
"c",
"d",
"e",
"f"
]
│
│[1]
↓
"b"
データを準備します。
SQL> UPDATE json_data SET data ='["a", "b", "c", "d", "e", "f"]';
1 row updated.
残念ながら、ドット表記では、トップレベルが配列であるJSONから、要素を得ることはできないようです。(この動作はどうなのだろうか・・・)
SQL> SELECT j.data[1] FROM json_data j;
SELECT j.data[1] FROM json_data j
*
ERROR at line 1:
ORA-00923: FROM keyword not found where expected
JSON_VALUEを使えば、トップレベルが配列であるJSONから要素の値を取得できます。
SQL> SELECT JSON_VALUE(data,'$[1]') FROM json_data j;
JSON_VALUE(DATA,'$[1]')
--------------------------------------------------------------------------------
b
トップレベルが配列だと、ドット表記法でうまくアクセスできない制限があるようなので、トップレベルをオブジェクトにして、もう一度試すことにします。
{
"k" :
[
"a",←★
"b",
"c",
"d",
"e",
"f"
]
}
データを準備します。
SQL> UPDATE json_data SET data ='{"k": ["a", "b", "c", "d", "e", "f"]}';
1 row updated.
トップレベルが配列でなければ、ドット表記でも配列の要素を得ることができます。
SQL> SELECT j.data.k[1] FROM json_data j;
K
--------------------------------------------------------------------------------
b
なお、ここで説明したドット表記法における配列アクセスはOracle Database 12.2から使用可能です。
Oracle Database 12.1では使用できません。
単純なドット表記法構文を使用して、配列とその要素にアクセスできるようになりました。
つぎに、キーおよび配列の要素の指定を組み合わせたJSONオブジェクトから値を取得してみます。
{"a": {
"b": {
"c": [
{"d": [0, [1, 2]]},
{"d": [3, 4]}
]
}
}}
│
│a.b.c[0].d[1][0]
↓
1
データを準備します。
SQL> UPDATE json_data SET data ='{"a": {
2 "b": {
3 "c": [
4 {"d": [0, [1, 2]]},
5 {"d": [3, 4]}
6 ]
7 }
8 }}';
1 row updated.
ドット表記法でアクセスできます。
SQL> SELECT j.data.a.b.c[0].d[1][0] FROM json_data j;
A
--------------------------------------------------------------------------------
1
JSON_VALUE()ファンクション+パス式でも当然アクセスできます。
SQL> SELECT JSON_VALUE(j.data,'$.a.b.c[0].d[1][0]') FROM json_data j;
JSON_VALUE(J.DATA,'$.A.B.C[0].D[1][0]')
--------------------------------------------------------------------------------
1
スライスは、配列のインデックス値の範囲を指定して、配列の一部を取得する操作です。
.
{
"k" :
[
0,←★
1,←★
2,←★
3,←★
4,←★
5,
6,
7,
8,
9
]
}
│
↓[0:5]
[
0,
1,
2,
3,
4
]
データを準備します。すでに確認したように、Oracle Database JSONのドット表記法ではトップレベルが配列であるJSONから要素を得ることはできないようなので、トップレベルをオブジェクトにし、その中に配列を入れています。
SQL> UPDATE json_data SET data ='{"k": [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]}';
1 row updated.
パス表記法: インデックス値の範囲指定は [0:4]ではなく [0 to 4]ですが、うまくスライスできています。
SQL> SELECT j.data.k[0 to 4] FROM json_data j;
K
--------------------------------------------------------------------------------
[0,1,2,3,4]
次にJSON_xxxファンクションを試しますが、返される値がスカラー値ではないため、JSON_VALUEは使えません。代わりにJSON_QUERYを使用します。
パス表記法のときと同様に、インデックス値の範囲指定は [0:4]ではなく [0 to 4]で指定します。
ただし、以下のようにJSON_QUERYの引数にデータとパス式のみを指定すると、値が得られません。
SQL> SELECT JSON_QUERY(j.data, '$.k[0 to 4]') FROM json_data j;
JSON_QUERY(J.DATA,'$.K[0TO4]')
--------------------------------------------------------------------------------
その理由は、 ERROR ON ERRORを指定するとわかります。
SQL> SELECT JSON_QUERY(j.data, '$.k[0 to 4]' ERROR ON ERROR) FROM json_data j;
SELECT JSON_QUERY(j.data, '$.k[0 to 4]' ERROR ON ERROR) FROM json_data j
*
ERROR at line 1:
ORA-40480: result cannot be returned without array wrapper
エラーメッセージの通り、返される値が複数の場合には、配列ラッパーを指定して、複数の値を配列に変換する必要があります。
JSON_QUERY()にWITH CONDITIONAL WRAPPERを指定して、配列ラッパーを適用すると、返される複数の値を配列化した値が得られます。
SQL> SELECT JSON_QUERY(j.data, '$.k[0 to 4]' WITH CONDITIONAL WRAPPER ERROR ON ERROR) FROM json_data j;
JSON_QUERY(J.DATA,'$.K[0TO4]'WITHCONDITIONALWRAPPERERRORONERROR)
--------------------------------------------------------------------------------
[0,1,2,3,4]
開始インデックス値、終了インデックス値を変えたパターンですが、パス表記法、JSON_QUERY+パス式の両方で処理できます。
SQL> SELECT j.data.k[5 to 9] FROM json_data j;
K
--------------------------------------------------------------------------------
[5,6,7,8,9]
SQL> SELECT JSON_QUERY(j.data, '$.k[5 to 9]' WITH CONDITIONAL WRAPPER ERROR ON ERROR) FROM json_data j;
JSON_QUERY(J.DATA,'$.K[5TO9]'WITHCONDITIONALWRAPPERERRORONERROR)
--------------------------------------------------------------------------------
[5,6,7,8,9]
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
│
│[:5]
↓
[0, 1, 2, 3, 4]
Oracle Database JSONでは、スライスで開始インデックス値を省略できません。
SQL> SELECT j.data.k[ to 4] FROM json_data j;
SELECT j.data.k[ to 4] FROM json_data j
*
ERROR at line 1:
ORA-00900: invalid SQL statement
開始インデックス値は明示的に指定する必要があるようです。先頭のインデックス値として、0を指定すれば、想定した値が得られます。
SQL> SELECT j.data.k[0 to 4] FROM json_data j;
K
--------------------------------------------------------------------------------
[0,1,2,3,4]
SQL> SELECT JSON_QUERY(j.data, '$.k[0 to 4]' WITH CONDITIONAL WRAPPER) FROM json_data j;
JSON_QUERY(J.DATA,'$.K[0TO4]'WITHCONDITIONALWRAPPER)
--------------------------------------------------------------------------------
[0,1,2,3,4]
JMESPATHのプロジェクションは、複数のデータ(コレクション)を内包するJSONを、「串刺し」するようにして配列データを得る操作です。
List Projectionsは、配列を対象にしたプロジェクションです。
{
"people" :
[
{
"first" : "James",←★
"last" : "d"
},
{
"first" : "Jacob",←★
"last" : "e"
},
{
"first" : "Jayden",←★
"last" : "f"
},
{
"missing" : "different"
}
],
"foo" :
{
"bar" : "baz"
}
}
│
│people[*].first
↓
[
"James",
"Jacob",
"Jayden"
]
データを準備します。
SQL> UPDATE json_data SET data ='
2 {
3 "people": [
4 {"first": "James", "last": "d"},
5 {"first": "Jacob", "last": "e"},
6 {"first": "Jayden", "last": "f"},
7 {"missing": "different"}
8 ],
9 "foo": {"bar": "baz"}
10 }
11 ';
1 row updated.
ドット表記法ではList Projectionが可能です。
SQL> SELECT d.data.people[*].first FROM json_documents d;
PEOPLE
--------------------------------------------------------------------------------
["James","Jacob","Jayden"]
JSON_QUERY()ファンクション+パス式でもList Projectionが可能です。ただし、WITH CONDITIONAL WRAPPER の指定が必要です。
SQL> SELECT JSON_QUERY(j.data, '$.people[*].first' WITH CONDITIONAL WRAPPER ERROR ON ERROR ) FROM json_data j;
JSON_QUERY(J.DATA,'$.PEOPLE[*].FIRST'WITHCONDITIONALWRAPPERERRORONERROR)
--------------------------------------------------------------------------------
["James","Jacob","Jayden"]
WITH CONDITIONAL WRAPPER を指定しないとエラーになります。
SQL> SELECT JSON_QUERY(j.data, '$.people[*].first' ERROR ON ERROR ) FROM json_data j;
SELECT JSON_QUERY(j.data, '$.people[*].first' ERROR ON ERROR ) FROM json_data j
*
ERROR at line 1:
ORA-40480: result cannot be returned without array wrapper
スライスプロジェクションは、配列を対象にし、スライスするプロジェクションです。
(JMESPath Tutorialの例が良くないので、スライスのインデックス値を変えています)
{
"people" :
[
{
"first" : "James",←★
"last" : "d"
},
{
"first" : "Jacob",←★
"last" : "e"
},
{
"first" : "Jayden",
"last" : "f"
},
{
"missing" : "different"
}
],
"foo" :
{
"bar" : "baz"
}
}
│
│people[:2].first
↓
[
"James",
"Jacob"
]
データを準備します。
SQL> UPDATE json_data SET data ='
2 {
3 "people": [
4 {"first": "James", "last": "d"},
5 {"first": "Jacob", "last": "e"},
6 {"first": "Jayden", "last": "f"},
7 {"missing": "different"}
8 ],
9 "foo": {"bar": "baz"}
10 }
11 ';
1 row updated.
ドット表記法でも処理可能です。配列範囲の終了インデックス値が-1となることに気をつけてください。
SQL> SELECT j.data.people[0 to 1].first FROM json_data j;
PEOPLE
--------------------------------------------------------------------------------
["James","Jacob"]
JSON_QUERYファンクション+パス式でも同様に処理可能です。
SQL> SELECT JSON_QUERY(j.data, '$.people[0 to 1].first' WITH CONDITIONAL WRAPPER ERROR ON ERROR) FROM json_data j;
JSON_QUERY(J.DATA,'$.PEOPLE[0TO1].FIRST'WITHCONDITIONALWRAPPERERRORONERROR)
--------------------------------------------------------------------------------
["James","Jacob"]
なお、ドット表記法において、返される要素数が1つになるようにインデックス値を変えると、返されるデータの型が配列ではなくなります。
SQL> SELECT j.data.people[0 to 0].first FROM json_data j;
PEOPLE
--------------------------------------------------------------------------------
James
SQL> SELECT j.data.people[0].first FROM json_data j;
PEOPLE
--------------------------------------------------------------------------------
James
返される要素数に応じて、返される型が変わるのはドット表記法独特の動作です。詳細は以下を参照してください。
なお、WITH CONDITIONAL WRAPPERを指定したJSON_QUERYでは、返される要素数が1でも配列として返されます。
SQL> SELECT JSON_QUERY(j.data, '$.people[0 to 0].first' WITH CONDITIONAL WRAPPER ERROR ON ERROR) FROM json_data j;
JSON_QUERY(J.DATA,'$.PEOPLE[0TO0].FIRST'WITHCONDITIONALWRAPPERERRORONERROR)
--------------------------------------------------------------------------------
["James"]
SQL> SELECT JSON_QUERY(j.data, '$.people[0].first' WITH CONDITIONAL WRAPPER ERROR ON ERROR) FROM json_data j;
JSON_QUERY(J.DATA,'$.PEOPLE[0].FIRST'WITHCONDITIONALWRAPPERERRORONERROR)
--------------------------------------------------------------------------------
["James"]
Object Projectionsは、オブジェクトを対象にするプロジェクションです。
{
"ops" :
{
"functionA" :
{
"numArgs" : 2←★
},
"functionB" :
{
"numArgs" : 3←★
},
"functionC" :
{
"variadic" : true
}
}
}
│
│ops.*.numArgs
↓
[
2,
3
]
データを準備します。
SQL> UPDATE json_data SET data ='
2 {
3 "ops": {
4 "functionA": {"numArgs": 2},
5 "functionB": {"numArgs": 3},
6 "functionC": {"variadic": true}
7 }
8 }
9 ';
1 row updated.
ドット表記法では、キー箇所に'*'を指定できないため、Object Projectionができません。
SQL> SELECT j.data.ops.*.numArgs FROM json_data j;
SELECT j.data.ops.*.numArgs FROM json_data j
*
ERROR at line 1:
ORA-01747: invalid user.table.column, table.column, or column specification
JSON_QUERYファンクション+パス式では、キー箇所に'*'を指定でき、Object Projectionを実行できます。
SQL> SELECT JSON_QUERY(j.data, '$.ops.*.numArgs' WITH CONDITIONAL WRAPPER ) FROM json_data j;
JSON_QUERY(J.DATA,'$.OPS.*.NUMARGS'WITHCONDITIONALWRAPPER)
--------------------------------------------------------------------------------
[2,3]
フラット化プロジェクションは、プロジェクション結果が配列の入れ子になっている場合、それをフラットにするプロジェクションです。
Oracle Database JSONでは相当する機能が無いようです。
パイプ相当の処理は、複数回JSON_xxxファンクションを適用することで一応実現できます(読みづらいですが)。
SQL> SELECT JSON_VALUE(JSON_QUERY(j.data, '$.people[*].first' WITH CONDITIONAL WRAPPER ERROR ON ERROR ), '$[0]' ERROR ON ERROR ) FROM json_data j;
JSON_VALUE(JSON_QUERY(J.DATA,'$.PEOPLE[*].FIRST'WITHCONDITIONALWRAPPERERRORONERR
--------------------------------------------------------------------------------
James
JMESPATHのマルチセレクトは、JSON結果から、配列やオブジェクトを生成する処理です。
{
"people": [
{
"name": "a",
"state": {"name": "up"}
},
{
"name": "b",
"state": {"name": "down"}
},
{
"name": "c",
"state": {"name": "up"}
}
]
}
を入力にして、people[].[name, state.name] というパス式を指定することで、以下のように配列の配列が得られます。
│ people[].[name, state.name]
↓
[
[ "a", "up" ],
[ "b", "down" ],
[ "c", "up" ]
]
また、people[].{Name: name, State: state.name} というパス式を指定することで、以下のようにオブジェクトの配列が得られます。
│ people[].{Name: name, State: state.name}
↓
[
{ "Name": "a", "State": "up" },
{ "Name": "b", "State": "down" },
{ "Name": "c", "State": "up" }
]
このようにJMESPATHのマルチセレクトは、JSONの構造(形式)を変換するときに有用です。
また、変換における配列生成、オブジェクト生成の処理をパス式に組み込む形で記述できるため、非常にスマートに指定できます。
なお、Oracle Database JSONでは、JSONデータの生成はファンクションとして提供されており、パス式に組み込むことができないため、
JMESPATHのマルチセレクト類似の処理は実現できません。
ただ、そもそも、Oracle Databaseにおいて、JSON→JSONの変換処理を実現する必要性が高いかというと、微妙な気がします。(言い訳気味ですが)
むしろ、JSON→表の変換処理の方がありそうな気がしませんか?(強引)
というわけで、JSON_TABLEファンクションの紹介を兼ねて、JSON→表の変換処理を行ってみたいと思います。
データを準備します。
SQL> UPDATE json_data SET data ='
2 {
3 "people": [
4 {
5 "name": "a",
6 "state": {"name": "up"}
7 },
8 {
9 "name": "b",
10 "state": {"name": "down"}
11 },
12 {
13 "name": "c",
14 "state": {"name": "up"}
15 }
16 ]
17 }
18 ';
1 row updated.
JSON_TABLEファンクションを使い、表形式でデータを得ます。
JSON_TABLEファンクション内のCOLUMNS句で、得られる表の列について、データ型やパス式を指定しています。
SQL> SELECT x.* FROM json_data j, JSON_TABLE(j.data, '$.people[*]' ERROR ON ERROR
2 COLUMNS (
3 name VARCHAR2(32) PATH '$.name',
4 state VARCHAR2(32) PATH '$.state.name')) x;
NAME STATE
-------------------------------- --------------------------------
a up
b down
c up
それぞれのデータについて型の指定ができるため、ひらたく言うと「お行儀がイイ感じ」で表形式のデータを得られます。
JSON形式で得たデータを、表形式でデータベースで永続する場合に重宝しそうです。
本文で記載した内容と一部重複しますが、最後にOracle Database JSONのTipsをまとめておきます。
.
SQL> SELECT JSON_QUERY(j.data, '$.k[0 to 4]') FROM json_data j;
JSON_QUERY(J.DATA,'$.K[0TO4]')
--------------------------------------------------------------------------------
.
SQL> SELECT JSON_QUERY(j.data, '$.k[0 to 4]' ERROR ON ERROR) FROM json_data j;
SELECT JSON_QUERY(j.data, '$.k[0 to 4]' ERROR ON ERROR) FROM json_data j
*
ERROR at line 1:
ORA-40480: result cannot be returned without array wrapper
設定しないと、ORA-00904が発生します。エラーメッセージがあまり親切でないので要注意です!
SQL> SELECT j.data1.a FROM json_data2 j;
SELECT j.data1.a FROM json_data2 j
*
ERROR at line 1:
ORA-00904: "J"."DATA1"."A": invalid identifier
.
Oracle Database JSONでは、返される値の種類に応じてファンクションを使い分ける必要があります。
具体的には以下のとおりです。
返される値 JSON_xxxファンクション
--------------------------- ------------
スカラー値 JSON_VALUE
JSON(配列、オブジェクト) JSON_QUERY
List Projectionの説明の箇所で取り上げた実行例ですが、JSON_QUERYが複数の値を返す場合、配列にラップしないと、エラーORA-40480が発生します。
SQL> SELECT JSON_QUERY(j.data, '$.people[*].first' ERROR ON ERROR ) FROM json_data j;
SELECT JSON_QUERY(j.data, '$.people[*].first' ERROR ON ERROR ) FROM json_data j
*
ERROR at line 1:
ORA-40480: result cannot be returned without array wrapper
WITH CONDITIONAL WRAPPER などを指定すれば、複数の値をラップした配列が返されます。
SQL> SELECT JSON_QUERY(j.data, '$.people[*].first' WITH CONDITIONAL WRAPPER ERROR ON ERROR ) FROM json_data j;
JSON_QUERY(J.DATA,'$.PEOPLE[*].FIRST'WITHCONDITIONALWRAPPERERRORONERROR)
--------------------------------------------------------------------------------
["James","Jacob","Jayden"]
JSON_QUERYファンクションはスカラー値を返せません。パス式の評価結果が常にスカラー値になるとわかっているのなら、JSON_VALUEを使えばよいのですが、
対象のJSONドキュメントに応じて、評価結果がJSON(の一部)になったり、スカラー値になったりする場合はどうすればよいのでしょうか?
WITH CONDITIONAL WRAPPERを指定しておくと、評価結果がスカラー値になった場合にみ配列でラップしてくれます。なかなか気が利いています。
WITH CONDITIONAL WRAPPERがデフォルトの動作でもよい気がしますが、なにか理由があるのでしょうかねぇ・・・?