PL/SQL外部ファンクション・インタフェースの概要

外部ファンクション・インタフェース(FFI)は、よく知っているJavaScriptのような方法でPL/SQLパッケージに簡単にアクセスできるように設計されています。

mle-js-plsql-ffi APIを使用すると、PL/SQLパッケージおよびプロシージャを中心にラッパーが作成されるため、後続のコールでは、JavaScriptオブジェクトおよびファンクションのようにラッパーと対話できます。この方法は、MLE JavaScript SQLドライバの代替として使用できる場合があります。

多くのデータベース機能は、PL/SQLパッケージ(組込み、APEXなどのフレームワークによってインストールされたもの、またはユーザー定義PL/SQLコード)の形式で使用できます。外部ファンクション・インタフェース(FFI)を使用すると、SQL文を実行せずに、パッケージおよびプロシージャのPL/SQL機能にJavaScriptコードから直接アクセスできるため、既存のPL/SQL機能をサーバー側のJavaScriptアプリケーションとシームレスに統合できます。たとえば、データベース・プロシージャをJavaScriptファンクションとして起動し、JavaScript値をファンクション引数として渡すことができます。

session.executeを使用して匿名PL/SQLブロック内のDBMS_RANDOMパッケージを使用する次のJavaScriptスニペットについて考えてみます:

CREATE OR REPLACE FUNCTION get_random_number(
    p_lower_bound NUMBER,
    p_upper_bound NUMBER
) RETURN NUMBER
AS MLE LANGUAGE JAVASCRIPT
{{
    const result = session.execute(
        'BEGIN :randomNum := DBMS_RANDOM.VALUE(:low, :high); END;',
        {
            randomNum: {
                type: oracledb.NUMBER,
                dir: oracledb.BIND_OUT
            }, low: {
                type: oracledb.NUMBER,
                dir: oracledb.BIND_IN,
                val: P_LOWER_BOUND
            }, high: {
                type: oracledb.NUMBER,
                dir: oracledb.BIND_IN,
                val: P_UPPER_BOUND
            }
        }
    );
    
    return result.outBinds.randomNum;
}};
/

SELECT get_random_number(1,100);

FFIを使用すると、前述の例の実装に必要なボイラープレート・コードを削減できます。次のスニペットは、前述のスニペットと同じ機能をより簡潔に実現します:

CREATE OR REPLACE FUNCTION get_random_number(
    p_lower_bound NUMBER,
    p_upper_bound NUMBER
) RETURN NUMBER
AS MLE LANGUAGE JAVASCRIPT
{{
    const { resolvePackage } = await import ('mle-js-plsql-ffi');

    const dbmsRandom = resolvePackage('dbms_random');

    return dbmsRandom.value(P_LOWER_BOUND, P_UPPER_BOUND);
}};
/

SELECT get_random_number(1,100);

関連項目:

mle-js-plsql-ffi APIの詳細は、サーバー側JavaScript APIのドキュメントを参照してください

FFIを使用したオブジェクト解決

mle-js-plsql-ffi APIでは一連のファンクションを使用でき、それぞれが対応するデータベースを表すJavaScriptオブジェクトを戻します。

パッケージおよび最上位のファンクションおよびプロシージャの解決には、次のファンクションを使用できます:

  • resolvePackage('<pkg_name>')
  • resolveProcedure('<proc_name>')
  • resolveFunction('<func_name>')

解決するオブジェクトが自分のスキーマ内にあるか、パブリック・シノニムがある場合、オブジェクト名を所有スキーマで修飾することはオプションです。オブジェクトが別のスキーマにある場合は、オブジェクトにアクセスするために必要な権限があり、所有スキーマでその名前を修飾する必要があります。MLE JavaScript SQLドライバと同様に、すべての操作は独自のセキュリティ・コンテキストで実行されます。

ノート:

指定されたデータベース・オブジェクトが存在しないか、そのオブジェクトにアクセスできない場合、RangeErrorが発生します。指定された名前が正しい型ではないデータベース・オブジェクトに解決されると、TypeErrorが発生します。データベース・リンクはサポートされていません。データベース・リンクを使用して名前を解決しようとすると、Errorになります。

ノート:

提供されているFFIファンクションは、PL/SQLと同じ大/小文字の区別ルールに従います。つまり、名前はデフォルトで先頭文字が大文字になります。引用符で囲まれた識別子の場合、大/小文字の区別を示すために、二重引用符と一重引用符を組み合せたJavaScriptディクショナリ表記を使用する必要があります:

// call a procedure with case-sensitive name
myPkg['"MyProc"']();

// read a global variable with a case-sensitive name
console.log(myPkg['"MyVar"']);

