ユーザー定義のデータ型の使用

コレクション、レコードおよび抽象データ型は、JavaScriptファンクションのパラメータとして使用でき、データベース内JavaScriptを使用してデータベースに挿入できます。

ユーザー定義型は次の方法で定義できます。

  • PL/SQLブロック内。この場合、ユーザー定義型はローカル型であり、ブロック内での参照のみが可能です。スタンドアロン・サブプログラムまたはパッケージ・サブプログラム内にPL/SQLブロックがある場合にのみ、データベースに格納されます。
  • パッケージ仕様部内。ここでは、ユーザー定義型はパブリック項目であり、パッケージ名で修飾することでパッケージ外で参照できます。
  • スキーマ・レベル。この場合、ユーザー定義型は、CREATE TYPE文を使用して作成されるスタンドアロン型です。DROP TYPE文を使用して削除するまでデータベースに格納されます。スキーマ・レベルではRECORD型を作成できないことに注意してください。

ローカル型とスタンドアロン型およびパブリック項目は、JavaScriptでの使用がサポートされています。

ノート:

MLEを使用してオブジェクトのメンバー・ファンクションを起動することはできません。試行すると、アクションはORA-04161: メソッドの起動は許可されていませんで失敗します。

JavaScriptでのレコード・データ型の使用

レコード・データ型は、たとえば、JavaScriptファンクションのパラメータとして使用でき、データベースに挿入できます。

ファンクション引数としてのレコード・データ型の使用

レコードの一般的なユースケースは、ファンクションに渡されるデータです。PL/SQLパッケージでローカル・レコードが宣言されている次の例を考えてみます。

CREATE OR REPLACE PACKAGE rec_mle_js AS

    TYPE person_t IS RECORD (
        surname      VARCHAR2(100),
        firstname    VARCHAR2(100),
        street       VARCHAR2(100),
        city_name    VARCHAR2(50),
        country_name VARCHAR2(50)
    );

END rec_mle_js;
/

このレコードを使用して、個人の住所を記述できます。MLEモジュール内のファンクションを使用すると、住所レコードをデータベースに保持できます。データ・モデルは正規化されます。国、市区町村および実際の住所レコードには個別の表があります。JavaScriptのレコードをシリアライズする想定される実装は次のようになります。

CREATE OR REPLACE MLE MODULE rec_module
LANGUAGE JAVASCRIPT AS

export function insertPerson(personRec) {
    // Validate input: personRec must be a non-null object
    if (
        personRec === null ||
        personRec === undefined ||
        typeof personRec !== "object" ||
        Array.isArray(personRec)
    ) {
        throw new TypeError('insertPerson: "personRec" must be a non-null object');
    }
    // validate if the country exists
    let result = session.execute(
        "select country_id from country where country_name = :country_name",
        [personRec.COUNTRY_NAME],
    );
    // insert the country if it does not exist
    if (result.rows.length === 0) {
        session.execute(
            "insert into country (country_name) values (:country_name)",
            [personRec.COUNTRY_NAME],
        );
    }
    // validate if the city exists
    result = session.execute(
        "select city_id from city where city_name = :city_name",
        [personRec.CITY_NAME],
    );
    // insert the city if it does not exist
    let city_id;
    if (result.rows.length === 0) {
        result = session.execute(
            `insert into city (city_name) values (:city_name)
            returning city_id into :city_id`,
            {
                city_name: {
                    type: oracledb.STRING,
                    dir: oracledb.BIND_IN,
                    val: personRec.CITY_NAME,
                },
                city_id: {
                    type: oracledb.NUMBER,
                    dir: oracledb.BIND_OUT,
                },
            },
        );
        city_id = result.outBinds.city_id[0];
    } else {
        city_id = result.rows[0].CITY_ID;
    }
    // insert into the address table
    result = session.execute(
        `insert into address set
            surname     = :surname,
            firstname   = :firstname,
            street      = :street,
            city_id     = :city_id`,
        {
            surname: { val: personRec.SURNAME },
            firstname: { val: personRec.FIRSTNAME },
            street: { val: personRec.STREET },
            city_id: { val: city_id },
        },
    );

    return result.rowsAffected === 1;
}
/

モジュールrec_moduleinsertPerson()に含まれる唯一のファンクションは、レコード内のデータを分解し、それを使用して様々な表に移入します。

SQLおよびPL/SQLでこのファンクションを公開する場合は、次のようにパッケージ定義を拡張できます。

CREATE OR REPLACE PACKAGE rec_mle_js as 

    TYPE person_t IS RECORD (
        surname      VARCHAR2(100),
        firstname    VARCHAR2(100),
        street       VARCHAR2(100),
        city_name    VARCHAR2(50),
        country_name VARCHAR2(50)
    );

    FUNCTION insert_person(p_person person_t)
    RETURN BOOLEAN
    AS MLE MODULE rec_module
        SIGNATURE 'insertPerson(object)';
END rec_mle_js;
/

