在Dialects之间翻译存储程序的详细指南

在过去的几年里,我们投入了大量的精力来改善我们在jOOQ中的程序语言能力。最初是一个简单的内部API,支持像这样的DDL条款的模拟:

-- Some dialect that supports this
create table if not exists t (i varchar(10));

-- Db2
begin
  declare continue handler for sqlstate '42710' begin end;
  execute immediate 'create table T (I varchar(10))';
end

-- Oracle
begin
  execute immediate 'create table T (I varchar2(10))';
exception
  when others then
    if sqlerrm like 'ORA-00955%' then null;
    else raise;
    end if;
end;

-- SQL Server
begin try
  create table T (I varchar(10))
end try
begin catch
  if error_number() != 2714 throw;
end catch

...发展成为一个成熟的API,用于在数据库服务器中执行各种过程性逻辑。

匿名块

上面的例子显示了大多数RDBMS称之为 "匿名块",类似于Java的匿名类,即没有名字的过程性逻辑元素。

根据数据库的情况,这些块会被即时解释,或者像普通SQL语句一样被编译和缓存。由于各种原因,它们可能非常有用:

  • 创建原子式的临时代码单元,在一次服务器往返中执行,类似于上述带有集成异常处理的DDL脚本
  • 创建动态程序性代码。这对许多人来说可能是深奥的,但对其他人来说却是正确的做法。所有的jOOQ都是关于动态SQL的,那么为什么不包括动态PL/SQL、T-SQL、pgplsql等?
  • 为了绕过康威法则带来的限制,当你没有办法得到必要的GRANT或其他官僚主义的令牌,在生产中酌情部署你的程序。我的意思是,这在很多公司仍然是一件事。
  • Conway's Law的一个较小的例子是,当你是一个产品供应商时,你不知道你是否可以在客户的生产系统上创建程序。就不要这样做。如果你不能,就以匿名块的形式运行你的过程性逻辑,如果你能,就以过程的形式运行。同样的jOOQ代码。
  • 如果你的过程性代码变化非常频繁(甚至是动态的),存储它可能会引起棘手的问题。如果你曾经在Oracle工作过,遇到过可怕的无闩事件,你就知道我的意思。

我绝不是在提倡你应该使用匿名块而不是一般的存储过程。如果你可以的话,把你的代码存储在数据库中以获得更好的性能和重复使用。但有时你不能这样做,有时你也不应该这样做。

所以,jOOQ一如既往地支持--各种过程性逻辑元素的混合,包括:

  • 带有变量声明的区块
  • IF 声明
  • 循环,包括LOOP,WHILE,REPEAT,FOR 循环
  • EXIT (或 )和 (或 )用于循环控制流LEAVE CONTINUE ITERATE
  • RETURN 从程序或函数返回
  • GOTO (ghasp!)
  • SIGNALRAISE
  • 标签
  • CALL 语句来调用其他存储程序
  • EXECUTE 语句(用于从程序逻辑中运行动态SQL,这是哪一级的概念?)

而且我们一直在增加更多的支持。Java代码可能看起来是这样的:

Variable<Integer> i = var(name("i"), INTEGER);
ctx.begin(
  for_(i).in(1, 10).loop(
    insertInto(T).columns(T.COL).values(i)
  )
).execute();

假设你因为某些原因不能运行批量插入语句,这可能是一个办法。它可以翻译成各种方言,如下所示。

Db2 和MySQL(它不支持匿名块,但支持语句批处理)

begin
  declare I bigint;
  set I = 1;
  while I <= 10 do
    insert into T (COL) values (I);
    set I = (I + 1);
  end while;
end;

PostgreSQL

do $$
begin
  for I in 1 .. 10 loop
    insert into T (COL) values (I);
  end loop;
end;
$$

Oracle

begin
  for I in 1 .. 10 loop
    insert into T (COL) values (I);
  end loop;
end;

SQL服务器

begin
  declare @I bigint = 1;
  while @I <= 10 begin
    insert into T (COL) values (I);
    set @I = (@I + 1);
  end;
end;

与jOOQ一样,你不必一开始就编写基于jOOQ API的代码。当你的程序性(或SQL)逻辑是动态的,这是推荐的方法,但jOOQ也可以解析和翻译字符串形式的静态SQL。这就是SQL的babelfish。在这里玩一玩,了解更多:https://www.jooq.org/translate/

将代码存储为一个过程

如果你没有上述的使用情况,你会想把这些代码存储为一个过程(或函数):

  • 为了更好地重复使用
  • 为了获得更好的性能

