Oracle 数据库中的 JavaScript

本页介绍如何使用 Oracle 数据库多语言引擎 (MLE) 在 Oracle 数据库中运行 JavaScript。MLE 由 GraalVM 提供支持:它可以在 Linux x64 上的 Oracle Database 23ai(及更高版本)中运行 JavaScript 代码。

准备工作

  1. 获取一个 Oracle Cloud 免费套餐账户,并选择一个提供 Oracle Database 23ai 或更高版本的主区域。
  2. 预置一个自治数据库事务处理实例,然后启动您喜欢的 SQL IDE,例如以下之一

    先决条件:请参阅 Oracle Database 23c Free - Developer Release 中 JavaScript 简介中标题为“在 Oracle Database 23c Free - Developer Release 中使用 JavaScript 的先决条件”的部分。(尽管针对的是早期版本,但它们仍然有效。)

示例

这些示例假设您已经创建了一个数据库账户,该账户具有创建 MLE 模块和环境以及 PL/SQL 函数、过程和包所需的权限。该账户还被授予执行 JavaScript 代码的权限。(有关更多信息,请参阅 MLE 安全。)

  1. 第一个示例展示了如何创建 MLE 模块。

     create or replace mle module helper_module_inline
     language javascript as
    
     /**
     * Convert a delimited string into key-value pairs and return JSON
     * @param {string} inputString - the input string to be converted
     * @returns {JSON}
     */
     function string2obj(inputString) {
         if ( inputString === undefined ) {
             throw `must provide a string in the form of key1=value1;...;keyN=valueN`;
         }
         let myObject = {};
         if ( inputString.length === 0 ) {
             return myObject;
         }
         const kvPairs = inputString.split(";");
         kvPairs.forEach( pair => {
             const tuple = pair.split("=");
             if ( tuple.length === 1 ) {
                 tuple[1] = false;
             } else if ( tuple.length != 2 ) {
                 throw "parse error: you need to use exactly one '=' between " +
                         "key and value and not use '=' in either key or value";
             }
             myObject[tuple[0]] = tuple[1];
         });
         return myObject;
     }
    
     /**
     * Convert a JavaScript object to a string
     * @param {object} inputObject - the object to transform to a string
     * @returns {string}
     */
     function obj2String(inputObject) {
         if ( typeof inputObject != 'object' ) {
             throw "inputObject isn't an object";
         }
         return JSON.stringify(inputObject);
     }
    
     export { string2obj, obj2String }
     /
    

    注意:MLE 仅支持 ECMA Script 模块。CommonJS 和其他常用技术不可用。

    上述模块定义了两个函数

    1. string2obj()
    2. obj2Str()

    模块末尾的 export 关键字表示这些函数可以被其他 MLE 模块、PL/SQL 以及 SQL 调用。

  2. 第二个示例展示了如何使这些函数在 SQL 和 PL/SQL 中可用。JavaScript 模块中的函数不能直接在 SQL 和 PL/SQL 中调用。相反,您需要为它们创建调用规范。调用规范可以被认为是 PL/SQL 中的“包装器”。根据您的要求,您可以使用独立的函数和过程,或将它们分组到一个包中。由于这两个函数属于 helper_module,它们将放入一个包中。

     create or replace package helper_pkg as
    
         function string2obj(
             p_inputString varchar2
         ) return JSON
             as mle module helper_module_inline
             signature 'string2obj';
    
         function obj2String(
             p_inputObject JSON
         ) return varchar2
             as mle module helper_module_inline
             signature 'obj2String';
    
     end helper_pkg;
     /
    

    有了调用规范,您可以将一个由 ; 分隔的字符串转换为 JavaScript 对象,并将其返回给调用者,如下所示

     select json_serialize(
         helper_pkg.string2obj(
             'order_id=1;order_date=2023-04-24T10:27:52;order_mode=mail;promotion_id=1'
         )
         pretty
     ) string2obj;
    

    您应该看到以下输出

     STRING2OBJ
     ----------------------------------------
     {
         "order_id" : "1",
         "order_date" : "2023-04-24T10:27:52",
         "order_mode" : "mail",
         "promotion_id" : "1"
             }
    
  3. 从 Oracle Database 23.3 开始,如果您只需要调用一段标准的 JavaScript 代码,则可以在调用规范中内联提供 JavaScript 代码,如下所示

     create or replace function date_to_epoch(
       "ts" timestamp
     ) return number
     as mle language javascript
     ;
     /
    

    内联 JavaScript 过程有一些限制,例如它们不能导入其他 MLE 模块(但它们可以要求 MLE 附带的库)。这就是为什么您应该考虑使用 MLE 模块来编写更复杂的逻辑和/或使其在其他模块中可重用。您现在可以直接从 SQL 调用 date_to_epoch() 函数,如本例所示

     select
       date_to_epoch (
         to_timestamp('2000-01-01 12:00:00', 'yyyy-mm-dd hh24:mi:ss')
       ) seconds_since_epoch
     /
    
  4. 您可以从其他模块调用 MLE 模块。这通常发生在创建更复杂逻辑或第三方模块时。将流行的 validator.js 模块加载到数据库中,如 在 Oracle Database 23c Free - Developer Release 中使用 JavaScript 社区模块中所述——遵守许可证和合规性等注意事项。现在,您可以从您喜欢的内容分发网络 (CDN) 导入该模块的 ECMAScript (ESM) 版本,如上述文章所述。假设验证器模块在数据库中创建为 validator_module。您可以在自己的代码中如下使用该模块

     create or replace mle module validator_usage_demo_module
     language javascript as
    
     import validator from 'validator';
    
     export function emailCheck(str) {
       return validator.isEMail(str);
     }
    

    将导入名称 (validator) 映射到模块的元素称为 MLE 环境。由于没有 MLE 可以用来解析导入名称的文件系统,您必须明确声明映射,如下所示

     create or replace mle env validator_env imports (
       'validator' module validator_module
     );
    

    MLE 环境有助于将调用规范中的导入名称映射到模块。如果您想公开 SQL 函数 emailCheck(),请创建以下调用规范

     create or replace function email_check(p_str varchar2)
     return boolean
     as mle module validator_usage_demo_module
     env validator_env
     signature 'emailCheck';
     /
    

    现在您几乎可以在任何地方调用 email_check

