多言語エンジン: Oracle DatabaseでJavaScriptを実行 (2021/01/29)

多言語エンジン: Oracle DatabaseでJavaScriptを実行 (2021/01/29)

https://medium.com/graalvm/mle-executing-javascript-in-oracle-database-c545feb1a010
投稿者:Alina Yurenko

Photo by Max Langelott on Unsplash

21c以降、Oracle DatabaseはGraalVMを搭載したJavaScriptを実行できるようになりました。
このブログ記事では、この新機能の下を覗いてみましょう。
また、Oracle APEXのサーバーサイド言語としてのJavaScriptを示すフォローアップブログ記事も予定しています。

Alina YurenkoAlexander UlrichLucas BraunHugo Guiroux、およびStefan Dobre


最近、オラクルは、世界で最も先進的なデータベースの次のバージョンであるOracle Database 21cをリリースしました。
このリリースでは、データベース内で JavaScript コードを直接実行する機能が追加されています。
Oracle Databaseは、PL/SQL、Java、およびC言語でのサーバーサイド・プログラミングを長らくサポートしてきました。
サーバーサイド・ビジネス・ロジックは、多くのエンタープライズ・アプリケーションで重要な役割を果たしています。
これにより、ネットワークを介した不要なデータ転送が不要になり、特にテラバイト以上のデータに適用される場合には、
データを多用する操作のパフォーマンスを大幅に向上させることができます。
第二に、例えばビジネスルールをデータベース内に格納して実行することで、データにアクセスするユーザーだけでなく、
すべてのアプリケーションがルールに従っていることが保証されるため、セキュリティやコンプライアンス要件の実装を大幅に簡素化することができます。
最後に、一般的に使用される機能を中央の場所に保存し、通常のSQL文の上に単純なユーザー定義関数として実行することができるため、
すべてのアプリケーションでコードを複製する必要がありません。これは、ロジックがより複雑であったり、頻繁に変更される場合に特に実用的です。

21cでは、サポートされる言語のセットが拡張され、今日最も普及している人気のあるプログラミング言語の一つであるJavaScriptを含むようになりました。
開発者は、この人気のある言語をデータベースプログラミングに使用し、JavaScriptで利用可能なツールやライブラリの豊富なエコシステムを利用することができます。
PL/SQLと同様に、サーバーサイドのJavaScriptの実行は、データやSQLの実行に近いデータベースセッションのプロセス内でコードを実行することで、
データベースと緊密に統合されています。この緊密な統合により、一方の側ではJavaScript、もう一方の側ではSQLとPL/SQLの間で効率的なデータ交換が可能になります。

21cリリースでは、Oracle DatabaseでのJavaScriptサポートは、
ローコード・アプリケーション・フレームワークであるOracle Application Express(APEX)に焦点を当てています。
Oracle Database 21cおよびAPEX 20.2から、開発者はAPEXアプリケーションのサーバーサイド・ロジック(動的アクションやプロセスなど)を
JavaScriptで実装できるようになり、PL/SQLだけに制限されなくなりました。
これは、APEX開発者にとってエキサイティングな新機能であり、生産性を向上させ、APEXアプリケーションのクールな新機能を可能にする可能性を秘めていると考えています。
APEX の新しい JavaScript 機能については、フォローアップブログ記事で詳しく見ていきたいと思います。

APEX以外にも、Oracle DatabaseでのサーバーサイドJavaScript実行は、汎用のPL/SQL APIを介して利用できます。
このブログ記事では、このAPIと21cで利用可能な機能の一部を簡単にご紹介します。
さらに、サーバーサイドJavaScriptの実行がどのように実装されているかについても詳しく説明します。

Oracle Database Multilingual Engine(略してMLE)でJavaScriptの実行を強力にするコンポーネントのことを
「Oracle Database Multilingual Engine」と呼んでいます。
この名前から、MLEが単なるJavaScriptエンジンではないことがわかるかもしれません。
MLEの主な構成要素は、複数のプログラミング言語を高いパフォーマンスで実行できるポリグロットランタイムであるGraalVMです。
私たちはGraalVMのユニークなエンベッディング機能を利用して、GraalVMを基盤にMLEを構築しましたが、これは他のシナリオにも応用できると考えています。
しかし、その前に、ユーザー側を見てみましょう。

Oracle DatabaseでJavaScriptを使用


