ORDSのPMであるPeter O'Brienさんが、以下の紹介記事を書いています。
Multilingual Engine (MLE) for JavaScript Handlers
https://peterobrien.blog/2024/05/07/multilingual-engine-mle-for-javascript-handlers/
Oracle REST Data Services 24.2のドキュメントに、より詳しい説明が追記されました。
Oracle REST Data Services Developer's Guide, Release 24.2
ドキュメントの 2.5.2 Defining the REST Service and JavaScript Handler Using PL/SQL Function に記載されているコードで、ORDS.DEFINE_HANDLERの引数p_source_typeとしてmle/javascriptの指定が可能になり、また、引数p_mle_env_nameとしてMLE環境の割り当てができることがわかります。パッケージORDSのリファレンスの4 ORDS PL/SQL Package Referenceは更新されていないようで、引数p_mle_env_nameが含まれていません。
今の所、ORDSのSQL Developer WebおよびOracle APEXのRESTfulサービスの両方で、ハンドラのタイプにmle/javascriptを選択することはできないようです。そのため、JavaScriptで記述したハンドラを登録するには、ORDS.DEFINE_HANDLERを直接呼び出す必要があります。
今回、動作確認をするにあたって毎回PL/SQLを実行するのも面倒なので、ORDS.DEFINE_HANDLERを呼び出す簡単なAPEXアプリケーションを作ってみました。
https://github.com/ujnak/apexapps/blob/master/exports/ords-mle-handler.zip
ORDS.DEFINE_HANDLERに与える引数に対応したページ・アイテムを作成し、ページに配置しています。
begin
ords.define_handler(
p_module_name => :P1_MODULE_NAME
,p_pattern => :P1_PATTERN
,p_method => :P1_METHOD
,p_source_type => :P1_SOURCE_TYPE
,p_items_per_page => :P1_ITEMS_PER_PAGE
,p_mimes_allowed => :P1_MEMES_ALLOWED
,p_comments => :P1_COMMENTS
,p_mle_env_name => :P1_MLE_ENV_NAME
,p_source => :P1_SOURCE
);
end;
このアプリケーションは、RESTサービスのモジュールとテンプレートを作成する機能を含んでいません。最初に、RESTサービスのモジュールとしてdemojs、テンプレートとしてemployees/:idを作成しておきます。
Oracle APEXのRESTfulサービスからモジュールを開き、モジュールの作成をクリックします。
モジュールの作成をクリックします。
最初に以下のJavaScriptをGETハンドラとして登録します。
先ほど作成したハンドラを、更新可能なJSONドキュメントを出力するように変更します。
モジュールの作成につづけて、テンプレートの作成を行います。
URIテンプレートとしてemployee/:idを指定します。
テンプレートの作成をクリックします。
テンプレートが作成されます。
完全なURLを覚えておきます。今回の動作確認では、作成したORDSハンドラの呼び出しに、Postmanを使用します。
Peter O'brienさんの記事はサンプル・スキーマHRに含まれるemployees表を扱うコードになっていますが、APEXのサンプル・データセットのEMP/DEPTに含まれる表EMPをソースとするように書き換えています。
URLに従業員番号を含め、検索した従業員の従業員番号、名前、給与をJSONで返しています。
(req, resp) => {
const query = 'select empno, ename, sal from emp where empno = :1';
const res = session.execute(query, [req.uri_parameters.id]);
if (res.rows.length > 0) {
var employee_item = {};
employee_item['empno'] = res.rows[0].EMPNO;
employee_item['ename'] = res.rows[0].ENAME;
employee_item['sal'] = res.rows[0].SAL;
resp.content_type('application/json');
resp.json(employee_item);
} else {
resp.status(404);
}
}
Module Nameはdemojs、Patternはemployee/;id、MethodはGETを指定します。その下はデフォルトで、Source Typeはmle/javascript、Items Per Pageは0、Mimes Allowedはapplication/jsonとなっています。
Sourceに上記のコードを記述し、Define Handlerをクリックします。
以上で、JavaScriptで記述したRESTサービスを呼び出せるようになりました。
Postmanから呼び出してみます。
:idに7788を指定してRESTサービスを呼び出すと、JSONのレスポンスが返されます。
上記の例では、サーバー側のJavaScriptハンドラ内でSQLを実行しています。
一般論ですが、JavaScriptが得意な開発者は、それほどSQLが得意ではない場合が多いと思います。Oracle Database 23aiではJSON Relational Duality Viewのサポートにより、表に保存されているデータを、ドキュメントとして扱うことができます。ただし、SODA for In-Database Javascriptの実装はまだ、JSON Relational Duality Viewをサポートしていません。そのため、JSON Relational Duality Viewを、直接MLEのJavaScriptから操作できません。一旦、SQLを仲介する必要があります。
表EMPをドキュメントとして操作するために、JSON Relational Duality Viewのemp_dvを作成します。
以下のSQLを実行します。
create or replace json relational duality view emp_dv as
emp @insert @update @delete
{
_id: empno
ename: ename
job: job
mgr: mgr
hiredate: hiredate
sal: sal
comm: comm
deptno: deptno
}
;
(req, resp) => {
const query = "select json_serialize(data) json from emp_dv where json_value(data, '$._id') = :1";
const res = session.execute(query, [req.uri_parameters.id]);
if (res.rows.length > 0) {
resp.content_type('application/json');
resp.send(res.rows[0].JSON);
} else {
resp.status(404);
}
}
以下のようなデータが取得されます。
{
"_id": 7788,
"_metadata": {
"etag": "DDAF7F36DCF9026756EA13DA52175310",
"asof": "0000256597B50DA8"
},
"ename": "SCOTT",
"job": "ANALYST",
"mgr": 7566,
"hiredate": "1982-12-09T00:00:00",
"sal": 4000,
"comm": 100,
"deptno": 20
}
コードを読むと気が付きますが、JSON形式の従業員のデータを取得する際にjson_serializeを呼び出しています。
select json_serialize(data) json from emp_dv where json_value(data, '$._id') = :1
一見無駄に見えるのです。これを使わないとどうなるか、JSONをそのまま扱うようにハンドラのコードを書き換えてみます。
一見無駄に見えるのです。これを使わないとどうなるか、JSONをそのまま扱うようにハンドラのコードを書き換えてみます。
(req, resp) => {
const query = "select data json from emp_dv where json_value(data, '$._id') = :1";
const res = session.execute(query, [req.uri_parameters.id]);
if (res.rows.length > 0) {
resp.content_type('application/json');
resp.json(res.rows[0].JSON);
} else {
resp.status(404);
}
}
こうすると、ORDSから返されるJSONのデータは以下の形式になります。
{
"_id": 7788,
"_metadata": {
"asof": {
"0": 0,
"1": 0,
"2": 37,
"3": 101,
"4": 151,
"5": 182,
"6": 50,
"7": 36
},
"etag": {
"0": 221,
"1": 175,
"2": 127,
"3": 54,
"4": 220,
"5": 249,
"6": 2,
"7": 103,
"8": 86,
"9": 234,
"10": 19,
"11": 218,
"12": 82,
"13": 23,
"14": 83,
"15": 16
}
},
"comm": 100,
"deptno": 20,
"ename": "SCOTT",
"hiredate": "1982-12-09T00:00:00.000Z",
"job": "ANALYST",
"mgr": 7566,
"sal": 4000
}
この形式で出力されたETAGをアップデート時に含めると、常にETAGが変更されたと認識されるようです。そのため、データの更新ができません。
この状況を回避するため、JSON Relational Duality Viewからのデータの取り出し時に文字列に変換しています。
続いて、レスポンスとして受け取ったJSONを更新し、PUTすることでデータベースの値を更新するPUTハンドラを作成します。
ただし、URIパラメータを含むテンプレート(employee/:idの形式)に登録されたPUTハンドラは、送信されたリクエストを受け取りません。おそらく不具合だと思います。しかし、JSON Relational Duality Viewから取得したデータには必ず_id属性が含まれています。そのため、URLパラメータに:idが含まれていなくても、更新対象となる行は判別できます。
モジュールdemojsに(/:idのない)テンプレートemployeeを追加します。
PUTハンドラを作成します。
Module Nameはdemojs、Patternはemployee、MethodはPUTを指定します。JSON Relational Duality ViewにはJSONオブジェクトではなく、JSON.stringifyを呼び出し文字列を与えます。
Sourceに以下を記述します。
(req, resp) => {
const empObj = req.body;
const id = empObj._id;
const empStr = JSON.stringify(empObj);
try {
const update = "update emp_dv set data = :1 where json_value(data, '$._id') = :2";
const result = session.execute(update, [ empStr, id ]);
let res;
resp.content_type('application/json');
res = { "rows_updated": result.rowsAffected };
resp.json(res);
} catch (e) {
resp.status(422);
resp.json(e);
}
}
データの取得と更新を行うハンドラが作成できました。
最初に従業員番号7788のSCOTTのデータを取得します。
{
"_id": 7788,
"_metadata": {
"etag": "7641E848AFFDEB3DC85EEB8E186F6B8C",
"asof": "000025659760C848"
},
"ename": "SCOTT",
"job": "ANALYST",
"mgr": 7566,
"hiredate": "1982-12-09T00:00:00",
"sal": 3000,
"comm": null,
"deptno": 20
}
salを4000, commを100に変更して、PUTハンドラに送信します。
結果として { "rows_updated" : 1 } が返されます。正常に更新できています。
もう一度同じリクエストを送信すると、エラーが発生します。これは前回のリクエストと同じETAGを送信しているためです。
MLEで動作するJavaScriptのSODAでは、JSON Relational Duality Viewはサポートされていませんが、検索だけであれば呼び出すことができます。
DBMS_SODA.CREATE_DUALV_COLLECTIONを呼び出し、JSON Relational Duality ViewのEMP_DVから、SODAコレクションempColを作成します。
declare
l_col soda_collection_t;
begin
l_col := dbms_soda.create_dualv_collection(
collection_name => 'empCol'
,view_name => 'EMP_DV'
);
end;
作成されたSODAコレクションに対して、filterを呼び出すことでコレクションの検索ができるようになります。
表EMPから従業員のデータを取り出すGETハンドラは、SODAコレクションを使うと以下のように記述できます。SODAコレクションから取得したJSONドキュメントも、ETAGやASOFのデータが16進数の文字列になっていません。そのため、ファンクションconvertToHexを作成し、ASOFとETAGを16進数の文字列に付け替えています。
(req, resp) => {
const id = Number(req.uri_parameters.id);
const convertToHex = (numArr) => {
const len = Object.keys(numArr).length;
const hexArr = new Uint8Array(len);
for (let i = 0; i < len; i++) {
hexArr[i] = numArr[i];
}
return hexStr = Array.from(hexArr, byte => byte.toString(16).padStart(2, '0')).join('').toUpperCase();
};
const db = session.getSodaDatabase();
const col = db.openCollection("empCol");
if ( col === null ) {
throw `'empCol' does not exit`;
}
const emp = col.find().filter({"_id": id}).getOne();
const content = emp.getContent();
content._metadata.asof = convertToHex(content._metadata.asof);
content._metadata.etag = convertToHex(content._metadata.etag);
resp.content_type('application/json');
resp.json(content);
}
今回の記事は以上になります。
もう少し実装の精度が上がれば、MLEのJavaScript+SODA+JSON Relational Duality Viewの組み合わせで、SQLを使わずドキュメントの操作のみでアプリケーションを作成することもできるようになるでしょう。
Oracle APEXのアプリケーション作成の参考になれば幸いです。
完