类型转换

了解类型转换的工作原理非常重要——无论何时您的代码“离开”SQL 或 PL/SQL,类型都必须转换为其对应的 JavaScript 类型。(有关类型转换的更多信息,请参阅 MLE 类型转换。)

您可能知道 JavaScript Number 是以双精度 64 位二进制格式 IEEE 754 值实现的。它可能存在可存储的最大数值以及精度损失问题。以下示例演示了潜在的精度损失。

create table demo_table (
  id      number constraint pk_demo_table primary key,
  value   number not null
);

insert into demo_table(
  id,
  value
) values
  (1, 0.1),
  (2, 0.2);

create or replace procedure precision_loss(
  "id1" demo_table.id%type,
  "id2" demo_table.id%type)
as mle language javascript
;
/

begin precision_loss(1, 2); end;
/

precision_loss() 过程在控制台中打印的值为 0.30000000000000004(而不是 0.3)。为了避免精度损失,您可以遵循 从 MLE JavaScript SQL 驱动程序调用 PL/SQL 和 SQL 中列出的许多策略。一种方法是指示 SQL 驱动程序将数字作为 OracleNumber 类型返回:这是 Oracle 在 JavaScript 中 Number 数据类型的包装器类型

create or replace procedure precision_preserved(
  "id1" demo_table.id%type,
  "id2" demo_table.id%type)
as mle language javascript
;
/

begin precision_preserved(1, 2); end;
/

precision_preserved() 过程现在在控制台中打印正确的结果 (0.3)。包装器数据类型在 mle-js-plsqltypes 模块中有所记载。每当您需要数字精度时,请考虑使用其中一种包装器数据类型。

动态 JavaScript 执行

Oracle Database 21c 以 DBMS_MLE 包的形式引入了 MLE。它仍然可用,但其重点已转向框架开发人员和嵌入到 REPL(读取-评估-打印循环)系统中。APEX、Database Actions 及其他工具在底层使用该包。

以下是使用 DBMS_MLE 编码的基本示例

set serveroutput on;
declare
    l_ctx           dbms_mle.context_handle_t;
    l_source_code   clob;
begin
    -- Create execution context for MLE execution
    l_ctx    := dbms_mle.create_context();

    -- using q-quotes to avoid problems with unwanted string termination
    l_source_code :=
q'~

    const result = session.execute(
        `select 'hello, world'`,
        [],
        {
            outFormat: oracledb.OUT_FORMAT_ARRAY
        }
    );

    const message = result.rows[0][0];

    console.log(message);

   ~';
    dbms_mle.eval(
        context_handle => l_ctx,
        language_id => 'JAVASCRIPT',
        source => l_source_code,
        source_name => 'example01'
    );

    dbms_mle.drop_context(l_ctx);
exception
    when others then
        dbms_mle.drop_context(l_ctx);
        raise;
end;
/

匿名 PL/SQL 块在控制台打印 hello, world。如果您想使用 MLE 内置模块之外的其他模块,则必须使用 MLE 环境。基于先前创建的 validator_env 所奠定的基础,您可以如下使用 DBMS_MLE 验证电子邮件

set serveroutput on;
declare
    l_ctx           dbms_mle.context_handle_t;
    l_source_code   clob;
begin
    -- Create execution context for MLE execution and provide an environment
    l_ctx    := dbms_mle.create_context('VALIDATOR_ENV');

    -- using q-quotes to avoid problems with unwanted string termination
    l_source_code :=
q'~
(async() => {
  const { default: validator } = await import ('validator');
  const str = 'not an email address';

  console.log(`Is ${str} a valid email address? ${validator.isEmail(str)}`);
})()
   ~';
    dbms_mle.eval(
        context_handle => l_ctx,
        language_id => 'JAVASCRIPT',
        source => l_source_code,
        source_name => 'example02'
    );

    dbms_mle.drop_context(l_ctx);
exception
    when others then
        dbms_mle.drop_context(l_ctx);
        raise;
end;
/

您不能使用静态 import 语句,因为 DBMS_MLE 在脚本模式下评估 JavaScript 代码。但是,您可以使用如上述片段所示的动态导入。

APEX 和 Database Actions 是很棒的工具,因为它们允许您专注于编写 JavaScript(在上述示例中为 l_source_code)。

联系我们