2021年11月30日火曜日

Oracle APEXよりGraphの機能を呼び出してみる

 Oracle Databaseが持っている機能をAPEXから活用する方法を色々と確認してきましたが、Graphについてはまったく触ったことがなかったので、少し試してみました。

参照しているマニュアルはGraph Developer's Guide for RDF Graphです。

Autonomous Databaseでは制限事項として、マニュアルに以下の記載があります。

  • SEM_MATCH (SPARQL in SQL API), SEM_APIS.UPDATE_MODEL and SEM_APIS.SPARQL_to_SQL are not supported
Autonomous DatabaseでSPARQLを使うにはOracle RDF Serverを導入してください、と案内されています。Oracle APEXからOracle RDF Serverを呼び出す実装というのも、あまり現実的な気がしません。

そのため、以下の作業を試してみました。
  1. Oracle Database 21c XEでグラフのネットワークとモデルを作成し、データを投入する。
  2. SEM_APIS.SPARQL_TO_SQLを実行し、SQLを生成する。
  3. Autonomous Databaseに全く同じグラフのネットワークとモデルを作成し、データを投入する。
  4. XE上で生成したSQLをAutonomous Database上で実行する。
マニュアルに記載されているExample 1-1 Sharing a Network and Granting Query Only Privilege to Another Userに従ってSQLやプロシージャを実行しています。

最終的に、以下のようなアプリケーションを作成しています。


Oracle APEXから呼び出すことを考慮し、network_ownerはAPEXワークスペースのデータベース・ユーザーとし、network_userも同様とします。所有者とユーザーが同じなので、権限関連のプロシージャの呼び出し(sem_apis.grant_network_access_priv, sem_apis.grant_model_access_privs)は省略しています。

APEXのワークスペース・ユーザーはAPEXDEVとします。

以下より確認作業のログを記載します。

手元にインストールしたOracle Database 21c XEで作業を行います。グラフの所有者となるデータベース・ユーザーはAPEXDEVです。

データベースにAPEXDEVで接続し、データベース・ユーザーAPEXDEVのデフォルト表領域を確認します。APEXのワークペースを作成する際に同時にスキーマを作成している場合、通常は新規に表領域が作成され、作成したユーザーのデフォルト表領域になっています。

表領域名を確認したらSEM_APIS.CREATE_SEM_NETWORKを呼び出して、ネットワーク(NET1)を作成します。
begin
    sem_apis.create_sem_network(
        tablespace_name => 'APEXDEVのデフォルト表領域',
        network_owner => 'APEXDEV',
        network_name => 'NET1'
    );
end;
/

SQL> select default_tablespace from user_users;  


DEFAULT_TABLESPACE

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

APEX_2700775222008502


SQL> begin

    sem_apis.create_sem_network(

        tablespace_name => 'APEX_2700775222008502',

        network_owner => 'APEXDEV',

        network_name => 'NET1'

    );

end;

/  2    3    4    5    6    7    8  


PL/SQL procedure successfully completed.


SQL>  


続いて、SEM_APIS.CREATE_SEM_MODELを呼び出してモデル(M1)を作成します。
begin
    sem_apis.create_sem_model(
        model_name => 'M1',
        table_name => null,
        column_name => null,
        network_owner => 'APEXDEV',
        network_name => 'NET1'
    );
end;
/

SQL> begin

    sem_apis.create_sem_model(

        model_name => 'M1',

        table_name => null,

        column_name => null,

        network_owner => 'APEXDEV',

        network_name => 'NET1'

    );

end;

/  2    3    4    5    6    7    8    9   10  


PL/SQL procedure successfully completed.


SQL>


作成したモデルを確認します。net1#sem_model$を検索します。

SQL> set pages 1000 lines 180

SQL> set trims on trimo on

SQL> col owner format a8 

SQL> col model_id format 99999999

SQL> col model_name format a10

SQL> col table_name format a10

SQL> col column_name format a12

SQL> col model_tablespace_name format a30

SQL> col model_type format a10

SQL> col i format a1

SQL> select * from net1#sem_model$;


