photo by Max Langelott on Unsplash

※本記事は、Alina Yurenkoによる”Multilingual Engine: Executing JavaScript in Oracle Database“を翻訳したものです。


Alina Yurenko、Alexander Ulrich、Lucas Braun、Hugo Guiroux、Stefan Dobre 
この記事は同時掲載記事です。オリジナル記事はこちらをご覧ください
 
  
Oracle Database 21c以降、GraalVMを利用するJavaScriptを実行できるようになりました。本ブログ記事では、この新機能の内部について少しご紹介します。また、今後のブログ記事で、Oracle APEXでのサーバー側言語としてのJavaScriptについても取り上げる予定です。


オラクルは先ごろ、世界最先端のデータベースの次世代版となるOracle Database 21cをリリースしました。このリリースには、データベース内で直接JavaScriptコードを実行できる機能が追加されています。Oracle Databaseでは長い間、PL/SQL、JavaおよびCでのサーバー側プログラミングがサポートされてきました。サーバー側ビジネス・ロジックは、多くのエンタープライズ・アプリケーションで重要な役割を果たします。1つは、データをロジックに移動するのではなく、ロジックがデータのある場所で実行されるということです。これによって、ネットワーク上の不要なデータ送信がなくなり、特にテラバイト以上のデータがある場合など、データを多用するオペレーションのパフォーマンスを大幅に向上することができます。次に、たとえばデータベース内のビジネス・ルールを保管および実行することにより、すべてのアプリケーションだけでなく、データにアクセスするユーザーのルール順守が保証され、セキュリティとコンプライアンスの要件の実装を大幅に簡略化できます。最後に、通常のSQL文に加えて、よく使用する機能を中心的な場所に保管して、シンプルなユーザー定義の機能として実行できるため、すべてのアプリケーションでコードをレプリケートする必要がなくなります。この機能は、ロジックが複雑な場合や、頻繁に変わる傾向がある場合に特に実用的です。

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

21cリリースでは、Oracle Database内のJavaScriptのサポートは、ローコード・アプリケーション・フレームワークであるOracle Application Express(APEX)に焦点を当てています。Oracle Database 21cとAPEX 20.2以降、開発者はサーバー側ロジックをJavaScriptでAPEXアプリケーション(動的なアクションやプロセスなど)に実装でき、PL/SQLだけに制限されなくなります。これは、APEX開発者にとって、APEXアプリケーションで生産性を向上し、クールな新機能を実現できる可能性を秘めた、期待すべき新機能だと考えます。APEXでのJavaScriptの新機能の詳細については、今後のブログ記事で詳しく紹介する予定です。

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

Oracle Database内でのJavaScript実行を支えているコンポーネントは、Multilingual Engine(略称:MLE)と呼ばれています。この名前から、MLEは単なるJavaScriptエンジンではないということがお分かりいただけるかもしれません。内部を見てみると、MLEの主な要素はGraalVMであると言えます。 複数のプログラミング言語を高いパフォーマンスで実行できるポリグロット・ランタイムです。MLEは、GraalVMを基盤として、他のさまざまなシナリオにとっても興味深いと考えられるGraalVM独自の埋込み機能を使用して、構築されました。まずは、ユーザー向けの機能を見てみましょう。

 

Oracle DatabaseでJavaScriptを使用する

Oracle Databaseは、コード・スニペットにおけるエスケープの問題を回避するために、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はセッション内の1つのコンテキストに限定されません。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メソッドによって、現在のセッションを表す接続オブジェクトが返されます。APEXで実行されるサーバー・サイドJavaScriptコードは、便利なことに、apex.connプロパティを介して接続オブジェクトにアクセスすることができます。

問合せ結果をフェッチし、プレースホルダをSQL文にバインドする際に、mle-js-oracledbは一方のPL/SQL型と他方のJavaScript型の間で変換を行います。node-oracledbと同様に、デフォルトで、PL/SQL型はもっとも近いJavaScript型にそれぞれマッピングされます。上記の例では、結果列tsにPL/SQL型TIMESTAMP WITH TIME ZONEがあり、JavaScriptの日付値としてフェッチされます。 

