使用JavaScript和Python实现Oracle数据库的存储过程?


本文研究了多语言引擎(MLE)如何在GraalVM的帮助下在Oracle数据库中使用JavaScript和Python,为数据密集型计算带来巨大的生态系统。使用GraalVM,我们不仅可以快速将新语言引入Oracle数据库,而且还可以轻松获得高性能的推测JIT编译器。它可用于为查询的关键部分生成有效代码,例如运行时的数据转换。
在数据库方面,SQL已经被证明是过去几十年中查询数据的首选语言,实现更复杂的业务逻辑,就会达到SQL的限制,这时存储过程出台了,但是人才匮乏,找到JavaScript或Python开发人员要比找到PL / SQL开发人员容易得多。
将新的编程语言与数据库系统集成包括嵌入一个全新的运行时,具有自己的内存管理,线程机制等。这显着增加了数据库系统的体系结构和代码库的复杂性。具有多语言功能的GraalVM及其对嵌入的支持为此问题提供了解决方案。只需要嵌入一个运行包,即可在数据库系统中实现多种编程语言的高性能实现。

多语言引擎
在Oracle,我们目前正致力于将GraalVM嵌入到Oracle数据库和MySQL中,我们将这些扩展称为多语言引擎(MLE)。在本文中,我们仅关注MLE for Oracle数据库,Oracle Database MLE目前是一项实验性功能。
除了将GraalVM嵌入到Oracle数据库之外,我们还开发了尽可能方便地使用MLE的工具。例如,我们目前正在开发自动打包整个应用程序并使用单个命令将其部署到Oracle数据库的工具。
另一个主要工作领域是我们在Oracle数据库中提供的语言。语言需要扩展才能变得有用。例如,我们需要一个可以在数据库类型和语言类型之间进行转换的转换引擎,以及Oracle数据库的SQL引擎和新语言的SQL API之间的桥梁。

MLE提供了两种不同的方式来执行由MLE支持的语言编写的代码。首先,存储过程和用户​​定义的函数可以用MLE语言编写。其次,提供了一个名为DBMS_MLE的新PL / SQL包,用于动态脚本,即在运行时定义匿名脚本并执行它们。

使用动态MLE临时执行脚本
DBMS_MLE可以执行在PL / SQL中以字符串形式给出的脚本。数据通过所谓的绑定变量在两个方向(输入和输出)与脚本交换。最后,脚本可以打印将放入数据库输出缓冲区的消息。我们来看一个具体的例子:

DECLARE
  script_source CLOB;
  script_handle DBMS_MLE.SCRIPT_HANDLE_T;
  script_result CLOB;
BEGIN
  script_source := q'~
    var hist = {"b2000": "", "b4000": "", "b6000": ""};
   
// mle.sql is an obect that gives access to the SQL engine
    for (var row of mle.sql.execute(
"SELECT SAL FROM EMP").rows) {
      if (row[0] < 2000)
        hist.b2000 +=
"█";
      else if (row[0] < 4000)
        hist.b4000 +=
"█";
      else if (row[0] < 6000)
        hist.b6000 +=
"█";
    }
    console.log(JSON.stringify(hist));
   
// mle.binds is an object that holds all bind variables
    mle.binds.hello = mle.binds.hello.replace(
"PL/SQL", "Graal JavaScript " + Graal.versionJS);
    ~';
  -- create a new MLE script for the JavaScript (JS) code above
  script_handle := DBMS_MLE.CREATE_SCRIPT('JS', script_source);
  -- assign
"Hello from PL/SQL" to a bind variable named "hello"
  DBMS_MLE.BIND_VARIABLE(script_handle, 'hello', 'Hello from PL/SQL');
  -- execute the dynamic MLE script
  DBMS_MLE.EXECUTE_SCRIPT(script_handle);
  -- store the value of
"hello" after script execution in "script_result"
  DBMS_MLE.VARIABLE_VALUE(script_handle, 'hello', script_result);
  -- remove the 
  DBMS_MLE.DROP_SCRIPT(script_handle);
  DBMS_OUTPUT.PUT_LINE(script_result);
END;