Oracle Databaseは、データベースセッション内でJavaScriptコードを評価するためのDBMS_MLE PL/SQLパッケージを提供しています。
JavaScript コードの評価は、常にアプリケーションの状態をカプセル化したコンテキストに関連付けられています (例: グローバル JavaScript 変数)。
次のスニペットは、(DBMS_MLE.create_context() 関数を使用して)コンテキストを作成し、
そのコンテキストハンドルを使用して DBMS_MLE.eval を使用して JavaScript コードを評価し、
最後に DBMS_MLE.drop_context を使用して不要になったコンテキストを削除するという基本的なワークフローを示しています。
この例ではコードスニペットを q'~ と ~' の引用符で囲んでいますが、コードスニペットのエスケープ問題を避けるために PL/SQL の引用符構文を使用しています。

DECLARE
ctx DBMS_MLE.context_handle_t := DBMS_MLE.create_context();
BEGIN
DBMS_MLE.eval(ctx, 'JAVASCRIPT', q'~console.log("Hello, World!");~');
DBMS_MLE.drop_context(ctx);
END;
/


DBMS_MLE パッケージは、例えば JavaScript と PL/SQL の間で値を交換するための追加のプロシージャを提供します。

デフォルトでは、JavaScript 関数 console.log() は PL/SQL パッケージ DBMS_OUTPUT のバッファに書き込みます。
このスニペットをSQL Developer Web、SQLcl、SQL*Plusなどのクライアントを通して実行すると、
それらのツールのコンソール上でconsole.log()によって生成された出力を見ることができます。
クライアントによっては、クライアントで DBMS_OUTPUT の取得を有効にする必要があるかもしれません。
例えば、SQL*Plusでは、DBMS_OUTPUTで生成された出力の印刷はSET SERVEROUTPUT設定で制御されます。
DBMS_OUTPUTパッケージを使用して、出力を手動で取得することもできます。

DBMS_MLE はセッション内の単一のコンテキストに限定されません。
DBMS_MLE.create_context() を使用すると、セッション内に複数のコンテキストを作成することができ、それぞれが完全に独立した JavaScript ランタイムを表します。
開発者がコンテキストを細かく制御できるようになるのは、強力な機能です。
同じセッション内で異なるアプリケーションを別々のコンテキストで分離し、アプリケーション間の干渉を防ぐことができます。
しかし、コンテキストは自由ではないことに注意してください。
各コンテキストは現在のデータベースセッション内のメモリを消費します。
データベースリソースへの影響を最小限に抑えるために、アプリケーションは一度に必要以上のコンテキストを作成してはいけません。
これはSQLカーソルのようなリソースと何ら変わりません。

サーバーサイドのプログラミング(例えば APEX アプリケーションなど)で本当に役に立つためには、JavaScript のコードがデータベースと対話できる必要があります。
MLE は、SQL や PL/SQL ステートメントの実行をサポートする mle-js-oracledb JavaScript モジュールを提供しています。
次の例では、このモジュールを使用して現在時刻を返す単純な SQL クエリを実行します。

DECLARE
ctx DBMS_MLE.context_handle_t := DBMS_MLE.create_context();
user_code clob := q'~
const oracledb = require("mle-js-oracledb");
const sql = "SELECT SYSTIMESTAMP as ts FROM dual";
// execute query
const result = oracledb.defaultConnection().execute(sql);
console.log(JSON.stringify(result.rows));
~';
BEGIN
DBMS_MLE.eval(ctx, 'JAVASCRIPT', user_code);
DBMS_MLE.drop_context(ctx);
END;
/


mle-js-oracledb API は、通常のクライアントサイド node.js用のOracle Database ドライバに密接に従っています。
mle-js-oracledb を使用して、アプリケーションは任意の SQL 文 (クエリ、DML 文、匿名 PL/SQL ブロックなど) を実行したり、
値をプレースホルダにバインドしたり、クエリ結果を取得したりすることができます。
node-oracledb API を使用している既存の JavaScript コードは、通常、わずかな労力で mle-js-oracledb に適合させることができます。
node-oracledb に比べて、ここでは実際にデータベースに接続する必要はありません。
MLE の JavaScript コードから実行されるすべての SQL 文は、現在のデータベースセッション内で実行されます。
oracledb.defaultConnectionメソッドは、現在のセッションを表すConnectionオブジェクトを返します。
APEX で実行されるサーバーサイドの JavaScript コードは、apex.conn プロパティを使用して接続オブジェクトに便利にアクセスできることに注意してください。

