2021年12月2日木曜日

RDFグラフの問合せにバインド変数を使用する

 先日、RDFグラフをAPEXで扱う記事を書いたのですが、基本的すぎて面白みがないと感じたので、RDFグラフの検索にバインド変数を使ってみました。アプリケーションの作成に当たっては、マニュアルRDFナレッジ・グラフ開発者ガイドに記載されている家系の情報を使っています。

以下のような動作をするアプリケーションを作成します。人の名前を入力すると、上の対話レポートにその人の子供を一覧し、下の対話モード・レポートには孫を一覧します。

アプリケーション開発に使用する環境は、以前の記事と同じです。データの準備やSQL の取り出しは手元で動かしているOracle Database 21c XEを使います。APEXアプリケーションはAutonomous Databaseに作成します。

両方の環境ともに、APEXDEVというスキーマで作業を行います。

Oracle Database 21c XEにロードしたグラフのデータは、DataPumpを使ってAutonomous Databaseへ移行します。


データの準備とSQLの取り出し


Oracle Database 21c XEに家系の情報をロードし、APEXアプリケーションに埋め込むSQLの取り出しを行います。

もしネットワークNET1が作成されていたら、削除しておきます。まっさらな環境で作業を行なうことで、エラーが発生する可能性を減らしておきます。

begin
sem_apis.drop_sem_network(
cascade => true
, options => null
, network_owner => 'APEXDEV'
, network_name => 'NET1'
);
end;
/

家系の情報をロードします。以下の作業を行なっています。

  1. SEM_APIS.CREATE_SEM_NETWORKを呼び出し、ネットワークNET1を作成。
  2. SEM_APIS.CRAETE_SEM_MODELを使用してモデルfamilyを作成。
  3. SEM_APIS.UPDATE_MODELを呼び出し、家系のスキーマを定義しデータを投入。
  4. SEM_APIS.CREATE_RULEBASEを呼び出し、grandParentOfのルール(parentOfのparentOf)を定義する。
  5. SEM_APIS.CREATE_ENTAILMENTを呼び出し、伴意(ルール索引)を作成する。
  6. SEM_MATCHを使った検索を実行し、動作の確認をする。

-- Create nework NET1
begin
sem_apis.create_sem_network(
tablespace_name => 'APEX_2700775222008502'
, network_owner => 'APEXDEV'
, network_name => 'NET1'
);
end;
/
-- Create the model.
begin
sem_apis.create_sem_model(
model_name => 'family'
, table_name => null
, column_name => null
, network_owner => 'APEXDEV'
, network_name => 'NET1'
);
end;
/
-- Insert RDF triples using SEM_APIS.UPDATE_MODEL. These express the following information:
BEGIN
-- Insert some TBox (schema) information.
SEM_APIS.UPDATE_MODEL('family',
'PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>
PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
PREFIX family: <http://www.example.org/family/>
INSERT DATA {
# Person is a class.
family:Person rdf:type rdfs:Class .
# Male is a subclass of Person.
family:Male rdfs:subClassOf family:Person .
# Female is a subclass of Person.
family:Female rdfs:subClassOf family:Person .
# siblingOf is a property.
family:siblingOf rdf:type rdf:Property .
# parentOf is a property.
family:parentOf rdf:type rdf:Property .
# brotherOf is a subproperty of siblingOf.
family:brotherOf rdfs:subPropertyOf family:siblingOf .
# sisterOf is a subproperty of siblingOf.
family:sisterOf rdfs:subPropertyOf family:siblingOf .
# A brother is male.
family:brotherOf rdfs:domain family:Male .
# A sister is female.
family:sisterOf rdfs:domain family:Female .
# fatherOf is a subproperty of parentOf.
family:fatherOf rdfs:subPropertyOf family:parentOf .
# motherOf is a subproperty of parentOf.
family:motherOf rdfs:subPropertyOf family:parentOf .
# A father is male.
family:fatherOf rdfs:domain family:Male .
# A mother is female.
family:motherOf rdfs:domain family:Female .
}',
network_owner=>'APEXDEV',
network_name=>'NET1');
-- Insert some ABox (instance) information.
SEM_APIS.UPDATE_MODEL('family',
'PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>
PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
PREFIX family: <http://www.example.org/family/>
INSERT DATA {
# John is the father of Suzie and Matt
family:John family:fatherOf family:Suzie .
family:John family:fatherOf family:Matt .
# Janice is the mother of Suzie and Matt
family:Janice family:motherOf family:Suzie .
family:Janice family:motherOf family:Matt .
# Sammy is the father of Cathy and Jack
family:Sammy family:fatherOf family:Cathy .
family:Sammy family:fatherOf family:Jack .
# Suzie is the mother of Cathy and Jack
family:Suzie family:motherOf family:Cathy .
family:Suzie family:motherOf family:Jack .
# Matt is the father of Tom and Cindy
family:Matt family:fatherOf family:Tom .
family:Matt family:fatherOf family:Cindy .
# Martha is the mother of Tom and Cindy
family:Martha family:motherOf family:Tom .
family:Martha family:motherOf family:Cindy .
# Cathy is the sister of Jack
family:Cathy family:sisterOf family:Jack .
# Jack is male
family:Jack rdf:type family:Male .
# Tom is male.
family:Tom rdf:type family:Male .
# Cindy is female.
family:Cindy rdf:type family:Female .
}',
network_owner=>'APEXDEV',
network_name=>'NET1');
END;
/
-- General inferencing in the family model
begin
sem_apis.create_rulebase(
rulebase_name => 'family_rb'
, options => null
, network_owner => 'APEXDEV'
, network_name => 'NET1'
);
end;
/
-- insert grandparent rule
insert into net1#semr_family_rb
(
rule_name
, antecedents
, filter
, consequents
, aliases
)
values
(
'grandparent_rule'
, '(?x :parentOf ?y) (?y :parentOf ?z)'
, NULL
, '(?x :grandParentOf ?z)'
, SEM_ALIASES(SEM_ALIAS('','http://www.example.org/family/'))
);
commit;
-- Create the entailment.
begin
sem_apis.create_entailment(
index_name_in => 'rdfs_rix_family'
, models_in => SEM_Models('family')
, rulebases_in => SEM_Rulebases('RDFS','family_rb')
, network_owner=>'APEXDEV'
, network_name=>'NET1'
);
end;
/
-- Select all grandfathers and their grandchildren from the family model,
-- without inferencing. (With no inferencing, no results are returned.)
SELECT x$rdfterm grandfather, y$rdfterm grandchild
FROM TABLE(SEM_MATCH(
'PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>
PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
PREFIX : <http://www.example.org/family/>
SELECT ?x ?y
WHERE {?x :grandParentOf ?y . ?x rdf:type :Male}',
SEM_Models('family'),
null, null, null, null,
' PLUS_RDFT=VC ',
null, null,
'APEXDEV', 'NET1'));
-- Select all grandfathers and their grandchildren from the family model.
-- Use inferencing from both the RDFS and family_rb rulebases.
SELECT x$rdfterm grandfather, y$rdfterm grandchild
FROM TABLE(SEM_MATCH(
'PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>
PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
PREFIX : <http://www.example.org/family/>
SELECT ?x ?y
WHERE {?x :grandParentOf ?y . ?x rdf:type :Male}',
SEM_Models('family'),
SEM_Rulebases('RDFS','family_rb'),
null, null, null,
' PLUS_RDFT=VC ',
null, null,
'APEXDEV', 'NET1'));
スクリプトを実行すると最後の検索結果が、以下のように表示されます。最初のSEM_MATCH関数を含むSELECT文は結果を返さないので、2つめのSELECT文の結果のみが表示されます。