OWNER   MODEL_ID MODEL_NAME TABLE_NAME COLUMN_NAME  MODEL_TABLESPACE_NAME     MODEL_TYPE I

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

APEXDEV  1 M1       APEX_2700775222008502     M F


SQL> 


データを1行だけインサートした後、コミットします。
insert into net1#rdft_m1(triple) values(sdo_rdf_triple_s('M1','<urn:person1>','<urn:name>','"Peter"','APEXDEV','NET1'));

SQL> insert into net1#rdft_m1(triple) values(sdo_rdf_triple_s('M1','<urn:person1>','<urn:name>','"Peter"','APEXDEV','NET1'));


1 row created.


SQL> commit;


Commit complete.


SQL> 


以上で準備は完了です。

SEM_MATCHを使った検索を行ってみます。optionsの指定については、マニュアルのこちらに記載されています。
select s$rdfterm, p$rdfterm, o$rdfterm
from table(sem_match(
    query => 'SELECT ?s ?p ?o WHERE { ?s ?p ?o }'
    , models => SEM_MODELS('M1')
    , rulebases => null
    , aliases => null
    , filter => null
    , index_status => null 
    , options =>' PLUS_RDFT=VC '
    , graphs => null
    , named_graphs => null
    , network_owner => 'APEXDEV'
    , network_name => 'NET1'
));

SQL> col s$rdfterm format a20

SQL> col p$rdfterm format a20

SQL> col o$rdfterm format a20

SQL> select s$rdfterm, p$rdfterm, o$rdfterm

from table(sem_match(

    query => 'SELECT ?s ?p ?o WHERE { ?s ?p ?o }'

    , models => SEM_MODELS('M1')

    , rulebases => null

    , aliases => null

    , filter => null

    , index_status => null 

    , options =>' PLUS_RDFT=VC '

    , graphs => null

    , named_graphs => null

    , network_owner => 'APEXDEV'

    , network_name => 'NET1'

));  2    3    4    5    6    7    8    9   10   11   12   13   14  


S$RDFTERM     P$RDFTERM   O$RDFTERM

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

<urn:person1>     <urn:name>   "Peter"


SQL>


SEM_MATCH関数はAutonomous Databaseでは実行できないので、代わりに実行可能なSQLを取り出します。queryの文字列を引数としてSEM_APIS.SPARQL_TO_SQLを呼び出します
declare
    sparql_stmt clob;
    sql_stmt    clob;
begin
    sparql_stmt := 'SELECT ?s ?p ?o WHERE { ?s ?p ?o }';
    sql_stmt := 
        sem_apis.sparql_to_sql(
            sparql_query => sparql_stmt
            , models => sem_models('M1')
            , rulebases => null
            , aliases => null
            , index_status => null
            , options => ' PLUS_RDFT=VC '
            , graphs => null
            , named_graphs => null
            , network_owner => 'APEXDEV'
            , network_name => 'NET1'
    );
    dbms_output.put_line(sql_stmt);
end;
/
serveroutputonにし、m1.sqlというファイルにSQLを出力します。

SQL> set serveroutput on

SQL> spool m1.sql

SQL> declare

    sparql_stmt clob;

    sql_stmt    clob;

begin

    sparql_stmt := 'SELECT ?s ?p ?o WHERE { ?s ?p ?o }';

    sql_stmt := 

        sem_apis.sparql_to_sql(

            sparql_query => sparql_stmt

            , models => sem_models('M1')

            , rulebases => null

            , aliases => null

            , index_status => null

            , options => ' PLUS_RDFT=VC '

            , graphs => null

            , named_graphs => null

            , network_owner => 'APEXDEV'

            , network_name => 'NET1'

    );

    dbms_output.put_line(sql_stmt);

end;

