2024年7月1日月曜日

ORDSのハンドラをMLEのJavaScriptで記述する

ORDS 24.1.1より、ORDSのRESTサービスをJavaScriptで記述できるようになりました。Oracle Database 23aiのMLEつまりGraalVMで実行するため、データベースは23aiである必要があります。

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に与える引数に対応したページ・アイテムを作成し、ページに配置しています。


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サービスからモジュールを開き、モジュールの作成をクリックします。


モジュール名demojsベース・パス/demojs/とします。

モジュールの作成をクリックします。


モジュールの作成につづけて、テンプレートの作成を行います。


URIテンプレートとしてemployee/:idを指定します。

テンプレートの作成をクリックします。


テンプレートが作成されます。

完全なURLを覚えておきます。今回の動作確認では、作成したORDSハンドラの呼び出しに、Postmanを使用します。


最初に以下のJavaScriptをGETハンドラとして登録します。

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 NamedemojsPatternemployee/;idMethodGETを指定します。その下はデフォルトで、Source Typemle/javascriptItems Per Page0Mimes Allowedapplication/jsonとなっています。

Sourceに上記のコードを記述し、Define Handlerをクリックします。


以上で、JavaScriptで記述したRESTサービスを呼び出せるようになりました。

Postmanから呼び出してみます。

:id7788を指定して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
    }
;

先ほど作成したハンドラを、更新可能なJSONドキュメントを出力するように変更します。
(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);
    }
}

Postmanから呼び出し、従業員番号7788のデータをJSONドキュメントとして取得します。


以下のようなデータが取得されます。
{
    "_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をそのまま扱うようにハンドラのコードを書き換えてみます。

(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 NamedemojsPatternemployeeMethodPUTを指定します。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のデータを取得します。


SCOTTのデータがJSONドキュメントとして返されます。
{
    "_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
}
sal4000, comm100に変更して、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のアプリケーション作成の参考になれば幸いです。