示例中的匿名PL / SQL块使用变量script_source来保存JavaScript代码片段。此变量传递给函数DBMS_MLE.CREATE_SCRIPT()以创建新的Dynamic MLE脚本,然后可以通过函数DBMS_MLE.EXECUTE_SCRIPT()执行该脚本。
在我们执行脚本之前,我们通过DBMS_MLE.BIND_VARIABLE()定义并设置一个名为hello的绑定变量。可以在周围的PL / SQL程序或Dynamic MLE脚本中定义,设置和读取绑定变量。
该脚本使用内置的MLE SQL驱动程序(自动作为mle.sql提供)来查询EMP中所有员工的工资表。出于演示目的,我们为工资创建一个简单的直方图,并将其放入输出缓冲区(console.log())。在将控制权转移回PL / SQL之前,脚本操纵绑定变量hello。然后,PL / SQL块使用DBMS_MLE.VARIABLE_VALUE()函数提取绑定变量的值,并将其打印到输出缓冲区。要执行PL / SQL块,我们可以将它作为单个语句从任何客户端发送到数据库。例如,可以将整个块复制到SQL * Plus会话中,并通过输入斜杠字符来执行。
在执行上面的匿名PL / SQL块之后(例如,在SQL * Plus中),数据库输出缓冲区将具有以下内容(在SQL * Plus中显示SET SERVEROUTPUT ON或使用DBMS_OUTPUT.GET_LINE()来检索):

{"b2000":"████████","b4000":"█████","b6000":"█"}
Hello from Graal JavaScript 1.0

当然,我们可以轻松地使用Python来完成同样的事情:

DECLARE
  script_source CLOB;
  script_handle DBMS_MLE.SCRIPT_HANDLE_T;
  script_result CLOB;
BEGIN
  script_source := q'~
hist = { 'b2000': '', 'b4000': '', 'b6000': '' }
# mle.sql is an obect that gives access to the SQL engine
for row in mle.sql.execute('SELECT SAL FROM EMP'):
    if row[0] < 2000:
        hist['b2000'] += '█'
    elif row[0] < 4000:
        hist['b4000'] += '█'
    elif row[0] < 6000:
        hist['b6000'] += '█'
print(hist);
import sys
# mle.binds is a dictionary that holds all bind variables
mle.binds['hello'] = mle.binds['hello'].replace('PL/SQL', 'Python ' + sys.version);
    ~';
  -- create a new MLE script for the Python code above
  script_handle := DBMS_MLE.CREATE_SCRIPT('PYTHON', script_source);
  -- assign "Hello from PL/SQL" to a bind variable named "hello"
  DBMS_MLE.BIND_VARIABLE(script_handle, 'hello', 'Hello from PL/SQL');
  -- execute the dynamic MLE script
  DBMS_MLE.EXECUTE_SCRIPT(script_handle);
  -- store the value of
"hello" after script execution in "script_result"
  DBMS_MLE.VARIABLE_VALUE(script_handle, 'hello', script_result);
  -- remove the 
  DBMS_MLE.DROP_SCRIPT(script_handle);
  DBMS_OUTPUT.PUT_LINE(script_result);
END;

跨语言的组件共享​​​​​​​
在我们的第一个示例中,我们介绍了MLE SQL驱动程序,并展示了如何在JavaScript和Python中使用它。它看起来像是用过的语言实现的模块,但事实并非如此。我们不是为我们添加的每种语言实现从语言的SQL API到Oracle数据库的SQL引擎的完整桥接,而是由于GraalVM的多语言功能,我们必须只完成一次工作的主要部分。
简而言之,多语言功能允许在GraalVM上运行的语言访问对象并调用属于另一种语言的函数。因此,我们将所有语言(如数据转换和MLE SQL驱动程序)所需的基本组件实现为可以直接从所有其他语言使用的新内部语言。为了实现新语言,GraalVM提供了Truffle框架我们用于此目的。我们在每个MLE语言之上添加了一个特定于语言的瘦层,以隐藏一些内部结构并使它们看起来非常原生。
Truffle框架不仅可以实现可共享组件,还可以充分利用GraalVM的推测JIT编译器。在数据库的背景下,后者具有极其重要的意义,因为数据转换通常是主要的成本因素。

MLE存储过程
虽然在许多情况下运行以现代语言编写的脚本很方便,但它并不适合开发大型和复杂的应用程序。动态MLE需要PL / SQL中的骨架,并且不能直接使用第三方库。此外,代码最好由数据库管理,类似于数据。为了释放MLE的全部功能,我们允许以由用户定义的函数和存储过程组成的模块的形式持久地在数据库中存储和维护用户代码。对于无痛包装和模块部署,我们计划提供外部工具,通过单个命令完成所有工作。

存储过程允许开发人员运行需要在数据库服务器进程内执行多个SQL语句的代码。这避免了数据库客户端(通常是中间件)与数据库之间昂贵的网络往返。今天,Oracle数据库允许开发人员在PL / SQL或Java中实现存储过程。使用MLE,开发人员还可以使用JavaScript和Python实现存储过程。