/

  2    3    4    5    6    7    8    9   10   11   12   13   14   15   16   17   18   19   20   21  SELECT * FROM (

SELECT S,S$RDFVID,S$_PREFIX,S$_SUFFIX,S$RDFVTYP,S$RDFCLOB,S$RDFLTYP,S$RDFLANG,SEM_APIS.escape_rdf_term(S$RDFTERM, options=>' SPCLCH_ONLY=T

NETWORK_STORAGE_FORM=UNESC ')  AS S$RDFTERM,P,P$RDFVID,P$_PREFIX,P$_SUFFIX,P$RDFVTYP,P$RDFCLOB,P$RDFLTYP,P$RDFLANG,SEM_APIS.escape_rdf_term(P$RDFTERM, options=>' SPCLCH_ONLY=T

NETWORK_STORAGE_FORM=UNESC ')  AS P$RDFTERM,O,O$RDFVID,O$_PREFIX,O$_SUFFIX,O$RDFVTYP,O$RDFCLOB,O$RDFLTYP,O$RDFLANG,SEM_APIS.escape_rdf_term(O$RDFTERM, options=>' SPCLCH_ONLY=T

NETWORK_STORAGE_FORM=UNESC ')  AS O$RDFTERM,SEM$ROWNUM AS SEM$ROWNUM

 FROM (SELECT /*+ NO_MERGE(R) NO_SWAP_JOIN_INPUTS(R) LEADING(R V0 V1 V2) NO_SWAP_JOIN_INPUTS(V0)

NO_SWAP_JOIN_INPUTS(V1) NO_SWAP_JOIN_INPUTS(V2) */ V1.VNAME_PREFIX || V1.VNAME_SUFFIX AS S, V1.VALUE_ID AS S$RDFVID, V1.VNAME_PREFIX AS S$_PREFIX, V1.VNAME_SUFFIX AS S$_SUFFIX,

(CASE WHEN V1.VALUE_TYPE IS NULL THEN NULL WHEN V1.VALUE_TYPE IN ('UR','URI') THEN 'URI'

 WHEN V1.VALUE_TYPE IN ('BN', 'BLN') THEN 'BLN'

 ELSE 'LIT'

END)  AS S$RDFVTYP,

V1.LONG_VALUE AS S$RDFCLOB, V1.LITERAL_TYPE AS S$RDFLTYP, V1.LANGUAGE_TYPE AS S$RDFLANG,

CASE WHEN  (CASE WHEN V1.VALUE_TYPE IS NULL THEN NULL WHEN V1.VALUE_TYPE IN ('UR','URI')

THEN 'URI'

 WHEN V1.VALUE_TYPE IN ('BN', 'BLN') THEN 'BLN'

 ELSE 'LIT'

END)  IS NULL

THEN NULL

WHEN  (CASE WHEN V1.VALUE_TYPE IS NULL THEN NULL WHEN V1.VALUE_TYPE IN ('UR','URI')

THEN 'URI'

 WHEN V1.VALUE_TYPE IN ('BN', 'BLN') THEN 'BLN'

 ELSE 'LIT'

END)  IN ('UR','URI')

THEN '<' || V1.VNAME_PREFIX || V1.VNAME_SUFFIX || '>'

WHEN  (CASE WHEN V1.VALUE_TYPE IS

NULL THEN NULL WHEN V1.VALUE_TYPE IN ('UR','URI') THEN 'URI'

 WHEN V1.VALUE_TYPE IN ('BN', 'BLN') THEN 'BLN'

 ELSE 'LIT'

END)  IN ('BL','BLN','BN')

THEN V1.VNAME_PREFIX ||

V1.VNAME_SUFFIX

ELSE

'"' || REPLACE(V1.VNAME_PREFIX || V1.VNAME_SUFFIX,chr(0)) || '"' || NVL2(V1.LANGUAGE_TYPE,'@' || V1.LANGUAGE_TYPE,'') || NVL2(V1.LITERAL_TYPE,'^^<' ||

V1.LITERAL_TYPE || '>', '')END AS S$RDFTERM,

V0.VNAME_PREFIX || V0.VNAME_SUFFIX AS P, V0.VALUE_ID AS P$RDFVID, V0.VNAME_PREFIX AS P$_PREFIX, V0.VNAME_SUFFIX AS P$_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 P$RDFVTYP, V0.LONG_VALUE AS

P$RDFCLOB, V0.LITERAL_TYPE AS P$RDFLTYP, V0.LANGUAGE_TYPE AS P$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 P$RDFTERM,

V2.VNAME_PREFIX || V2.VNAME_SUFFIX AS O, V2.VALUE_ID AS O$RDFVID, V2.VNAME_PREFIX AS O$_PREFIX, V2.VNAME_SUFFIX AS O$_SUFFIX,  (CASE

WHEN V2.VALUE_TYPE IS NULL THEN NULL WHEN V2.VALUE_TYPE IN ('UR','URI') THEN 'URI'

 WHEN V2.VALUE_TYPE IN ('BN', 'BLN') THEN 'BLN'

 ELSE 'LIT'

END)  AS O$RDFVTYP, V2.LONG_VALUE AS

O$RDFCLOB, V2.LITERAL_TYPE AS O$RDFLTYP, V2.LANGUAGE_TYPE AS O$RDFLANG,

CASE WHEN  (CASE WHEN V2.VALUE_TYPE IS NULL THEN NULL WHEN V2.VALUE_TYPE IN ('UR','URI') THEN 'URI'

 WHEN

V2.VALUE_TYPE IN ('BN', 'BLN') THEN 'BLN'

 ELSE 'LIT'

END)  IS NULL

THEN NULL

WHEN  (CASE WHEN V2.VALUE_TYPE IS NULL THEN NULL WHEN V2.VALUE_TYPE IN ('UR','URI') THEN 'URI'

 WHEN

V2.VALUE_TYPE IN ('BN', 'BLN') THEN 'BLN'

 ELSE 'LIT'

END)  IN ('UR','URI')

THEN '<' || V2.VNAME_PREFIX || V2.VNAME_SUFFIX || '>'

WHEN  (CASE WHEN V2.VALUE_TYPE IS NULL THEN NULL

WHEN V2.VALUE_TYPE IN ('UR','URI') THEN 'URI'

 WHEN V2.VALUE_TYPE IN ('BN', 'BLN') THEN 'BLN'

 ELSE 'LIT'

END)  IN ('BL','BLN','BN')

THEN V2.VNAME_PREFIX ||

V2.VNAME_SUFFIX

ELSE

'"' || REPLACE(V2.VNAME_PREFIX || V2.VNAME_SUFFIX,chr(0)) || '"' || NVL2(V2.LANGUAGE_TYPE,'@' || V2.LANGUAGE_TYPE,'') || NVL2(V2.LITERAL_TYPE,'^^<' ||

V2.LITERAL_TYPE || '>', '')END AS O$RDFTERM,

1 AS SEM$ROWNUM

FROM (SELECT T0.P_VALUE_ID AS P$RDFVID,

T0.START_NODE_ID AS S$RDFVID,

T0.CANON_END_NODE_ID AS

O$RDFVID,

T0.START_NODE_ID AS BGP$1

FROM (

SELECT * FROM "APEXDEV".NET1#RDFM_M1) T0

WHERE (1=1)) R, "APEXDEV".NET1#RDF_VALUE$ V0, "APEXDEV".NET1#RDF_VALUE$ V1,

"APEXDEV".NET1#RDF_VALUE$ V2

WHERE (1=1)  AND (R.P$RDFVID = V0.VALUE_ID) AND (R.S$RDFVID = V1.VALUE_ID) AND (R.O$RDFVID = V2.VALUE_ID)) R

WHERE (1=1)

) WHERE (1=1)


PL/SQL procedure successfully completed.


SQL> 


SPARQLの代わりに実行可能なSQLが得られました。

次にAutonomous Databaseでの作業を行います。

APEXのワークスペースとしてAPEXDEVを作成し、RESTが有効化されていることを前提とします。

データベース・アクションにユーザーAPEXDEVでサインインし、SQLを実行します。実行するSQLやスクリプトはOracle Database 21c XEで実行したものと同じです。

最初にユーザーAPEXDEVのデフォルト表領域を確認します。Autonomous Databaseなので、DATAになっているはずです。


続いてSEM_APIS.CREATE_SEM_NETWORKを実行します。tablespace_nameにはDATAを指定します。


SEM_APIS.CREATE_SEM_MODELを実行し、モデルを作成します。


一行データをインサートします。


SEM_APIS.SPARQL_TO_SQLを実行して得られたSQLを実行します。


Autonomous Database上でグラフのデータが検索できることが確認できました。

Oracle APEXの簡単なアプリケーションを作ってみます。

アプリケーション作成ウィザードを呼び出し、空のアプリケーションを作成します。名前RDF Graphとします。


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

リージョンの作成を行います。識別タイトルグラフデータタイプ対話モード・レポートソース位置ローカル・データベースとし、タイプにはSQL問合せを選択します。

SQL問合せには、SEM_APIS.SPARQL_TO_SQLを呼び出して生成したSQLを入力します。


データを投入するためのフォームを開くボタンを作成します。

ボタンの作成を実行します。識別ボタン名B_CREATEラベル作成とします。レイアウトボタン位置対話モード・レポートの検索バーの右を選択します。動作アクションとして、このアプリケーションのページにリダイレクトを選択します。データベース・アクションとしてSQL INSERT操作を選択します。


ターゲットページとします。キャッシュのクリアにもを指定します。ページ3はこの後に作成します。


フォームのページを作成します。

ページの作成を実行しページ作成ウィザードを起動します。フォームを選択します。


フォームを選択します。


ページ番号ページ名トリプルとします。ページ・モードモーダル・ダイアログを選択します。へ進みます。


モーダル・ダイアログなので、ナビゲーションのプリファレンスこのページとナビゲーション・メニュー・エントリを関連付けないを選択します。へ進みます。


必要なページ・アイテムを生成するために、以下のSELECT文をソースとして設定します。
select
1 as triple_id,
'0123456789012345678901234567890123456789' as subject_name,
'0123456789012345678901234567890123456789' as property_name,
'0123456789012345678901234567890123456789' as object_value
from dual
ソース・タイプSQL問合せを指定し、SQL SELECT文を入力に上記を記述します。


主キー列としてTRIPLE_ID(Number)を選択し、作成をクリックします。


作成されたページより、DELETESAVEのボタンを削除します。CREATEだけが有効な処理になります。


左ペインでプロセス・ビューを開いて、プロセスプロセス・フォームトリプルを選択します。設定ターゲット・タイプPL/SQL Codeとし、以下のINSERT文をPL/SQLコードとして記述します。
insert into net1#rdft_m1(triple)
values(sdo_rdf_triple_s(
    'M1',
    :P3_SUBJECT_NAME,
    :P3_PROPERTY_NAME,
    '"' || :P3_OBJECT_VALUE || '"',
    'APEXDEV',
    'NET1'
));
失われた更新の防止OFF行のロックNoとします。


これでモデルM1にデータを投入するフォームが出来上がりました。

対話モード・レポートのリージョンがあるホーム・ページを開き、ダイアログが閉じたときに対話モード・レポートがリフレッシュされるよう、動的アクションを作成します。

左ペインで動的アクション・ビューを開き、ダイアログのクローズがタイミングとなるよう動的アクションの作成を行います。

作成した動的アクションの識別名前ダイアログのクローズタイミングイベントダイアログのクローズ選択タイプリージョンリージョンとしてグラフデータを選択します。


TRUEアクション識別アクションとして、リフレッシュを選択します。影響を受ける要素選択タイプリージョンリージョンとしてグラフデータを選択します。


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

元々のSPARQLによる問い合わせが、単に保存されたデータをそのまま印刷するもの - SELECT ?s ?p ?o WHERE { ?s ?p ?o } - なので面白味のない結果ですが、Oracle DatabaseのRDF Graphの機能を使ってみるための取り掛かりにはなるかと思います。

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

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

追記

削除処理に使ったコマンド一覧。
begin
    sem_apis.truncate_sem_model(
        model_name => 'M1',
        options => null,
        network_owner => 'APEXDEV',
        network_name => 'NET1'
    );
end;

begin
    sem_apis.drop_sem_model(
        model_name => 'M1'
        , options => null
        , network_owner => 'APEXDEV'
        , network_name => 'NET1'
    );
end;

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