クエリ結果をフェッチしたり、SQL文のプレースホルダをバインドしたりするとき、mle-js-oracledbは一方ではPL/SQL型、もう一方ではJavaScript型の間で変換します。
node-oracledb と同様に、PL/SQL 型はデフォルトで最も近いそれぞれの JavaScript 型にマップされます。
上記の例では、結果の列 ts は PL/SQL 型 TIMESTAMP WITH TIME ZONE を持ち、JavaScript の Date 値としてフェッチされています。

しかし、PL/SQL 型とネイティブ JavaScript 型の間の変換は常に適切ではありません。
データ型の変換は精度の低下を招く可能性があります。次の例では、NUMBER値を返すSQLクエリを実行します。
exp(4)の結果を小数点以下3桁に切り捨て、54.598という結果を期待しています。

DECLARE
ctx DBMS_MLE.context_handle_t := DBMS_MLE.create_context();
user_code clob := q'~
const oracledb = require("mle-js-oracledb");

// SQL NUMBER result: 54.598
const sql = "SELECT trunc(exp(4), 3) AS n FROM dual";
const result = oracledb.defaultConnection().execute(sql);
// fetch floating-point JavaScript number value
// Output: 54.598000000000006
console.log(result.rows[0][0].toString());
~';
BEGIN
DBMS_MLE.eval(ctx, 'JAVASCRIPT', user_code);
DBMS_MLE.drop_context(ctx);
END;
/


デフォルトでは、Oracle NUMBER値はJavaScriptの数値値としてフェッチされます。
より正確なOracle NUMBER形式から、より正確ではないJavaScriptの数値形式に変換すると、予期しない結果が発生する可能性があります。
この例では、返されたJavaScript番号54.598000000000006は浮動小数点変換により、SQLクエリの実際の結果とは若干異なります。
この特定の例ではこの影響はあまり関係ないように見えるかもしれませんが、例えば通貨の値を扱うようになると、この影響は大きく変化します - ここでは数値の精度が最も重要です。

この状況を改善するために、mle-js-oracledb APIは、開発者が特定のアプリケーションシナリオに適したデータ表現を選択できるようにしています。
先の2つの例のように、SQL値はネイティブのJavaScript型として取得することができ、既存のJavaScriptコードと便利に統合することができます。
別の方法として、MLEは選択したPL/SQL型に対してJavaScript APIを提供しており、ネイティブJavaScript型への変換の必要性を排除し、精度の損失を回避します。
次の例では、以前と同じSQLクエリを実行し、結果のNUMBER列を取得します。
しかし、今回はmle-js-oracledbにOracleNumberオブジェクトとして列をフェッチするように指示しています。

DECLARE
ctx DBMS_MLE.context_handle_t := DBMS_MLE.create_context();
user_code clob := q'~
const oracledb = require("mle-js-oracledb");
const sql = "SELECT trunc(exp(4), 3) AS n FROM dual";
// fetch NUMBER column as OracleNumber
const options = { fetchInfo: { N: { type: oracledb.ORACLE_NUMBER } } };
const result = oracledb.defaultConnection().execute(sql, [], options);
// print decimal OracleNumber value
// Output: 54.598
console.log(result.rows[0][0].toString());
~';
BEGIN
DBMS_MLE.eval(ctx, 'JAVASCRIPT', user_code);
DBMS_MLE.drop_context(ctx);
END;
/


OracleNumberオブジェクトは、元のSQL値と全く同じ10進数の値を表現します。
さらに、OracleNumberオブジェクトは、NUMBER型に対する対応するPL/SQL操作と同じセマンティクスを持つ10進数精度演算のメソッドを提供します。


多言語エンジン: Oracle DatabaseへのGraalVMの組み込み


ここまでで、Oracle Database 21cがJavaScriptを実行し、APEXアプリケーションがサーバーサイドのJavaScriptロジックの恩恵を受けられるようにする方法を見てきました。
しかし、実際にはどのように機能しているのでしょうか?
この記事の紹介では、Multilingual Engine (MLE) の中核は Oracle Database への GraalVM の組み込みであることを述べました。
GraalVMはJavaや他の言語のための優れたスタンドアロンランタイムであるだけでなく、ネイティブアプリケーションへの組み込みも可能です。
MLEは、GraalVMが既存のアプリケーションをプログラミング言語の機能(例えばJavaScriptの実行など)で豊かにすることができるという興味深い例です。
この記事の残りの部分では、MLEのアーキテクチャのいくつかの側面にスポットライトを当て、この埋め込みを可能にするGraalVMの機能について説明します。