在这种情况下,从jOOQ 3.15开始,你可以使用我们的 [CREATE PROCEDURE](https://www.jooq.org/doc/dev/manual/sql-building/ddl-statements/create-statement/create-procedure-statement/), [CREATE FUNCTION](https://www.jooq.org/doc/dev/manual/sql-building/ddl-statements/create-statement/create-function-statement/),甚至是 [CREATE TRIGGER](https://www.jooq.org/doc/dev/manual/sql-building/ddl-statements/create-statement/create-trigger-statement/)支持。

注意: CREATE PACKAGE 是我们的愿望清单上的重点,但可能不会再进入3.15。如果包只用于命名,它们可能会被其他方言的模式所模仿。其他的包级特征,例如包的状态可能更难翻译。

前面的匿名块可以很容易地被包裹在一个 [DSLContext.createProcedure()](https://www.jooq.org/javadoc/dev/org.jooq/org/jooq/DSLContext.html#createProcedure(org.jooq.Name))调用

Variable<Integer> i = var("i", INTEGER);
Parameter<Integer> i1 = in("i1", INTEGER);
Parameter<Integer> i2 = in("i2", INTEGER);

ctx.createProcedure("insert_into_t")
   .parameters(i1, i2)
   // You may or may not wrap your block in BEGIN .. END.
   // jOOQ will figure it out per dialect...
   .as(for_(i).in(i1, i2).loop(
     insertInto(T).columns(T.COL).values(i)
   ))
   .execute();

这将产生以下程序:

Db2和MySQL

create procedure INSERT_INTO_T(
  I1 integer,
  I2 integer
)
begin
  declare I bigint;
  set I = I1;
  while I <= I2 do
    insert into T (COL) values (I);
    set I = (I + 1);
  end while;
end;

MariaDB

create procedure INSERT_INTO_T(
  I1 int,
  I2 int
)
begin
  for I in I1 .. I2 do
    insert into T (COL) values (I);
  end for;
end;

Oracle

create procedure INSERT_INTO_T(
  I1 number,
  I2 number
)
as
begin
  for I in I1 .. I2 loop
    insert into T (COL) values (I);
  end loop;
end;

PostgreSQL

create procedure INSERT_INTO_T(
  I1 int,
  I2 int
)
language plpgsql
as
$$
begin
  for I in I1 .. I2 loop
    insert into T (COL) values (I);
  end loop;
end;
$$

SQL服务器

create procedure INSERT_INTO_T
  @I1 int,
  @I2 int
as
begin
  declare @I bigint = @I1;
  while @I <= @I2 begin
    insert into T (COL) values (@I);
    set @I = (@I + 1);
  end;
end;

在这里玩一玩,了解更多:https://www.jooq.org/translate/。我们也期待着你的错误报告和/或功能请求:https://github.com/jOOQ/jOOQ/issues/new/choose。

棘手的转换

程序性语言通过ISO/IEC 9075-4标准进行标准化,一些RBDMS令人惊讶地在很大程度上同意该标准,包括:

  • Db2
  • HSQLDB
  • MariaDB
  • MySQL

其他的就不太一样了,但是所有的过程性语言都同意他们是非常简单的语言,没有任何像子类型或参数多态性这样的 "花哨 "的东西(好吧,PL/SQL有一些子类型多态性,但不是非常复杂的一种。我们暂时不支持它),lambda表达式,动态调度,代数数据类型,等等。

它们的共同点是与SQL语言紧密结合,这也是它们的闪光之处。

但是,还是有一些微妙的区别。例如,它们在你可以声明变量的地方有所不同。有些有块范围,有些没有。有的坚持标准,即LEAVE ,有的则不需要标签。

想象一下,你写了这样一段 "幻想中的 "jOOQ代码:

Name t = unquotedName("t");
Name a = unquotedName("a");
Variable<Integer> i = var(unquotedName("i"), INTEGER);

ctx.begin(
     insertInto(t).columns(a).values(1),
     declare(i).set(2),
     loop(
       insertInto(t).columns(a).values(i),
       i.set(i.plus(1)),
       if_(i.gt(10)).then(loop(exit()), exit())
     )
   )
   .execute();

这只是原始循环的一个更复杂的版本,它将数值1-10插入到一个表中。除了展示loop(exit()) 的嵌套的转换能力,以及用EXIT 的无限LOOP ,而不是用索引的FOR 循环外,没有别的原因。

在一些方言中,有一些事情并不总是完全像这样工作!

让我们看看Db2是如何处理这个问题的

begin
  -- Variable declarations need to be "pulled up" to the beginning
  -- of the block, i.e. before the INSERT statement
  declare i integer;
  insert into t (a) values (1);

  -- While irrelevant to this example, the init value for the
  -- variable declaration must remain at the original location
  set i = 2;

  -- We need a label to be able to leave this loop
  alias_1:
  loop
    insert into t (a) values (i);
    set i = (i + 1);
    if i > 10 then

      -- Same here, a label is required
      alias_2:
      loop
        leave alias_2;
      end loop;
      leave alias_1;
    end if;
  end loop;
end

如果我们不在一个循环上使用EXIT ,那么就不会有一个标签。或者,你显然可以明确地给你的循环贴上标签,这总是被推荐的。但有时,你的原始源代码中并没有这个标签。

Oracle是如何处理这个问题的呢?

Oracle在这里有一个稍微不同的语法:

declare
  i number(10);
begin
  insert into t (a) values (1);
  i := 2;
  loop
    insert into t (a) values (i);
    i := (i + 1);
    if i > 10 then
      loop
        exit;
      end loop;
      exit;
    end if;
  end loop;
end;

主要的区别是,声明也是拉起来的,但是需要一个单独的DECLARE 块来声明BEGIN .. END 以外的变量。无标签的EXIT 是原生支持的,所以这里不需要进行任何转换。

如果你对这种转换的工作方式感兴趣,请阅读这篇文章

总结

无论你是要从一种方言迁移到另一种方言,还是要同时支持几种方言,还是要编写动态SQL和动态过程性逻辑,或者你只是喜欢用Java而不是本地SQL来写东西,或者你患有Conway's Law,不能轻易存储你的过程性代码,jOOQ都可以帮助你完成这些工作。

一段时间以来,jOOQ已经支持程序性语句作为最流行的方言的匿名块。从jOOQ 3.15开始,我们还将支持以一种与方言无关的方式将这种逻辑存储在数据库中,以及在我们的网站上解析/翻译程序代码,或作为一个库/CLIJDBC代理,在传统的JDBC应用中临时取代你的SQL/程序代码。

请继续关注jOOQ发展的这一令人振奋的领域的更多信息!

猜你喜欢

转载自juejin.im/post/7126373711703179272