从Java中调用存储程序的最佳方法(附实例)

67 阅读4分钟

jOOQ主要以其强大的类型安全、嵌入式、动态SQL功能而闻名,这些功能是通过代码生成来实现的。然而,代码生成的一个次要用例是将其用于存储过程(可能是专门用于存储过程)。

存储过程是将复杂的数据处理逻辑转移到服务器上的强大方式。由于性能的原因,这应该比大多数应用程序做得更频繁。例如,请看这篇关于节省服务器往返次数的文章。但它也可以作为一种实用的方法,向客户提供API,并向客户隐藏基于SQL的细节(如模式、表结构、事务脚本等),如果这对一个应用/团队来说是有用的。

在任何情况下,jOOQ都会通过生成所有函数、过程、包、UDT等的存根来大大帮助你。

一个程序的例子

在Oracle中,一个简单的示例程序是这样的:

CREATE OR REPLACE PROCEDURE my_proc (
  i1 NUMBER,
  io1 IN OUT NUMBER,
  o1 OUT NUMBER,
  o2 OUT NUMBER,
  io2 IN OUT NUMBER,
  i2 NUMBER
) IS
BEGIN
  o1 := io1;
  io1 := i1;

  o2 := io2;
  io2 := i2;
END my_proc;

它使用IN,OUT, 和IN OUT 参数。当用JDBC调用这个过程时,我们必须写一些类似的东西:

try (CallableStatement s = c.prepareCall(
    "{ call my_proc(?, ?, ?, ?, ?, ?) }"
)) {

    // Set all input values
    s.setInt(1, 1); // i1
    s.setInt(2, 2); // io1
    s.setInt(5, 5); // io2
    s.setInt(6, 6); // i2

    // Register all output values with their types
    s.registerOutParameter(2, Types.INTEGER); // io1
    s.registerOutParameter(3, Types.INTEGER); // o1
    s.registerOutParameter(4, Types.INTEGER); // o2
    s.registerOutParameter(5, Types.INTEGER); // io2

    s.executeUpdate();

    System.out.println("io1 = " + s.getInt(2));
    System.out.println("o1 = " + s.getInt(3));
    System.out.println("o2 = " + s.getInt(4));
    System.out.println("io2 = " + s.getInt(5));
}

这种方法存在着各种问题:

  • 通常的参数索引是容易出错的。如果你再增加一个参数,索引就会转移,这就很难管理。你可以使用命名的参数,但这样你还是会有错别字,而且不是所有的JDBC驱动都支持这个。不过,它们都支持索引参数。
  • 在API中,IN,IN OUT, 和OUT 参数之间没有明显的区别。你必须知道 哪个参数有哪个模式。JDBC的API在这里没有帮助你。
  • 你还必须知道哪个参数是哪种类型的,并正确地掌握这个问题。

还有许多其他注意事项和细节,但这些是最重要的。

使用jOOQ生成的代码

jOOQ的代码生成器只是为这个程序生成了一个存根。或者说,2个存根。一个带参数的调用模型类,以及一个允许在单个方法调用中调用该过程的方便方法。这就是它的模样:

// Generated code
public class MyProc extends AbstractRoutine<java.lang.Void> {

    // [...]
    private static final long serialVersionUID = 1L;

    public void setI1(Number value) {
        setNumber(I1, value);
    }

    public void setIo1(Number value) {
        setNumber(IO1, value);
    }

    public void setIo2(Number value) {
        setNumber(IO2, value);
    }

    public void setI2(Number value) {
        setNumber(I2, value);
    }

    public BigDecimal getIo1() {
        return get(IO1);
    }

    public BigDecimal getO1() {
        return get(O1);
    }

    public BigDecimal getO2() {
        return get(O2);
    }

    public BigDecimal getIo2() {
        return get(IO2);
    }
}

Oracle生成的代码对输入值使用Number ,对输出值使用BigDecimal ,以绑定到NUMBER 类型。其他的RDBMS支持INTEGER 类型,以防你的代码更多的使用这种类型。你显然可以使用强制类型,就像使用表一样,重写jOOQ代码生成器中的数据类型定义。

所以,现在是调用程序的一种方式:

MyProc call = new MyProc();
call.setI1(1);
call.setIo1(2);
call.setIo2(5);
call.setI2(6);

// Use the usual jOOQ configuration, e.g. the one configured by
// Spring Boot, etc.
call.execute(configuration);

System.out.println("io1 = " + call.getIo1());
System.out.println("o1 = " + call.getO1());
System.out.println("o2 = " + call.getO2());
System.out.println("io2 = " + call.getIo2());

这已经很简单了,而且允许动态调用过程。现在,在大多数情况下,jOOQ也会生成一个方便方法,允许在1行中调用这个过程。生成的方便方法看起来像这样:

public class Routines {
    // [...]

    public static MyProc myProc(
          Configuration configuration
        , Number i1
        , Number io1
        , Number io2
        , Number i2
    ) {
        MyProc p = new MyProc();
        p.setI1(i1);
        p.setIo1(io1);
        p.setIo2(io2);
        p.setI2(i2);

        p.execute(configuration);
        return p;
    }
}

所以,它为你做了输入参数的拼接,所以你可以像这样调用它:

MyProc result = Routines.myProc(configuration, 1, 2, 5, 6);

System.out.println("io1 = " + result.getIo1());
System.out.println("o1 = " + result.getO1());
System.out.println("o2 = " + result.getO2());
System.out.println("io2 = " + result.getIo2());

这两种调用过程的方法是等价的,不过,第一种方法也支持默认参数,以备你在过程定义中使用。

其他特点

前面的例子展示了这个jOOQ功能和存储过程的最常见的用法。还有更多,我将在后续的博文中讨论,很快,包括:

  • 嵌入在jOOQ SQL语句中的标量函数
  • 嵌入在jOOQ SQL语句中使用的表值函数(包括PIPELINED 函数)
  • 从存储过程返回的游标(包括声明为REF CURSOR 和未声明的)。
  • Oracle PL/SQL包
  • Oracle PL/SQL UDTs和它们的成员程序
  • Oracle PL/SQLTABLE,RECORD 和关联数组类型
  • 微软T-SQL的表值参数
  • 微软T-SQL