データベース・オブジェクトが解決されると、結果オブジェクトに対して次の操作を実行できます:

  • プロシージャ: 実行
  • ファンクション: 実行
  • パッケージ:
    • プロシージャの実行
    • ファンクションの実行
    • パブリック・パッケージ変数の読取りおよび書込み
    • 定数の読取り

resolvePackageを使用すると、結果オブジェクトのプロパティ読取りを介して、変数、定数、プロシージャおよびファンクションに直接アクセスできます。パッケージにプロパティ読取りで指定されたメンバーがない場合は、Referenceエラーがスローされます。アクセスされたメンバーがPL/SQLファンクションまたはプロシージャの場合、JavaScriptオブジェクトは、最上位のファンクションおよびプロシージャに対して解決される同じタイプのコール可能エンティティを戻します。構文の例については、次のスニペットを考えてみます:

// resolve a package
const myPkg = resolvePackage('my_package');

// call a procedure and function in the package
myPkg.my_proc();
let result = myPkg.my_func();

// read a global variable and constant in the package
console.log(myPkg.my_var);
console.log(myPkg.my_const);

// write a global variable in the package
myPkg.my_var = 42;

パッケージ変数および定数の場合、名前付きでない型のみがサポートされます。PL/SQLレコード型、ネストした表型、連想配列、ベクトル型およびADTの型はサポートされていません。

プロシージャまたはファンクションを解決するときに、コール可能なオブジェクトを受け取ります。ファンクションでは、オプションでoverrideReturnTypeインスタンス・メソッドを使用して戻り型を指定し、他のメタデータを変更できます。overrideReturnTypeを使用してmaxSize属性を増やす次の例を考えてみます:

  1. まず、文字列を戻すファンクションを作成します:
    CREATE OR REPLACE FUNCTION ret_string(
        MULTIPLIER NUMBER
    ) RETURN VARCHAR2 AS
    BEGIN
        return rpad('this string might be too long for the defaults ', MULTIPLIER, 'x');
    END;
    /
  2. FFIを使用してファンクションret_stringを解決する別のファンクションret_string_ffiを作成します:
    CREATE OR REPLACE FUNCTION ret_string_ffi(
        MULTIPLIER NUMBER
    ) RETURN VARCHAR2
    AS MLE LANGUAGE JAVASCRIPT
    {{
        const retStrFunc = plsffi.resolveFunction('ret_string');
        return retStrFunc(MULTIPLIER);
    }};
    /
  3. ret_string_ffiファンクションは、次のように、乗数値が十分に小さい値であれば動作します:
    SELECT ret_string_ffi(50);

    結果:

    RET_STRING_FFI(50)
    --------------------------------------------------------------------------------
    this string might be too long for the defaults xxx
  4. 大きい乗数値の場合、結果はデフォルトのバッファ長200バイトを超える可能性があり、エラーが発生します:
    SELECT ret_string_ffi(900);

    結果:

    SELECT ret_string_ffi(900)
                             *
    ERROR at line 1:
    ORA-04161: Error: Exception during subprogram execution (6502): ORA-06502:
    PL/SQL: value or conversion error: character string buffer too small
    ORA-04171: at :=> (<inline-src-js>:3:12)
  5. この問題を解決するには、overrideReturnTypeインスタンス・メソッドを使用して、戻されたメッセージのmaxSize属性を増やします:
    CREATE OR REPLACE FUNCTION ret_str_ffi_override(
        MULTIPLIER NUMBER
    ) RETURN VARCHAR2
    AS MLE LANGUAGE JAVASCRIPT
    {{
        const retStrFunc = plsffi.resolveFunction('ret_string');
        
        // overrideReturnType accepts either an oracledb type constant
        // such as oracledb.NUMBER, or a string containing the name of a 
        // user defined database type. If more information is needed, as
        // in this example, a parameter of type ReturnInfo can be provided
        retStrFunc.overrideReturnType({
            maxSize: 1000
        });
        return retStrFunc(MULTIPLIER);
    }};
    /
  6. 新しいret_str_ffi_overrideファンクションを使用すると、より大きな乗数のコールが機能するようになります:
    SELECT ret_str_ffi_override(900);

FFIを使用したサブプログラムへの引数の指定

argおよびargOfファンクションを使用して、外部ファンクション・インタフェース(FFI)でIN OUTおよびOUTパラメータを処理します。

JavaScriptとPL/SQLは、パラメータを異なる方法で処理します。たとえば、JavaScriptでは、PL/SQLと同じ方法で名前付きパラメータを使用できません。JavaScriptには、OUTおよびIN OUTパラメータに相当するものはなく、ファンクションをオーバーロードするオプションもありません。最後に、JavaScript型はデータベースの組込み型システムと異なっています。JavaScriptからPL/SQLをコールできるようにするには、FFIがこれらの違いに対応している必要があります。