次の図は、MLEのアーキテクチャを簡略化したものです。
これらの箱と矢印を見て、それが何を意味するのかを見てみましょう。


MLE Architecture

まずは一番下の層から見ていきましょう。
Oracle Database - C言語で書かれたネイティブアプリケーション - 実際にGraalVMに呼び出してJavaScriptのコードを実行するにはどうすればよいのでしょうか?
結局のところ、GraalVMはJavaで実装されています。
その答えは、JavaアプリケーションをJVMなしで実行できるスタンドアロンの実行ファイルにコンパイルすることができるGraalVM Native Imageです。
この機能は一般的に、必要なメモリ量が少なく、ほぼ瞬時に起動するJavaマイクロサービスに使用されます。
しかし、スタンドアロン実行ファイルと並んで、Native Imageは、既存のアプリケーションにロードできる共有ライブラリを生成することもできます。
これが多言語エンジンを可能にするコア技術です。Native Imageは、MLEランタイムとJavaScriptランタイムのような必要な
GraalVMコンポーネントをすべてJavaで実装した共有ライブラリにコンパイルし、必要に応じてデータベースプロセスにロードします。

多言語エンジンをロードした後、データベースプロセスは、コンテキストを管理し、JavaScriptコードを実際に実行するためにMLEネイティブイメージ内の関数を呼び出します。
同様に、MLEはJavaScriptコードにSQL実行などのサービスを提供するためにネイティブデータベースの関数を呼び出します。
上の図では、これらの呼び出し経路のいくつかをスケッチしています。
すべての矢印は、赤(ネイティブデータベースコード)から青(MLE Native Image)、またはその逆に交差しています。
このような呼び出しは頻繁に発生するため、パフォーマンスにとって非常に重要です。
幸いなことに、GraalVMはCコードとネイティブイメージ内の関数間の呼び出しを非常に効率的に実装しており、C関数間の通常の呼び出しに比べてオーバーヘッドはほとんどありません。

これまでのところ、Oracle DatabaseへのGraalVMの組み込みは、
C++で書かれたNode.jsランタイムにGraalVMのJavaScriptエンジンを埋め込むためにNative Imageを使用するGraalVMのNode.jsランタイムと似ているように見えます。
しかし、Node.jsが単一のOSプロセスで動作するのに対し、Oracle Databaseは、データベースセッションが
別々のOSプロセス(専用サーバー接続用のサーバープロセスと呼ばれる)で処理されるマルチプロセスアーキテクチャを特徴としています。
MLE Native Imageは、MLEを使用する各データベースセッションプロセスにロードされます。
MLE Native Image をロードして使用するデータベースプロセスの数が多くなる可能性があるため、起動時間とメモリフットプリントが重要になります。
高速な起動と低いメモリフットプリントは、一般的にNative Imageテクノロジーの主な利点です。
具体的には、多言語エンジンを構成するJavaクラスのかなりの数がイメージのビルド時に初期化されるため、
MLE Native Imageがロードされたときの起動時間を最小限に抑えることができます。
さらに、イメージのビルド時に初期化されたNative Imageヒープの領域は、実行時に読み取り専用でアクセスできるようになっており、データベースプロセス間で透過的に共有されます。
これにより、MLE を使用して JavaScript コードを実行する複数のデータベースセッションが同時に実行されるシナリオにおいて、MLE のメモリへの影響が軽減されます。

Node.jsのようなエンベッディングのシナリオでは、JavaScriptランタイムのためのメモリはオペレーティングシステムから直接割り当てられます。
Oracle Databaseへの統合のためには、MLEはそれ以上の深みに到達しなければなりません。
Oracle Databaseは、CPUやメモリなどのOSリソースを高度に管理し、データベースのワークロードに特化したリソース利用を最適化します。
さらに、Oracle Databaseはマルチテナントシステムであるため、個々のテナントが課せられたリソース制限を超えることはありません。
JavaScriptの実行が同じルールで行われるように、MLEはOracle Database Resource Managerと統合されています。
MLEは、JavaScriptの実行に必要なすべてのメモリをデータベースサービスを通じて割り当てますが、オペレーティングシステムから直接割り当てることはありません。
このようなすべての割り当ては、Resource Managerのポリシーに従います。
同様に、リソースマネージャによって課されるCPU制限はJavaScriptコードにも有効であり、データベースから要求された場合にはJavaScriptコードを確実にキャンセルすることができます。
これにより、MLE コンポーネントがデータベースインスタンス内の善良な市民であることが保証されます。