ただし、PL/SQL型とネイティブのJavaScript型間の変換は常に適切とは限りません。データタイプの変換によって、精度が失われる場合があります。次の例では、数値を返す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結果:54.598
              const sql = "SELECT trunc(exp(4), 3) AS n FROM dual";
              const result = oracledb.defaultConnection().execute(sql);

              // 浮動小数点のJavaScriptの数値をフェッチ
              // 出力:54.598000000000006
              console.log(result.rows[0][0].toString());
           ~';

        BEGIN
           DBMS_MLE.eval(ctx, 'JAVASCRIPT', user_code);
           DBMS_MLE.drop_context(ctx);
        END;
        /

デフォルトで、Oracleの数値はJavaScriptの数値としてフェッチされます。より精度の高いOracleの数値形式から精度の低い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";

              // NUMBER列をOracleNumberとしてフェッチ
              const options = { fetchInfo: { N: { type: oracledb.ORACLE_NUMBER } } };
              const result = oracledb.defaultConnection().execute(sql, [], options);

              // 10進数のOracleNumber値を表示
              // 出力: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演算と同じセマンティクを用いる、小数点以下桁数の精度を計算する方法を提供します。

 

Multilingual Engine:Oracle Database内にGraalVMを埋め込む

ここまでで、Oracle Database 21cではどのようにJavaScriptを実行し、APEXアプリケーションがサーバー・サイドJavaScriptロジックからメリットを得られるかを見てきました。しかし、内部的にこれはどのような仕組みになっているのでしょうか。本記事の導入部分では、Multilingual Engine(MLE)の中核をなしているのが、Oracle Database内のGraalVMの埋込みであると言及しました。GraalVMはJavaやその他の言語向けのすばらしいスタンドアロン・ランタイムであるだけでなく、ネイティブ・アプリケーションにも埋め込むことが可能です。MLEは、GraalVMで、JavaScriptの実行などのプログラミング言語の機能によって既存のアプリケーションがいかに強化されるかを示す興味深い例となっています。この記事の残りの部分では、MLEのアーキテクチャの側面の一部に焦点を当て、この埋込みを可能にするGraalVMの機能について説明します。

以下の図は、MLEのアーキテクチャを簡略化したものです。このいくつかのボックスと矢印が何を表しているのかを見ていきましょう。
 

MLEのアーキテクチャ  

まずは最下層から見ていきます。Oracle Database(Cで記述されたネイティブ・アプリケーション)はどのようにして実際にGraalVMを呼び出し、JavaScriptコードを実行しているのでしょうか。いずれにしても、GraalVMはJavaに実装されています。答えは、GraalVMのネイティブ・イメージです。このネイティブ・イメージは、Javaアプリケーションを、JVMなしで実行できるスタンドアロンの実行可能ファイルにコンパイルできます。この機能は特に、メモリ要件が低く、ほぼ直ちに起動するJavaマイクロサービスに使用されています。しかし、スタンドアロンの実行可能ファイルとともに、ネイティブ・イメージは、既存のアプリケーション内にロードできる共有ライブラリも生成することができます。これが、Multilingual Engineを実現する中核テクノロジーです。ネイティブ・イメージはMLEランタイムと、JavaScriptランタイム(すべてJavaで実装)などの必要とされるすべてのGraalVMコンポーネントを、オンデマンドでデータベース・プロセスにロードされる共有ライブラリにコンパイルします。

Multilingual Engineのロード後、データベース・プロセスによって、MLEのネイティブ・イメージの機能が呼び出され、コンテキストが管理され、実際にJavaScriptコードが実行されます。同様に、MLEによって、JavaScriptコードへのSQL実行といったサービスを提供するために、ネイティブのデータベース機能が呼び出されます。こうした呼び出しパスの一部を、上の図に描写しています。赤の部分(ネイティブのデータベース・コード)から青の部分(MLEネイティブ・イメージ)へ、またその逆へかかるすべての矢印です。このような呼出しが頻繁に発生するため、これはパフォーマンスにとって非常に重要となります。幸いにも、GraalVMでは、ネイティブ・イメージ内のCコードと関数間の呼出しを非常に効率的に実装し、C関数間の通常の呼出しではオーバーヘッドはほとんどありません。

ここまでのところでは、Oracle DatabaseでのGraalVMの埋込みは、イメージの構築時と似ているように見えます。MLEネイティブ・イメージがロードされる際の起動の手間を最小化します。さらに、イメージの構築時に初期化されており、実行時に読込み専用のアクセスとなっているネイティブ・イメージのヒープ領域は、データベース・プロセス間で透過的に共有されます。これにより、MLEでJavaScriptコードを実行する複数の同時データベース・セッションのシナリオにおいて、MLEのメモリの影響が低減します。