假设我们想提高员工的工资,但禁止非经理人的薪水超过10,000美元。我们可以从一个JavaScript函数开始,该函数更新员工的薪水并返回新薪水:

const sql = _dbRequire('@oracle/sql');
function updateGetSal(empno, raise) {
  if (sql.execute('UPDATE emp SET sal = sal + :1 WHERE empno = :2',
      [raise, empno]).rowsAffected != 0) {
    const row = sql.execute('SELECT sal FROM emp WHERE empno = :1',
        [empno]).firstRow();
    return row[0];
  }
  return false;
}

请注意,为了提高安全性和性能,我们在SQL语句中使用绑定变量。在这种特殊情况下,我们通过给出一个值数组来设置绑定变量的值。这意味着数组[raise,empno]中的值的位置确定它替换的绑定变量(即,第一个绑定变量将设置为raise的值,第二个绑定变量将设置为empno的值) 。或者,可以按名称设置绑定变量。
接下来,我们可以定义一个函数来检查员工是否是经理:

function isManager(empno) {
  const row = sql.execute('SELECT count(*) FROM emp WHERE mgr = :1',
      [empno]).firstRow();
  return row[0] > 0;
}

有了这两个辅助函数,我们现在可以实现我们的业务逻辑:

module.exports.salraise = function(empno, raise) {
const newsal = updateGetSal(empno, raise);
if (newsal && newsal > 10000 && !isManager(empno)) {
sql.execute('ROLLBACK');
} else {
sql.execute('COMMIT');
}
};

对module.exports的赋值用于将函数salraise()导出到数据库。将所有内容放在名为load_salraise.js的文件中,我们可以添加其他代码来执行部署到数据库中:

const async = require('async');
const oracledb = require('oracledb');

// the name of the module being created in the database
const moduleName = 'raise.js';

// database user
const dbUser = 'scott';

// database user password
const dbPwd = 'tiger';

// database connection string
const dbInstance = 'hr.us.example.com/hr.rdbms.dev.us.oracle.com';

oracledb.autoCommit = true;

const doConnect = function(cb) {
  oracledb.getConnection(
      {
        user: dbUser,
        password: dbPwd,
        connectString: dbInstance,
      },
      cb);
};

const doCleanup = function(conn) {
  conn.close(function(err) {
    if (err) {
      console.error(err.message);
    }
  });
};

const source = `
const sql = _dbRequire('@oracle/sql');
function updateGetSal(empno, raise) {
  if (sql.execute('UPDATE emp SET sal = sal + :1 WHERE empno = :2',
      [raise, empno]).rowsAffected != 0) {
    const row = sql.execute('SELECT sal FROM emp WHERE empno = :1',
        [empno]).firstRow();
    return row[0];
  }
  return false;
}
function isManager(empno) {
  const row = sql.execute('SELECT count(*) FROM emp WHERE mgr = :1',
      [empno]).firstRow();
  return row[0] > 0;
}
module.exports.salraise = function(empno, raise) {
  const newsal = updateGetSal(empno, raise);
  if (newsal && newsal > 10000 && !isManager(empno)) {
    sql.execute('ROLLBACK');
  } else {
    sql.execute('COMMIT');
  }
};
`;

const createProcedure = function(conn, cb) {
  conn.execute(
      'CREATE OR REPLACE JAVASCRIPT SOURCE NAMED
"' + moduleName + '" AS ' +
      source,
      function(err, result) {
        if (err) {
          return cb(err, conn);
        }
        conn.execute(
            'CREATE OR REPLACE PROCEDURE raiseSal (empno NUMBER, raise ' +
            'NUMBER) AS LANGUAGE JAVASCRIPT NAME ' +
            '\'raise.js.salraise(empno number, raise number)\';',
            function(err, result) {
              if (err) {
                return cb(err, conn);
              }
              return cb(null, conn);
            });
      }
  );
};

async.waterfall(
    [
      doConnect,
      createProcedure,
      doCleanup,
    ],
    function(err, conn) {
      if (err) {
        console.error('In waterfall error cb: ==>', err, '<==');
      }
      if (conn) {
        doCleanup(conn);
      }
    });

我们现在可以运行部署模块代码的脚本,并通过Node.js 将函数salraise()注册为存储过程:

$ npm install oracledb async
$ node load_salraise.js

可以像调用任何其他过程一样调用新创建的JavaScript存储过程。例如,从SQL * Plus:

$ sqlplus scott/tiger

SQL> SELECT sal FROM emp WHERE empno = 7369;

       SAL
----------
       800

SQL> CALL salraise(7369, 200);

Call completed.

SQL> SELECT sal FROM emp WHERE empno = 7369;

       SAL
----------
      1000