場合によっては、レコードに基づいてネストした表を実装することを選択できます。これは、データを保持する別のオプションです。データベース内JavaScriptでのネストした表の使用の詳細は、JavaScriptでのコレクションの使用を参照してください。

JavaScriptファンクションからレコードを返す

パラメータとしてファンクションにレコードを受け取るだけでなく、レコードを返すこともできます。

export function getPerson(id) {
    // Validate input: id must be a provided positive integer
    if (id === undefined || id === null) {
        throw new TypeError('getPerson: "id" is required');
    }
    if (typeof id !== "number" || !Number.isInteger(id) || id <= 0) {
        throw new TypeError('getPerson: "id" must be a positive integer');
    }

    // fetch the person's details from the database based on the ID
    // provided as input to the function
    const result = session.execute(
        `select
            a.address_id,
            a.firstname,
            a.surname,
            a.street,
            ci.city_name,
            co.country_name
        from
            address a 
            left join city ci on (a.city_id = ci.city_id)
            left join country co on (ci.country_id = co.country_id)
        where
            a.address_id = :address_id`,
        [id],
    );

    // raise an error if the fetch returned no rows
    if (result.rows.length !== 1) {
        throw new Error(`getPerson: cannot find a person with ID ${id}`);
    }

    // a call to session.getDbObjectClass() returns a DbObject prototype object,
    // representing the database type
    const person_t = session.getDbObjectClass("REC_MLE_JS.PERSON_T");

    // you can inspect it if you like
    // console.log(JSON.stringify(person_t.prototype));

    // Now that the object prototype has been found, an object can be created by passing a
    // JavaScript object to the constructor. Note that the keys are ALL IN UPPERCASE
    const person = new person_t({
            "SURNAME": result.rows[0].SURNAME,
            "FIRSTNAME": result.rows[0].FIRSTNAME,
            "STREET": result.rows[0].STREET,
            "CITY_NAME": result.rows[0].CITY_NAME,
            "COUNTRY_NAME": result.rows[0].COUNTRY_NAME,
        });

    return person;
}

PL/SQLによって返されたレコードの使用

ファンクション・パラメータおよびJavaScriptファンクションの戻り型に加えて、PL/SQLによって返されるレコードも、データベース内JavaScriptのコンテキストで使用できます。

CREATE OR REPLACE FUNCTION get_person_name("id" NUMBER)
RETURN VARCHAR2
AS MLE LANGUAGE JAVASCRIPT
{{
    // assume for the sake of this example that get_person() is implemented
    // as a public function in REC_MLE_PKG. It takes an ID as the search
    // parameter and returns a person record as defined earlier
    const result = session.execute(
        'begin :person := rec_mle_pkg.get_person(:id); end;',
        {
            person: {
                type: "REC_MLE_PKG.PERSON_T",
                dir: oracledb.BIND_OUT
            },
            id: {
                type: oracledb.NUMBER,
                val: id,
                dir: oracledb.BIND_IN
            }
        }
    );

    return `${result.outBinds.person.FIRSTNAME} ${result.outBinds.person.SURNAME}`;
}};
/

PL/SQL無名ブロックではなく、外部ファンクション・インタフェース(FFI)を使用することもできます。

CREATE OR REPLACE FUNCTION get_person_name_ffi("id" NUMBER)
RETURN VARCHAR2
AS MLE LANGUAGE JAVASCRIPT
{{
    // assume for the sake of this example that get_person() is implemented
    // as a public function in REC_MLE_PKG. It takes an ID as the search
    // parameter and returns a person record as defined earlier
    // this example uses the Foreign Function Interface (FFI) but is otherwise
    // identical to the previous one
    const rec_mle_pkg = plsffi.resolvePackage('REC_MLE_PKG');

    const person = rec_mle_pkg.get_person(id);

    return `${person.FIRSTNAME} ${person.SURNAME}`;

}};
/

この場合のように、FFIを使用すると、より簡潔でわかりやすいコードになる可能性があります。FFIの使用の詳細は、「PL/SQL外部ファンクション・インタフェースの概要」を参照してください。

JavaScriptでのコレクションの使用

Oracle AI Databaseでは、連想配列(または索引付き表)、可変サイズの配列(またはVARRAY)、ネストした表など、複数のコレクション型がサポートされています。これらのコレクション型は、データベース内JavaScriptでサポートされています。

コレクション・サポートのデータベース内JavaScript実装は、node-oracledbの実装と密接に連携しています。node-oracledb実装の詳細は、Node-oracledbのドキュメントを参照してください。

JavaScriptでPL/SQLファンクションに連想配列を渡す

コレクションに関する一般的なユースケースは、PL/SQL APIへのコレクションの提供です。

PL/SQLファンクションが連想配列を受け入れ、その要素の合計を返す例を考えてみます。「JavaScriptでのレコード・データ型の使用」の例に示すように、名前付きの型を使用して連想配列をバインドできますが、次の例で実装されているメソッドを使用する方が効率的です。ここでは、連想配列型の名前ではなく、各要素の型が使用されます。