Node.jsなどの埋込みのシナリオでは、JavaScriptランタイムのメモリは、オペレーティング・システムから直接割り当てられます。Oracle Databaseへの統合については、MLEはそれよりも深く到達しなければなりません。Oracle Databaseには、特にデータベースのワークロードについてリソース使用率を最適化するための、CPUやメモリなどのOSリソースを高度に管理できる機能も含まれます。さらに、Oracle Databaseは個々のテナントが、設定されているリソースの上限を超えることのないマルチテナント・システムです。JavaScript実行が同じルールに従うように、MLEはOracle Database Resource Managerと統合されています。MLEでは、JavaScriptランタイムに必要なすべてのメモリを、オペレーティング・システムから直接ではなく、データベース・サービスを介して割り当てます。このような割当はすべてリソース・マネージャのポリシーの対象となります。同様に、リソース・マネージャによって設定されているCPU上限はJavaScriptコードに対して有効であり、データベースによって要求された場合は、確実にJavaScriptコードをキャンセルすることができます。このため、MLEコンポーネントはデータベース・インスタンスでメモリを無駄に使用しません。

それでは、より上層部に目を向けて、MLEコンテキストの管理について説明しましょう。MLEコンテキストはDBMS_MLE APIを使用して作成することができます。前述のように、MLEでは開発者がデータベース・セッションでのコンテキスト管理を制御できるため、柔軟なプログラミング・モデルが提供されます。ただし、これはMLEの特別な機能ではありません。アプリケーションおよびランタイムの状態をカプセル化し、論理的に独立している複数のコンテキストを使用できる機能は、GraalVMの奥深くに組み込まれています。組込み開発者向けのGraalVM Polyglot APIでは、複数の独立したコンテキストの作成がサポートされます。MLEのコンテキスト管理用APIは、Polyglot API上に直接実装されます。

MLEでは同じデータベース・セッションで複数のコンテキストを作成できることを思い出しながら、MLEランタイムで管理されるポリグロット・コンテキストへの直接マッピングをコンテキストがどのように処理するかを表した以下の図を見ていきましょう。

MLEコンテキスト管理 

コンテキストによってアプリケーションの状態がカプセル化される一方で、GraalVMでは、ポリグロット・エンジン内の他のプログラム・リソースが管理されます。データベース・セッションを通じて、MLEは複数のコンテキストの下で同じポリグロット・エンジンを使用します。同じJavaScriptアプリケーション・コードが、エンジンを共有する同じセッションの複数のコンテキストで評価される場合、基盤となるエンジンは、それらのコンテキスト全体でJavaScriptコードの実行可能ファイルの表現を再利用できます。これにより、複数のコンテキストが使用される場合に、メモリ・フットプリントを低減できるほか、JavaScriptコードを解析するための時間を短縮できます。

GraalVMの強力な機能の1つが、言語の相互運用性です。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ドライバが呼び出され、SQL文が実行され、問合せ結果がフェッチされます。Truffleの多言語呼出しの効率的な実装のおかげで、このやり取りでは、2つのJavaScript関数の呼出しに比べて、実質的にオーバーヘッドがありません。

以上で、Multilingual Engineの一部の内部機能に関する紹介は終わりです。要約すると、Oracle Database Multilingual Engineによって、開発者は、幅広く使用されている最新のプログラミング言語であるJavaScriptで、サーバー側ロジックを記述できます。MLEは、GraalVMの独自の機能によって、Oracle Databaseなどの高度に複雑なシステムへの埋込みがいかに可能になるかを示す一例となっています。

 

今後の展望

Oracle Database 21cでJavaScriptがサポートされるようになったことで、GraalVMをOracle Databaseに近づける最初の一歩を踏み出せました。私たちは、今後のリリースでMultilingual Engineを改善していく予定であり、最新の言語による開発者のサーバー側プログラミングの体験をさらに向上でき、Oracle Database上に優れたアプリケーションを実現できる素晴らしい機能がたくさんあると考えています。そのような機能の1つとして考えられるのが、JavaScriptモジュールを最優先としてサポートすることと、NPMパッケージ向けの統合とツールを提供することです。また、さらに多くの言語をサポートすることで、MLEを真の多言語にすることも目指します。

MLEの使用についてフィードバックを共有したい場合や、今後のリリースに向けて機能をリクエストしたい場合は、こちらまでお送りください。 

GraalVMの詳細や始め方については、oracle.com/graalvmをご覧ください。