GRANDFATHER

--------------------------------------------------------------------------------

GRANDCHILD

--------------------------------------------------------------------------------

<http://www.example.org/family/John>

<http://www.example.org/family/Cathy>


<http://www.example.org/family/John>

<http://www.example.org/family/Jack>


<http://www.example.org/family/John>

<http://www.example.org/family/Tom>


<http://www.example.org/family/John>

<http://www.example.org/family/Cindy>


RDFグラフの準備は完了したのでSQLを取り出します。

子供を検索するSQLを取り出します。
set serveroutput on
および
spool children.sql
などを最初に実行し、出力されるSQLを保存します。オプションにUSE_BIND_VAR=PLSQLを追加します。

DECLARE
sparql_stmt clob;
sql_stmt clob;
cur sys_refcursor;
vid number;
term varchar2(4000);
c_val varchar2(4000);
BEGIN
-- Add a dummy bind clause in the SPARQL statement
sparql_stmt := 'PREFIX : <http://www.example.org/family/>
SELECT ?c WHERE {
BIND("" as ?s)
?s :parentOf ?c }';
-- Get the SQL translation for SPARQL statement
sql_stmt := sem_apis.sparql_to_sql(
sparql_stmt,
sem_models('family'),
SEM_Rulebases('RDFS','family_rb'),
null,
null,' USE_BIND_VAR=PLSQL PLUS_RDFT=VC ', null, null,
'APEXDEV', 'NET1');
dbms_output.put_line(sql_stmt);
-- Execute with <http://www.example.org/family/Martha>
term := '<http://www.example.org/family/Martha>';
vid := sem_apis.res2vid('NET1#RDF_VALUE$',term);
dbms_output.put_line(chr(10)||'?s='||term);
open cur for 'select c$rdfterm from('|| sql_stmt || ')' using vid,term;
loop
fetch cur into c_val;
exit when cur%NOTFOUND;
dbms_output.put_line('|-->?c='||c_val);
end loop;
close cur;
-- Execute with <http://www.example.org/family/Sammy>
term := '<http://www.example.org/family/Sammy>';
vid := sem_apis.res2vid('NET1#RDF_VALUE$',term);
dbms_output.put_line(chr(10)||'?s='||term);
open cur for 'select c$rdfterm from('|| sql_stmt || ')' using vid,term;
loop
fetch cur into c_val;
exit when cur%NOTFOUND;
dbms_output.put_line('|-->?c='||c_val);
end loop;
close cur;
END;
/
最後に以下が出力されます。MarthaとSammyの子供が一覧されています。この部分は確認のための出力なので、SELECT文を取り出す際にファイルから削除します。

?s=<http://www.example.org/family/Martha>

|-->?c=<http://www.example.org/family/Cindy>

|-->?c=<http://www.example.org/family/Tom>


?s=<http://www.example.org/family/Sammy>

|-->?c=<http://www.example.org/family/Jack>

|-->?c=<http://www.example.org/family/Cathy>


出力されたSELECT文にはバインド変数のプレースホルダーとして:0:1が含まれます。これをAPEXで使用するページ・アイテム名に変更します。:0:P1_VID:1は:P1_TERMに置き換えます。