この実装では、PL/SQL配列バインドに型を設定する必要があります。使用できるオプションは次のとおりです。
  • oracledb.STRING
  • oracledb.DB_TYPE_VARCHAR
  • oracledb.NUMBER
  • oracledb.DB_TYPE_NUMBER
  • oracledb.DB_TYPE_NVARCHAR
  • oracledb.DB_TYPE_CHAR
  • oracledb.DB_TYPE_NCHAR
  • oracledb.DB_TYPE_BINARY_FLOAT
  • oracledb.DB_TYPE_BINARY_FLOAT
  • oracledb.DB_TYPE_DATE
  • oracledb.DB_TYPE_TIMESTAMP
  • oracledb.DB_TYPE_TIMESTAMP_LTZ
  • oracledb.DB_TYPE_TIMESTAMP_TZ
  • oracledb.DB_TYPE_RAW

次の例では、2つのファンクションを使用します。associative_array_in()はその要素の合計を計算し、associative_array_out()は連想配列の要素を返します。

CREATE OR REPLACE PACKAGE aa_mle_pkg AS 

-- some setup code has been omitted for brevity

    TYPE numtype IS TABLE OF NUMBER INDEX BY BINARY_INTEGER;
    FUNCTION associative_array_in(p_values numtype) RETURN NUMBER;
    FUNCTION associative_array_out RETURN numtype;

END aa_mle_pkg;
/

JavaScriptでは、次のようにassociative_array_in()ファンクションを使用できます。

CREATE OR REPLACE PROCEDURE mle_associative_array_in
AS MLE LANGUAGE JAVASCRIPT
{{
    const result = session.execute(
        `begin
            :sum := aa_mle_pkg.associative_array_in(:values);
        end;`,
        // you can alternatively use positional binds, too
        {
            sum: {
                type: oracledb.NUMBER,
                dir: oracledb.BIND_OUT
            },
            values: {
                type: oracledb.NUMBER,
                dir: oracledb.BIND_IN,
                val: [ 1, 2, 3 ]
            }
        }
    );

    console.log(`the sum of numbers in the array is ${result.outBinds.sum}`);
}};
/

OUTおよびIN OUTバインドの場合、maxArraySizeバインド・プロパティを設定する必要があります。この値は、配列で返すことができる要素の最大数です。PL/SQLブロックがこの制限を超えてデータの挿入を試行すると、エラーが発生します。

コレクションを引数としてPL/SQLコード・ユニットに渡すのではなく、コードでPL/SQLによって返されるコレクションを受け入れる必要がある場合があります。前のAA_MLE_PKGを使用する例を続行して、associative_array_out()ファンクションの使用方法の例を次に示します。

CREATE OR REPLACE PROCEDURE mle_associative_array_out
AS MLE LANGUAGE JAVASCRIPT
{{

    const result = session.execute(
        "begin :values := aa_mle_pkg.associative_array_out; end;",
        {
            values: {
                dir: oracledb.BIND_OUT,
                type: "AA_MLE_PKG.NUMTYPE",
                maxArraySize: 3,
            },
        },
    );

    const res = result.outBinds.values;
    console.log(JSON.stringify(res));
}};
/

JavaScriptでPL/SQLファンクションにネストした表を渡す

連想配列に加えて、ネストした表は、データベース内JavaScriptを使用してコレクション型を作業に統合するもう1つのオプションです。この例では、ファンクションinitCapsはネストした表を入力として受け取り、各要素を大文字にした新しいリストを返します。

CREATE OR REPLACE PACKAGE nt_mle_js AS
    TYPE roster_t IS TABLE OF VARCHAR2(15);

    FUNCTION initCaps(p_list roster_t)
    RETURN JSON
    AS MLE MODULE nt_module
        SIGNATURE 'initCaps';
    
END;
/

CREATE OR REPLACE MLE MODULE nt_module
LANGUAGE JAVASCRIPT AS

    /**
     * Capitalizes each non-empty string in the given array:
     * first character uppercased, remaining characters lowercased.
     * Falsy elements (such as '', null, undefined) are returned as-is.
     * The input array is not mutated; a new array is returned.
     * This works only with US-ASCII characters and does not respect
     * non-English language input.
     *
     * @param {(Array<string>)} names - List of names to transform
     * @returns {(Array<string>)} A new list with each item init-capped
     * @throws {Error} If names is not a provided array
     *
     */

    export function initCaps(names) {
        if (! names || ! Array.isArray(names)) {
            throw new Error("Must provide a list of names to the initCaps function");
        }

        return names.map( (s) => s ? s[0].toUpperCase() +
                s.slice(1).toLowerCase() : s)
    }
/

DECLARE
    l_names nt_mle_js.roster_t := nt_mle_js.roster_t(
        'john',
        'paul',
        'ringo',
        'george'
    );
    l_uc_names JSON;
BEGIN
    l_uc_names := nt_mle_js.initCaps(l_names);

    DBMS_OUTPUT.PUT_LINE(
        JSON_SERIALIZE(
            l_uc_names
            pretty
        )
    );
END;
/

結果:

[
    "John",
    "Paul",
    "Ringo",
    "George"
]