PL/SQLサブプログラム・パラメータの詳細は、『Oracle Database PL/SQL言語リファレンス』を参照してください。

次のプロシージャは、次のような場合を示しています:
  • 複数のパラメータが定義されている。
  • パラメータが、INOUTおよびIN OUTモードの組合せを提供している。
  • VARCHAR2 OUT変数のデフォルトのmaxSizeが不十分である
CREATE OR REPLACE PROCEDURE my_proc_w_args(
    p_arg1      IN NUMBER,
    p_arg2      IN NUMBER,
    p_arg3      IN OUT JSON,
    p_arg4      OUT TIMESTAMP,
    p_arg5      OUT VARCHAR2
) AS
BEGIN
  
  SELECT
    JSON_TRANSFORM(p_arg3,
      SET '$.lastUpdate' = systimestamp,
      SET '$.value' = p_arg1 + p_arg2
    )
    into p_arg3;
    
  p_arg4 := systimestamp;

  -- the length of the string will exceed the default
  -- length of 200 characters for the out bind, mandating
  -- the use of maxSize in args().
  p_arg5 := rpad('x', 255, 'x');
    
END;
/

INモードを使用して渡されるパラメータには、特別な処理は必要ありません。FFIには、OUTおよびIN OUTパラメータをそれぞれ処理するためのarg()およびargOf()ファンクションが用意されています。FFIを使用して提供されるすべてのパラメータは基本的にバインド・パラメータであるため、session.execute()を使用してPL/SQLを直接コールする場合に使用するdirvaltypeおよびmaxSizeプロパティと同じものを使用して、それらの動作に影響を与えることができます。

argファンクションは、引数を表すオブジェクトを生成します。オプションで、dirvaltypeおよびmaxSizeプロパティの任意の組合せを含む、MLE JavaScript SQLドライバと同じオブジェクトを受け入れます。

argOfファンクションは、指定された値の引数を表すオブジェクトを生成します。

パラメータは、次の2つの方法で渡すことができます:
  • 位置指定引数のリストとして。
  • オブジェクトを使用して引数を指定し、名前付きパラメータをシミュレートする。

前述の例で作成したファンクションmy_proc_w_argsに基づいて、次のように位置指定引数を使用してFFIでファンクションを呼び出すことができます:

CREATE OR REPLACE PROCEDURE my_proc_w_args_positional(
    "arg1" NUMBER,
    "arg2" NUMBER
) AS MLE LANGUAGE JAVASCRIPT
{{
    const myProc = plsffi.resolveProcedure('my_proc_w_args');

    // arg3 is an IN OUT parameter of type JSON. my_proc_with_args
    // will modify it in place and return it to the caller
    const arg3 = plsffi.argOf({id: 10, value: 100});

    // arg4 is a pure OUT parameter
    const arg4 = plsffi.arg();

    // arg5 represents an OUT parameter as well but due to the
    // length of the return string, it must be provided with additional
    // metadata
    const arg5 = plsffi.arg({
        maxSize: 1024
    });

    myProc(arg1, arg2, arg3, arg4, arg5);

    console.log(`the updated JSON looks like this: ${JSON.stringify(arg3.val)}`);
    console.log(`the calculation happened at ${arg4.val}`);
    console.log(`the length of the string returned is ${arg5.val.length} characters`);
}};
/

2つ目のオプションは、単一のプレーンなJavaScriptオブジェクトとして提供される名前付き引数を使用することです。次に、FFI APIは各プロパティを、プロパティの名前に一致する引数にマップします。

CREATE OR REPLACE PROCEDURE my_proc_w_args_named(
    "arg1" NUMBER,
    "arg2" NUMBER
) AS MLE LANGUAGE JAVASCRIPT
{{
    const myProc = plsffi.resolveProcedure('my_proc_w_args');

    // arg3 is an IN OUT parameter of type JSON. my_proc_with_args
    // will modify it in place and return it to the caller
    const arg3 = plsffi.argOf({id: 10, value: 100});

    // arg4 is a pure OUT parameter
    const arg4 = plsffi.arg();

    // arg5 represents an OUT parameter as well but due to the
    // length of the return string must be provided with additional
    // metadata
    const arg5 = plsffi.arg({
        maxSize: 1024
    });

    myProc({
        p_arg1: arg1,
        p_arg2: arg2,
        p_arg3: arg3,
        p_arg4: arg4,
        p_arg5: arg5
    });

    console.log(`the updated JSON looks like this: ${JSON.stringify(arg3.val)}`);
    console.log(`the calculation happened at ${arg4.val}`);
    console.log(`the length of the string returned is ${arg5.val.length} characters`);
}};
/