次に、DBMS_MLE API を使用して作成できる MLE コンテキストの管理について説明します。
先ほど説明したように、MLE は柔軟なプログラミングモデルを提供することで、データベースセッション内のコンテキスト管理を開発者に制御させます。
しかし、これは MLE の特別な機能ではありません。
アプリケーションと実行時の状態をカプセル化し、論理的に独立した複数のコンテキストを使用する機能は、GraalVMに深く組み込まれています。
エンベッダ用のGraalVM Polyglot APIは、複数の独立したコンテキストの作成をサポートしています。
コンテキスト管理のためのMLEのAPIは、Polyglot APIの上に直接実装されています。

MLEでは、同じデータベースセッション内で複数のコンテキストを作成することができることを思い出してください。
次の図を見てみましょう。
コンテキストハンドルがどのようにして MLE ランタイムで管理されている Polyglot コンテキストに直接マッピングされているかを示しています。


MLE Context Management

コンテキストがアプリケーションの状態をカプセル化している間、GraalVMは他のプログラムリソースをポリグロットエンジン内で管理します。
データベースセッションを通して、MLEは複数のコンテキストの下で同じポリグロットエンジンを使用します。
エンジンを共有する同じセッション内の複数のコンテキストで同じJavaScriptアプリケーションコードが評価される場合、
その下にあるエンジンは、それらのコンテキスト間でJavaScriptコードの実行可能な表現を再利用することができます。
これにより、複数のコンテキストが使用されている場合、JavaScript コードを解析するのに必要な時間だけでなく、メモリフットプリントを削減することができます。

GraalVMの強力な機能として、言語間の相互運用性があります。
GraalVMのTruffle言語実装フレームワークの上に実装されたJavaScriptと他のプログラミング言語は、
ほとんどオーバーヘッドなしでメソッドを呼び出してお互いのデータにアクセスすることで相互作用を行うことができます。
MLEでは、GraalVMの言語相互運用性を利用して、JavaScriptコードにデータベース機能へのアクセスを制御された安全な方法で提供しています。
前のセクションでは、mle-js-oracledbモジュールが提供するSQL実行用のJavaScript APIを見てきました。
この JavaScript API は実際には MLE SQL Driver と呼ばれるコンポーネントの上に実装されています。
このコンポーネントは、安全なAPIを使用して現在のデータベースセッション内でのSQL文の実行をサポートします。
mle-js-oracledb モジュールの JavaScript 実装は、Truffle の言語相互運用性プロトコルを介して MLE SQL Driver を呼び出し、SQL 文を実行してクエリ結果を取得します。
Truffle の効率的な言語横断呼び出しの実装により、この相互作用は、2 つの JavaScript 関数間の呼び出しに比べて実質的にオーバーヘッドがありません。

これで、Multilingual Engineの内部についてのツアーを終了します。
要約すると、Oracle Database Multilingual Engineを使用すると、開発者はサーバーサイドのロジックを、
広く使用されている最新のプログラミング言語であるJavaScriptで記述することができます。
MLEは、GraalVMのユニークな機能が、Oracle Databaseのような非常に複雑なシステムへの組み込みを可能にしている例です。

未来へのビジョン


Oracle Database 21cでJavaScriptがサポートされたことで、GraalVMをOracle Databaseに導入する第一歩を踏み出しました。
私たちは今後のリリースで多言語エンジンを改善することを楽しみにしており、
最新の言語を使用したサーバーサイドプログラミングの開発者体験をさらに向上させ、
Oracle Database上での優れたアプリケーションを可能にする多くの優れた機能があると信じています。
そのような機能の1つとして、JavaScriptモジュールを一級市民としてサポートし、NPMパッケージの統合とツールを提供しています。
また、より多くの言語のサポートを追加することで、MLEを真の多言語にすることを目指しています。

MLEの使用に関するフィードバックや、今後のリリースに向けた機能のリクエストは、こちらで受け付けています。

Oracle CloudでMultilingual Engineを無料でご利用いただけます。

コメント

このブログの人気の投稿

Oracle Database 19cサポート・タイムラインの重要な更新 (2024/11/20)

Oracle APEXのInteractive Gridで、Oracle Formsと比較して、重複行の検証を制御/通過させる方法 (2022/07/21)

Oracle APEX 24.1の一般提供の発表 (2024/06/17)