SELECT * FROM (
SELECT
C,C$RDFVID,C$_PREFIX,C$_SUFFIX,C$RDFVTYP,C$RDFCLOB,C$RDFLTYP,C$RDFLANG,SEM_APIS.
escape_rdf_term(C$RDFTERM, options=>' SPCLCH_ONLY=T NETWORK_STORAGE_FORM=UNESC
') AS C$RDFTERM,SEM$ROWNUM AS SEM$ROWNUM
FROM (WITH /*+ MATERIALIZE */
BV0 AS
(SELECT :P1_VID AS VALUE_ID, SDO_RDF_TERM(:P1_TERM) AS TERM FROM DUAL)
SELECT /*+
NO_MERGE(R) NO_SWAP_JOIN_INPUTS(R) LEADING(R V0) NO_SWAP_JOIN_INPUTS(V0) */
V0.VNAME_PREFIX || V0.VNAME_SUFFIX AS C, V0.VALUE_ID AS C$RDFVID,
V0.VNAME_PREFIX AS C$_PREFIX, V0.VNAME_SUFFIX AS C$_SUFFIX, (CASE WHEN
V0.VALUE_TYPE IS NULL THEN NULL WHEN V0.VALUE_TYPE IN ('UR','URI') THEN 'URI'
WHEN V0.VALUE_TYPE IN ('BN', 'BLN') THEN 'BLN'
ELSE 'LIT'
END) AS C$RDFVTYP,
V0.LONG_VALUE AS C$RDFCLOB, V0.LITERAL_TYPE AS C$RDFLTYP, V0.LANGUAGE_TYPE AS
C$RDFLANG,
CASE WHEN (CASE WHEN V0.VALUE_TYPE IS NULL THEN NULL WHEN
V0.VALUE_TYPE IN ('UR','URI') THEN 'URI'
WHEN V0.VALUE_TYPE IN ('BN', 'BLN')
THEN 'BLN'
ELSE 'LIT'
END) IS NULL
THEN NULL
WHEN (CASE WHEN V0.VALUE_TYPE IS
NULL THEN NULL WHEN V0.VALUE_TYPE IN ('UR','URI') THEN 'URI'
WHEN V0.VALUE_TYPE
IN ('BN', 'BLN') THEN 'BLN'
ELSE 'LIT'
END) IN ('UR','URI')
THEN '<' ||
V0.VNAME_PREFIX || V0.VNAME_SUFFIX || '>'
WHEN (CASE WHEN V0.VALUE_TYPE IS NULL
THEN NULL WHEN V0.VALUE_TYPE IN ('UR','URI') THEN 'URI'
WHEN V0.VALUE_TYPE IN
('BN', 'BLN') THEN 'BLN'
ELSE 'LIT'
END) IN ('BL','BLN','BN')
THEN
V0.VNAME_PREFIX || V0.VNAME_SUFFIX
ELSE
'"' || REPLACE(V0.VNAME_PREFIX ||
V0.VNAME_SUFFIX,chr(0)) || '"' || NVL2(V0.LANGUAGE_TYPE,'@' ||
V0.LANGUAGE_TYPE,'') || NVL2(V0.LITERAL_TYPE,'^^<' || V0.LITERAL_TYPE || '>',
'')END AS C$RDFTERM,
1 AS SEM$ROWNUM
FROM (SELECT BV0.VALUE_ID AS
S$RDFVID,
T0.CANON_END_NODE_ID AS C$RDFVID,
T0.START_NODE_ID AS BGP$1
FROM
(
SELECT * FROM (
SELECT * FROM "APEXDEV".NET1#RDFM_FAMILY
UNION ALL
SELECT *
FROM "APEXDEV".NET1#RDFI_RDFS_RIX_FAMILY
)) T0, BV0 BV0
WHERE T0.START_NODE_ID =
BV0.VALUE_ID AND
T0.P_VALUE_ID = 7703787944492226018) R,
"APEXDEV".NET1#RDF_VALUE$ V0
WHERE (1=1) AND (R.C$RDFVID = V0.VALUE_ID))
R
WHERE (1=1)
) WHERE (1=1) AND 1 NOT IN (SELECT * FROM
"APEXDEV".NET1#RDF_RI_SHAD_2$)
view raw children.sql hosted with ❤ by GitHub

同様に孫を検索するSQLを取り出します。

DECLARE
sparql_stmt clob;
sql_stmt clob;
cur sys_refcursor;
vid number;
term varchar2(4000);
c_val varchar2(4000);
BEGIN
-- Add a dummy bind clause in the SPARQL statement
sparql_stmt := 'PREFIX : <http://www.example.org/family/>
SELECT ?c WHERE {
BIND("" as ?s)
?s :grandParentOf ?c }';
-- Get the SQL translation for SPARQL statement
sql_stmt := sem_apis.sparql_to_sql(
sparql_stmt,
sem_models('family'),
SEM_Rulebases('RDFS','family_rb'),
null,
null,' USE_BIND_VAR=PLSQL PLUS_RDFT=VC ', null, null,
'APEXDEV', 'NET1');
dbms_output.put_line(sql_stmt);
-- Execute with <http://www.example.org/family/John>
term := '<http://www.example.org/family/John>';
vid := sem_apis.res2vid('NET1#RDF_VALUE$',term);
dbms_output.put_line(chr(10)||'?s='||term);
open cur for 'select c$rdfterm from('|| sql_stmt || ')' using vid,term;
loop
fetch cur into c_val;
exit when cur%NOTFOUND;
dbms_output.put_line('|-->?c='||c_val);
end loop;
close cur;
END;
/

最後に以下が出力されます。Johnの孫が一覧されています。SELECT文を取り出すにあたって削除します。

?s=<http://www.example.org/family/John>

|-->?c=<http://www.example.org/family/Cindy>

|-->?c=<http://www.example.org/family/Jack>

|-->?c=<http://www.example.org/family/Cathy>

|-->?c=<http://www.example.org/family/Tom>


出力されたSELECT文のバインド変数の:0:P1_VID:1:P1_TERMに置き換えます。

SELECT * FROM (
SELECT
C,C$RDFVID,C$_PREFIX,C$_SUFFIX,C$RDFVTYP,C$RDFCLOB,C$RDFLTYP,C$RDFLANG,SEM_APIS.
escape_rdf_term(C$RDFTERM, options=>' SPCLCH_ONLY=T NETWORK_STORAGE_FORM=UNESC
') AS C$RDFTERM,SEM$ROWNUM AS SEM$ROWNUM
FROM (WITH /*+ MATERIALIZE */
BV0 AS
(SELECT :P1_VID AS VALUE_ID, SDO_RDF_TERM(:P1_TERM) AS TERM FROM DUAL)
SELECT /*+
NO_MERGE(R) NO_SWAP_JOIN_INPUTS(R) LEADING(R V0) NO_SWAP_JOIN_INPUTS(V0) */
V0.VNAME_PREFIX || V0.VNAME_SUFFIX AS C, V0.VALUE_ID AS C$RDFVID,
V0.VNAME_PREFIX AS C$_PREFIX, V0.VNAME_SUFFIX AS C$_SUFFIX, (CASE WHEN
V0.VALUE_TYPE IS NULL THEN NULL WHEN V0.VALUE_TYPE IN ('UR','URI') THEN 'URI'
WHEN V0.VALUE_TYPE IN ('BN', 'BLN') THEN 'BLN'
ELSE 'LIT'
END) AS C$RDFVTYP,
V0.LONG_VALUE AS C$RDFCLOB, V0.LITERAL_TYPE AS C$RDFLTYP, V0.LANGUAGE_TYPE AS
C$RDFLANG,
CASE WHEN (CASE WHEN V0.VALUE_TYPE IS NULL THEN NULL WHEN
V0.VALUE_TYPE IN ('UR','URI') THEN 'URI'
WHEN V0.VALUE_TYPE IN ('BN', 'BLN')
THEN 'BLN'
ELSE 'LIT'
END) IS NULL
THEN NULL
WHEN (CASE WHEN V0.VALUE_TYPE IS
NULL THEN NULL WHEN V0.VALUE_TYPE IN ('UR','URI') THEN 'URI'
WHEN V0.VALUE_TYPE
IN ('BN', 'BLN') THEN 'BLN'
ELSE 'LIT'
END) IN ('UR','URI')
THEN '<' ||
V0.VNAME_PREFIX || V0.VNAME_SUFFIX || '>'
WHEN (CASE WHEN V0.VALUE_TYPE IS NULL
THEN NULL WHEN V0.VALUE_TYPE IN ('UR','URI') THEN 'URI'
WHEN V0.VALUE_TYPE IN
('BN', 'BLN') THEN 'BLN'
ELSE 'LIT'
END) IN ('BL','BLN','BN')
THEN
V0.VNAME_PREFIX || V0.VNAME_SUFFIX
ELSE
'"' || REPLACE(V0.VNAME_PREFIX ||
V0.VNAME_SUFFIX,chr(0)) || '"' || NVL2(V0.LANGUAGE_TYPE,'@' ||
V0.LANGUAGE_TYPE,'') || NVL2(V0.LITERAL_TYPE,'^^<' || V0.LITERAL_TYPE || '>',
'')END AS C$RDFTERM,
1 AS SEM$ROWNUM
FROM (SELECT BV0.VALUE_ID AS
S$RDFVID,
T0.CANON_END_NODE_ID AS C$RDFVID,
T0.START_NODE_ID AS BGP$1
FROM
(
SELECT * FROM (
SELECT * FROM "APEXDEV".NET1#RDFM_FAMILY
UNION ALL
SELECT *
FROM "APEXDEV".NET1#RDFI_RDFS_RIX_FAMILY
)) T0, BV0 BV0
WHERE T0.START_NODE_ID =
BV0.VALUE_ID AND
T0.P_VALUE_ID = 8440289324123914894) R,
"APEXDEV".NET1#RDF_VALUE$ V0
WHERE (1=1) AND (R.C$RDFVID = V0.VALUE_ID))
R
WHERE (1=1)
) WHERE (1=1) AND 1 NOT IN (SELECT * FROM
"APEXDEV".NET1#RDF_RI_SHAD_2$)

これで対話モード・レポートのソースとなるSELECT文の準備ができました。


データの移行


Oracle Database 21c XEからAutonomous Databaseでデータを移行します。

expdpの実行をユーザーAPEXDEVで行なうため、APEXDEVに必要な権限を与えます。SYS AS SYSDBAで接続し、以下を実行します。ディレクトリの作成権限をAPEXDEVに与えます。

SQL> grant create any directory to apexdev;


Grant succeeded.


SQL> 


ユーザーAPEXDEVで接続し、ディレクトリdump_dirを作成します。

SQL> create directory dump_dir as '/home/oracle';


Directory created.


SQL> 


expdpを実行します。ダンプ・ファイルは/home/oracle/rdf.dmpとして出力します。

expdp apexdev/********@localhost/xepdb1 directory=dump_dir dumpfile=rdf.dmp version=19

$ expdp apexdev/********@localhost/xepdb1 directory=dump_dir dumpfile=rdf.dmp version=19


Export: Release 21.0.0.0.0 - Production on Thu Dec 2 14:50:38 2021

Version 21.3.0.0.0


Copyright (c) 1982, 2021, Oracle and/or its affiliates.  All rights reserved.


Connected to: Oracle Database 21c Express Edition Release 21.0.0.0.0 - Production

Starting "APEXDEV"."SYS_EXPORT_SCHEMA_01":  apexdev/********@localhost/xepdb1 directory=dump_dir dumpfile=rdf.dmp version=19 

Processing object type SCHEMA_EXPORT/TABLE/TABLE_DATA

Processing object type SCHEMA_EXPORT/TABLE/INDEX/STATISTICS/INDEX_STATISTICS

Processing object type SCHEMA_EXPORT/TABLE/INDEX/STATISTICS/FUNCTIONAL_INDEX/INDEX_STATISTICS

Processing object type SCHEMA_EXPORT/TABLE/STATISTICS/TABLE_STATISTICS

Processing object type SCHEMA_EXPORT/STATISTICS/MARKER

Processing object type SCHEMA_EXPORT/PRE_SCHEMA/PROCACT_SCHEMA

Processing object type SCHEMA_EXPORT/SEQUENCE/SEQUENCE

Processing object type SCHEMA_EXPORT/TABLE/TABLE

Processing object type SCHEMA_EXPORT/TABLE/GRANT/OWNER_GRANT/OBJECT_GRANT

Processing object type SCHEMA_EXPORT/TABLE/COMMENT

Processing object type SCHEMA_EXPORT/PROCEDURE/PROCEDURE

Processing object type SCHEMA_EXPORT/PROCEDURE/GRANT/OWNER_GRANT/OBJECT_GRANT

Processing object type SCHEMA_EXPORT/PROCEDURE/ALTER_PROCEDURE

Processing object type SCHEMA_EXPORT/VIEW/VIEW

Processing object type SCHEMA_EXPORT/VIEW/GRANT/OWNER_GRANT/OBJECT_GRANT

Processing object type SCHEMA_EXPORT/TABLE/INDEX/INDEX

Processing object type SCHEMA_EXPORT/TABLE/INDEX/FUNCTIONAL_INDEX/INDEX

Processing object type SCHEMA_EXPORT/TABLE/CONSTRAINT/CONSTRAINT

Processing object type SCHEMA_EXPORT/TABLE/CONSTRAINT/REF_CONSTRAINT

Processing object type SCHEMA_EXPORT/VIEW/TRIGGER

. . exported "APEXDEV"."NET1#RDF_LINK$":"MODEL_2"        23.95 KB     219 rows

. . exported "APEXDEV"."NET1#RDF_VALUE$"                 23.67 KB      94 rows

. . exported "APEXDEV"."NET1#RDF_RULEBASE$"              6.585 KB       9 rows

. . exported "APEXDEV"."NET1#RDF_CLIQUE$":"MODEL_0"          0 KB       0 rows

. . exported "APEXDEV"."NET1#RDF_COLLISION$"             5.070 KB       1 rows

. . exported "APEXDEV"."NET1#RDF_CRS_URI$"               293.2 KB    5688 rows

. . exported "APEXDEV"."NET1#RDF_DELTA$":"MODEL_0"           0 KB       0 rows

. . exported "APEXDEV"."NET1#RDF_GRANT_INFO$"                0 KB       0 rows

. . exported "APEXDEV"."NET1#RDF_HIST$"                  7.328 KB       1 rows

. . exported "APEXDEV"."NET1#RDF_LINK$":"MODEL_0"            0 KB       0 rows

. . exported "APEXDEV"."NET1#RDF_LINK$":"MODEL_1"        12.67 KB      29 rows

. . exported "APEXDEV"."NET1#RDF_MODEL$_TBL"             8.109 KB       1 rows

. . exported "APEXDEV"."NET1#RDF_MODEL_INTERNAL$"        8.093 KB       1 rows

. . exported "APEXDEV"."NET1#RDF_NAMESPACE$"                 0 KB       0 rows

. . exported "APEXDEV"."NET1#RDF_NETWORK_INDEX_INTERNAL$"  14.77 KB      12 rows

. . exported "APEXDEV"."NET1#RDF_PARAMETER"              6.492 KB       2 rows

. . exported "APEXDEV"."NET1#RDF_PRECOMP$"               6.828 KB       1 rows

. . exported "APEXDEV"."NET1#RDF_PRECOMP_DEP$"           5.968 KB       3 rows

. . exported "APEXDEV"."NET1#RDF_PRED_STATS$":"MODEL_0"      0 KB       0 rows

. . exported "APEXDEV"."NET1#RDF_PRED_STATS$":"MODEL_1"      0 KB       0 rows

. . exported "APEXDEV"."NET1#RDF_RI_SHAD_2$"                 0 KB       0 rows

. . exported "APEXDEV"."NET1#RDF_RULE$"                  19.73 KB      20 rows

. . exported "APEXDEV"."NET1#RDF_SYSTEM_EVENT$"              0 KB       0 rows

. . exported "APEXDEV"."NET1#RDF_TERM_STATS$":"MODEL_0"      0 KB       0 rows

. . exported "APEXDEV"."NET1#RDF_TERM_STATS$":"MODEL_1"      0 KB       0 rows

. . exported "APEXDEV"."NET1#RDF_TS$"                        0 KB       0 rows

. . exported "APEXDEV"."NET1#RENAMED_APPTAB_RDF_MODEL_ID_1"      0 KB       0 rows

. . exported "APEXDEV"."NET1#SEM_INDEXTYPE_METADATA$"        0 KB       0 rows

Master table "APEXDEV"."SYS_EXPORT_SCHEMA_01" successfully loaded/unloaded

******************************************************************************

Dump file set for APEXDEV.SYS_EXPORT_SCHEMA_01 is:

  /home/oracle/rdf.dmp

Job "APEXDEV"."SYS_EXPORT_SCHEMA_01" successfully completed at Thu Dec 2 14:51:29 2021 elapsed 0 00:00:49


$ 


出力されたダンプ・ファイルをオブジェクト・ストレージにアップロードします。

適当なバケットを開き、アップロードをクリックします。


ドロワーが開くので、ファイルとしてrdf.dmpを選択しアップロードをクリックします。ファイルのアップロードが完了したら、ドロワーを閉じます


オブジェクトとしてアップロードしたrdf.dmpが一覧されます。右端のハンバーガー・メニューを開き、事前承認済リクエストの作成を実行します。


有効期限を短期間(1日など)に設定し直し、事前承認済リクエストの作成を実行します。


事前承認済リクエストのURLが表示されるので、コピーしてどこかに保存しておきます。Autonomous Database側からは、このURLよりダンプ・ファイルを取得します。


URLのコピーができたら、ダイアログを閉じます。

出力されたダンプ・ファイルをインポートします。

最初にAutonomous Database側でもグラフのデータを削除しておきます。

SQLワークショプSQLコマンドより、SEM_APIS.DROP_SEM_NETWORKを実行します。これはOracle Database 21c XEで実行したコマンドと全く同じです。


Autonomous Databaseのデータベース・アクションに、管理者であるADMINで接続します。開発SQLを開きます。

オブジェクト・ストレージにあるダンプ・ファイルをディレクトリDATA_PUMP_DIRへダウンロードします。DBMS_CLOUD.GET_OBJECTを呼び出します。object_uriとして、先ほどコピーした事前承認済のURLを指定します。
-- 入れ替える場合は、すでにあるファイルを削除する。
begin
    dbms_cloud.delete_file(
        directory_name => 'DATA_PUMP_DIR'
        , file_name => 'rdf.dmp');
end;
/

begin
    dbms_cloud.get_object(
        object_uri => 'https://objectstorage.ap-tokyo-1.oraclecloud.com/p/事前承認済のURL/o/rdf.dmp',
        directory_name => 'DATA_PUMP_DIR'
    );
end;

DataPump APIを使って、インポート処理を行います。以前に記載したDataPump APIの紹介記事をもとに、以下のスクリプトを実行します。

DECLARE
ind NUMBER; -- Loop index
h1 NUMBER; -- Data Pump job handle
percent_done NUMBER; -- Percentage of job complete
job_state VARCHAR2(30); -- To keep track of job state
le ku$_LogEntry; -- For WIP and error messages
js ku$_JobStatus; -- The job status from get_status
jd ku$_JobDesc; -- The job description from get_status
sts ku$_Status; -- The status object returned by get_status
BEGIN
-- Create a (user-named) Data Pump job to do a "full" import (everything
-- in the dump file without filtering).
h1 := DBMS_DATAPUMP.OPEN(
operation => 'IMPORT',
job_mode => 'FULL',
remote_link => NULL,
job_name => 'IMP_APEXDEV'
);
-- Specify the single dump file for the job (using the handle just returned)
-- and directory object, which must already be defined and accessible
-- to the user running this procedure. This is the dump file created by
-- the export operation in the first example.
DBMS_DATAPUMP.ADD_FILE(
handle => h1,
filename => 'rdf.dmp',
directory => 'DATA_PUMP_DIR'
);
-- A metadata remap will map all schema objects from HR to BLAKE.
-- DBMS_DATAPUMP.METADATA_REMAP(
-- handle => h1,
-- name => 'REMAP_SCHEMA',
-- old_value => 'APEXDEV',
-- value => 'APEXDEV'
-- );
-- If a table already exists in the destination schema, skip it (leave
-- the preexisting table alone). This is the default, but it does not hurt
-- to specify it explicitly.
DBMS_DATAPUMP.SET_PARAMETER(
handle => h1,
name => 'TABLE_EXISTS_ACTION',
value => 'SKIP'
);
-- Start the job. An exception is returned if something is not set up properly.
DBMS_DATAPUMP.START_JOB(h1);
-- The import job should now be running. In the following loop, the job is
-- monitored until it completes. In the meantime, progress information is
-- displayed. Note: this is identical to the export example.
percent_done := 0;
job_state := 'UNDEFINED';
while (job_state != 'COMPLETED') and (job_state != 'STOPPED') loop
dbms_datapump.get_status(h1,
dbms_datapump.ku$_status_job_error +
dbms_datapump.ku$_status_job_status +
dbms_datapump.ku$_status_wip,-1,job_state,sts);
js := sts.job_status;
-- If the percentage done changed, display the new value.
if js.percent_done != percent_done
then
dbms_output.put_line('*** Job percent done = ' ||
to_char(js.percent_done));
percent_done := js.percent_done;
end if;
-- If any work-in-progress (WIP) or Error messages were received for the job,
-- display them.
if (bitand(sts.mask,dbms_datapump.ku$_status_wip) != 0)
then
le := sts.wip;
else
if (bitand(sts.mask,dbms_datapump.ku$_status_job_error) != 0)
then
le := sts.error;
else
le := null;
end if;
end if;
if le is not null
then
ind := le.FIRST;
while ind is not null loop
dbms_output.put_line(le(ind).LogText);
ind := le.NEXT(ind);
end loop;
end if;
end loop;
-- Indicate that the job finished and gracefully detach from it.
dbms_output.put_line('Job has completed');
dbms_output.put_line('Final job state = ' || job_state);
dbms_datapump.detach(h1);
END;
/
view raw import.sql hosted with ❤ by GitHub



実行結果は以下になりました。いくつかエラーや警告が出ていますが、これから行なう作業には影響はありませんでした。
Master table "ADMIN"."IMP_APEXDEV" successfully loaded/unloaded
Starting "ADMIN"."IMP_APEXDEV":  
Processing object type SCHEMA_EXPORT/PRE_SCHEMA/PROCACT_SCHEMA
Processing object type SCHEMA_EXPORT/SEQUENCE/SEQUENCE
Processing object type SCHEMA_EXPORT/TABLE/TABLE
Table "APEXDEV"."NET1#RENAMED_APPTAB_RDF_MODEL_ID_1" exists. All dependent
metadata and data will be skipped due to table_exists_action of skip
Processing object type SCHEMA_EXPORT/TABLE/TABLE_DATA
. . imported "APEXDEV"."NET1#RDF_LINK$":"MODEL_2"        23.95 KB     219 rows
. . imported "APEXDEV"."NET1#RDF_VALUE$"                 23.67 KB      94 rows
*** Job percent done = 32
. . imported "APEXDEV"."NET1#RDF_RULEBASE$"              6.585 KB       9 rows
. . imported "APEXDEV"."NET1#RDF_CLIQUE$":"MODEL_0"          0 KB       0 rows
. . imported "APEXDEV"."NET1#RDF_COLLISION$"             5.070 KB       1 rows
*** Job percent done = 99
. . imported "APEXDEV"."NET1#RDF_CRS_URI$"               293.2 KB    5688 rows
. . imported "APEXDEV"."NET1#RDF_DELTA$":"MODEL_0"           0 KB       0 rows
. . imported "APEXDEV"."NET1#RDF_GRANT_INFO$"                0 KB       0 rows
. . imported "APEXDEV"."NET1#RDF_HIST$"                  7.328 KB       1 rows
. . imported "APEXDEV"."NET1#RDF_LINK$":"MODEL_0"            0 KB       0 rows
. . imported "APEXDEV"."NET1#RDF_LINK$":"MODEL_1"        12.67 KB      29 rows
. . imported "APEXDEV"."NET1#RDF_MODEL$_TBL"             8.109 KB       1 rows
. . imported "APEXDEV"."NET1#RDF_MODEL_INTERNAL$"        8.093 KB       1 rows
. . imported "APEXDEV"."NET1#RDF_NAMESPACE$"                 0 KB       0 rows
. . imported "APEXDEV"."NET1#RDF_NETWORK_INDEX_INTERNAL$"  14.77 KB      12 rows
. . imported "APEXDEV"."NET1#RDF_PARAMETER"              6.492 KB       2 rows
. . imported "APEXDEV"."NET1#RDF_PRECOMP$"               6.828 KB       1 rows
. . imported "APEXDEV"."NET1#RDF_PRECOMP_DEP$"           5.968 KB       3 rows
. . imported "APEXDEV"."NET1#RDF_PRED_STATS$":"MODEL_0"      0 KB       0 rows
. . imported "APEXDEV"."NET1#RDF_PRED_STATS$":"MODEL_1"      0 KB       0 rows
. . imported "APEXDEV"."NET1#RDF_RI_SHAD_2$"                 0 KB       0 rows
. . imported "APEXDEV"."NET1#RDF_RULE$"                  19.73 KB      20 rows
. . imported "APEXDEV"."NET1#RDF_SYSTEM_EVENT$"              0 KB       0 rows
. . imported "APEXDEV"."NET1#RDF_TERM_STATS$":"MODEL_0"      0 KB       0 rows
. . imported "APEXDEV"."NET1#RDF_TERM_STATS$":"MODEL_1"      0 KB       0 rows
. . imported "APEXDEV"."NET1#RDF_TS$"                        0 KB       0 rows
. . imported "APEXDEV"."NET1#SEM_INDEXTYPE_METADATA$"        0 KB       0 rows
Processing object type SCHEMA_EXPORT/TABLE/GRANT/OWNER_GRANT/OBJECT_GRANT
Processing object type SCHEMA_EXPORT/PROCEDURE/PROCEDURE
ORA-31684: Object type PROCEDURE:"APEXDEV"."NET1#SDO_RDF_PROC_DR_TRUNC_1"
already exists

Processing object type SCHEMA_EXPORT/PROCEDURE/GRANT/OWNER_GRANT/OBJECT_GRANT
Processing object type SCHEMA_EXPORT/PROCEDURE/ALTER_PROCEDURE
ORA-39111: Dependent object type
ALTER_PROCEDURE:"APEXDEV"."NET1#SDO_RDF_PROC_DR_TRUNC_1" skipped, base object
type PROCEDURE:"APEXDEV"."NET1#SDO_RDF_PROC_DR_TRUNC_1" already exists

Processing object type SCHEMA_EXPORT/VIEW/VIEW
ORA-31684: Object type VIEW:"APEXDEV"."FAMILY_RDF_DATA" already exists

Processing object type SCHEMA_EXPORT/VIEW/GRANT/OWNER_GRANT/OBJECT_GRANT
Processing object type SCHEMA_EXPORT/TABLE/INDEX/INDEX
Processing object type SCHEMA_EXPORT/TABLE/INDEX/FUNCTIONAL_INDEX/INDEX
Processing object type SCHEMA_EXPORT/TABLE/CONSTRAINT/CONSTRAINT
Processing object type SCHEMA_EXPORT/TABLE/INDEX/STATISTICS/INDEX_STATISTICS
Processing object type
SCHEMA_EXPORT/TABLE/INDEX/STATISTICS/FUNCTIONAL_INDEX/INDEX_STATISTICS
Processing object type SCHEMA_EXPORT/TABLE/CONSTRAINT/REF_CONSTRAINT
Processing object type SCHEMA_EXPORT/VIEW/TRIGGER
Processing object type SCHEMA_EXPORT/TABLE/STATISTICS/TABLE_STATISTICS
Processing object type SCHEMA_EXPORT/STATISTICS/MARKER
ORA-39082: Object type TRIGGER:"APEXDEV"."NET1#SDO_RDF_TRIG_AVIEW_1" created
with compilation warnings

*** Job percent done = 100
Job "ADMIN"."IMP_APEXDEV" completed with 4 error(s) at Thu Dec 2 06:54:12 2021
elapsed 0 00:00:25
Job has completed
Final job state = COMPLETED


PL/SQL procedure successfully completed.

Elapsed: 00:00:31.075

以上でRDFのデータのインポートが完了しました。


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


人の名前を与えて、その人の子供と孫を一覧するAPEXアプリケーションを作成します。

アプリケーション作成ウィザードを実行し、空のアプリケーションを作成します。名前バインド変数を使ったRDF問合せとします。アプリケーションの作成をクリックします。


アプリケーションが作成されたら、ページ・デザイナホーム・ページを開きます。

リージョンの作成を行います。識別名前Childrenタイプ対話モード・レポートを選択します。ソース位置ローカル・データベースタイプSQL問合せを選択します。SQL問合せには、Oracle Database 21c XEで生成した子供を一覧するSQLを設定します。


ページ・アイテムの作成を行います。識別名前P1_PERSONタイプテキスト・フィールドとします。ラベル人名とします。


RDFグラフを検索するSELECT文に与える値を保持するページ・アイテムP1_VIDP1_TERMを作成します。これらの値はP1_PERSONから導出されます。タイプ非表示にし、設置保護された値OFFにします。


対話モード・レポートChildrenを選択し、送信するページ・アイテムとしてP1_VIDP1_TERMを指定します。


もう一つ対話モード・レポートのリージョンを作成します。識別名前Grandchildrenタイプ対話モード・レポートとし、SQL問合せとして、孫を一覧するSQLを設定します。送信するページ・アイテムとしてP1_VIDP1_TERMを指定します。


ページ・アイテムP1_PERSONに値が設定されたら、それを元にP1_VIDとP1_TERMを設定する動的アクションを作成します。

ページ・アイテムP1_PERSON上で動的アイテムの作成を実行します。識別名前検索条件の導出とします。タイミングはデフォルトで、イベント変更選択タイプアイテムアイテムP1_PERSONになります。P1_PERSONが変更されたときに、アクションが実行されます。


TRUEアクション識別アクションとして、サーバー側のコードを実行を選びます。設定言語としてPL/SQLを選択し、PL/SQLコードとして以下を記述します。

declare
term varchar2(4000);
vid number;
begin
term := '';
vid := sem_apis.res2vid('NET1#RDF_VALUE$', term);
:P1_TERM := term;
:P1_VID := vid;
end;
view raw bindvar.sql hosted with ❤ by GitHub

送信するアイテムとしてP1_PERSON戻すアイテムとしてP1_VIDP1_TERMを指定します。


バインド変数に与える値がP1_VID、P1_TERMに設定されたので、対話モード・レポートのリージョンChildren、Grandchildrenをリフレッシュします。

TRUEアクションを作成し、識別アクションリフレッシュ影響を受ける要素タイプリージョンリージョンとしてChildrenを選択します。もうひとつ同じTRUEアクションを作成し、そちらはリージョンとしてGrandchildrenを選択します。


以上でアプリケーションは完成です。実行すると最初のGIF画面のように動作します。

今回作成したアプリケーションのエクスポートを以下に置きました。
https://github.com/ujnak/apexapps/blob/master/exports/rdf-query-with-bind-variable.sql

Oracle RDF Graph Serverを実装すると、RDFを扱う一連の操作をREST API経由で呼び出すことができるようです。Autonomous Database単体では実行できなかったSPARQLも実行できます。Oracle RDF Graph Serverについてのマニュアルの記載はこちらです。SPARQL問合せキャッシュを参照すると、変換されたSQLにもアクセスできそうです。

グラフとして扱うことが自然なデータであれば、グラフとして扱う方がローコードのスタイルに合うように思います。幸いオラクル・データベースではグラフを扱うことができます。とはいえ、グラフの勉強は必要になります。

以上になります。

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

追記
  1. DataPumpを使ったエクスポート/インポートを行う際に、ローカルのデータベースとAutonomous Databaseでバージョンを合わせていませんでした。ローカルが21c、Autonomous Databaseは19cです。しかし、19cのExpress Editionはリリースされていないので、21cか18cかの選択になります。ライセンスを購入している場合は19cを使うと良いかと思います。MDSYS.SDO_RDFパッケージに含まれるプロシージャに違いがあるようです。
  2. やはりDataPumpでのデータ移行は強引なので(当然、サポートもされない)、Autonomous Databaseではユーザーがルールベースを追加することはできない前提でRDFナレッジ・グラフを使うのが妥当と言えます。