JavaScriptでobjectとして表される単一の引数を持つPL/SQLサブプログラムがあるエッジ・ケースに注意してください。直感的に、これを単一の位置指定引数として渡すことができますが、その場合、FFIはそれを名前付き引数オブジェクトとして解釈します。

この例外には2つの方法があります:
  • 名前付き引数でサブプログラムをコールしているように、オブジェクトに引数をラップできます。
  • 引数はplsffi.argOf()でラップでき、FFIはそれを単一の位置指定引数として認識します。

これらのオプションを示す次の例を考えてみます:

-- PL/SQL subprogram we want to call
CREATE OR REPLACE PROCEDURE my_proc(my_arg JSON) AS
BEGIN
    -- Process my_arg
END;

-- JavaScript function that calls my_proc
CREATE OR REPLACE PROCEDURE my_javascript_proc
AS MLE LANGUAGE JAVASCRIPT
{{
    const myProc = plsffi.resolveProcedure('my_proc');
    const myArg = { prop1: 10, prop2: 'foo' };

    // Catch the exception that will happen if the FFI tries
    // to interpret this as a call with named arguments
    try {
        myProc(myArg);
    } catch (err) {
        console.log(`if uncaught, this would have been a ${err}`);
    }

    // Option 1: Make it into a real named argument call.
    myProc({ my_arg: myArg });

    // Option 2: Wrap with argOf() to let the FFI know that it's a 
    // positional argument list call.
    myProc(plsffi.argOf(myArg));
}};

PL/SQLを使用すると、開発者は、PL/SQLパッケージで定義されているファンクションおよびプロシージャのシグネチャをオーバーロードできます。FFIではオーバーロードの選択は実行されませんが、各引数のバインドに使用するPL/SQL型を決定する必要があります。残念ながら、すべての場合において、この決定を独自に行うことはできません。特に、次の場合に当てはまります:

  • コールするために正しいシグネチャの判断に必要な引数にJavaScript値が指定されていない場合。値がないと、FFIには一致するPL/SQL型のセットを認識する方法がありません。
  • 1つのJavaScript型が複数のPL/SQL型に対して実行可能な場合。
FFIはSQLドライバ定数を使用して、ユーザー定義型の標準型および文字列(型名を含む)を表すことに注意してください。SQLドライバの定数は、次の2つの種類があります:
  • DB_TYPE_*で始まる定数は、JavaScript値をPL/SQL値に変換する方法を制御します。
  • その他すべてのものは、戻されるPL/SQL値をJavaScript値に変換する方法を制御するために使用されます。

型解決に役立つように引数の型を指定する場合は、DB_TYPE_*定数のいずれかを使用することをお薦めします。

次のPL/SQLパッケージを考えてみます。

CREATE OR REPLACE package overload_pkg AS

    FUNCTION my_func(
        p_arg1 IN BINARY_FLOAT
    ) RETURN VARCHAR2;
    
    FUNCTION my_func(
        p_arg1 IN INTEGER
    ) RETURN VARCHAR2;
END;
/

CREATE OR REPLACE PACKAGE BODY overload_pkg AS
    
    FUNCTION my_func(
        p_arg1 IN BINARY_FLOAT
    ) RETURN VARCHAR2 AS
    BEGIN
        RETURN 'binary_float';
    END;
    
    FUNCTION my_func(
        p_arg1 IN INTEGER
    ) RETURN VARCHAR2 AS
    BEGIN
        RETURN 'integer';
    END;
END;
/

このように、my_procはオーバーロードされ、BINARY_FLOATINTEGERの両方を受け入れます。JavaScriptでは、これらの両方の型が数値データ型として表されるため、複数のオーバーロードが有効です。FFI APIで正しい解決方法を選択できない場合は、PL/SQL型を指定して、特定のオーバーロードされたPL/SQLファンクションを強制的に実行できます。

CREATE OR REPLACE PROCEDURE force_overload
AS MLE LANGUAGE JAVASCRIPT
{{
    const myPkg = plsffi.resolvePackage('overload_pkg');

    let result = 'not yet called';

    // Catch error ORA-04161: Error: Exception during subprogram execution 
    // (4161): Multiple subprograms match the provided signature
    try {
        result = myPkg.my_func(42);
    } catch (err) {
        console.log(`if uncaught, this would have been a ${err}`);
    }

    // Solution: use argOf to make this work
    result = myPkg.my_func(plsffi.argOf(42, {type: oracledb.DB_TYPE_BINARY_FLOAT}))
    console.log(`and the result is: ${result}`);
}};
/

型がユーザー定義の場合もエラーが発生します。たとえば、すべてのJavaScriptオブジェクトは、すべてのPL/SQLレコードに対して実行可能とみなされます。この場合、目的の型の名前を指定すれば十分です。