Java8将彻底改变数据库访问


为我们的Java 8 series,我们很荣幸地主持了一个非常相关的客座帖子Dr. Ming-Yee Iu

姚明义博士于EPFL。他创建了开源项目Jinq演示一些在java中支持数据库查询的新技术。

我们的编辑说明:

自从Erik Meijer将LINQ引入。NET生态系统以来,我们Java爱好者一直在想我们是否也能拥有同样的LINQ。我们之前在博客上写过几次关于这个话题的文章:

而Java生态系统中的大多数LinqesqueAPI都是作为internal domain-specific languages like jOOQ,有些试图在字节码级别上解决集成问题,比如JaQu

JINQ通过Dr.Ming-Yee Iu所称的形式来形式化运行时字节码转换符号执行我们发现这非常有趣,以至于我们怀疑是否应该开始构建一个JINQ到jooq的JINQ提供程序,其中Java 8 Streams API could be combined with our great SQL standardisation and transformation features…

说服自己:

Java8好东西:Java8将彻底改变数据库访问

Java8终于来了!经过多年的等待,Java程序员最终将获得对Java中函数式编程的支持。函数式编程支持有助于精简现有代码,同时为Java语言提供强大的新功能。将被这些新特性打乱的一个领域是程序员如何使用Java中的数据库。函数式编程支持为更简单,更强大的数据库API开辟了令人兴奋的新可能性。Java8将支持访问数据库的新方法,这些方法与其他编程语言(如C#的LINQ)具有竞争力。

处理数据的功能方式

Java8不仅为Java语言添加了功能支持,而且用处理数据的新功能方式扩展了Java集合类。传统上,在Java中处理大量数据需要很多循环和迭代器。

例如,假设您有一个Customer对象:

Collection<Customer> customers;

如果您只对来自比利时的客户感兴趣,那么您必须遍历所有客户并保存您想要的客户。

Collection<Customer> belgians = new ArrayList<>();
for (Customer c : customers) {
    if (c.getCountry().equals("Belgium"))
        belgians.add(c);
}

这需要五行代码。它的抽象性也很差。如果您有1000万个客户,并且您希望通过使用两个线程并行过滤代码来加快代码的速度,会发生什么呢?你将不得不重写所有的东西来使用期货和大量的多线程代码。

使用Java8,您可以在一行中编写相同的代码。通过对函数式编程的支持,Java8允许您编写一个函数,说明您对哪些客户感兴趣(来自比利时的客户),然后使用该函数筛选集合。Java8有一个新的Streams API,允许您这样做。

customers.stream().filter(
    c -> c.getCountry().equals("Belgium")
);

Java8版本的代码不仅更短,而且代码也更容易理解。几乎没有样板。代码调用方法filter(),因此很明显,此代码用于筛选客户。您不必花费时间试图破译循环中的代码来理解它正在对其数据进行什么操作。

如果要并行运行代码,会发生什么呢?您只需使用不同类型的流。

customers.parallelStream().filter(
    c -> c.getCountry().equals("Belgium")
);

更令人兴奋的是,这种函数式的代码也可以用于数据库!

使用数据库的功能方式

传统上,程序员需要使用特殊的数据库查询语言来访问数据库中的数据。例如,下面是一些用于查找来自比利时的所有客户的JDBC代码:

PreparedStatement s = con.prepareStatement(
    "SELECT * "
  + "FROM Customer C "
  + "WHERE C.Country = ? ");
s.setString(1, "Belgium");
ResultSet rs = s.executeQuery();

大部分代码都是字符串形式的,编译器无法检查错误,而且由于粗心的编码可能导致安全问题。还有很多样板代码使得编写数据库访问代码相当繁琐。工具,如jOOQ通过提供可以使用特殊Java库编写的数据库查询语言来解决错误检查和安全性问题。或者您可以使用对象-关系映射器之类的工具,为常见的访问模式隐藏大量枯燥的数据库代码,但如果需要编写非琐碎的数据库查询,您将仍然需要再次使用专门的数据库查询语言。

使用Java8,可以使用处理Streams API时使用的相同函数风格编写数据库查询。例如,Jinq是一个探索未来数据库API如何利用函数式编程的开源项目。下面是一个使用jinq编写的数据库查询:

customers.where(
    c -> c.getCountry().equals("Belgium")
);

这段代码几乎与使用Streams API的代码相同。事实上,未来的Jinq版本将允许您直接使用Streams API编写查询。当代码运行时,Jinq将自动翻译将代码转换为数据库查询,如前面所示的JDBC查询。

因此,无需学习新的数据库查询语言,您就可以编写高效的数据库查询。您可以使用与Java集合相同风格的代码。您也不需要特殊的Java编译器或虚拟机。所有这些代码都使用普通的Java8 JDK编译和运行。如果您的代码中有错误,编译器会发现它们并报告给您,就像正常的Java代码一样。

Jinq支持像SQL92一样复杂的查询。选择,投影,联接和子查询都被支持。将Java代码翻译成数据库查询的算法在它将接受和翻译什么代码方面也非常灵活。例如,Jinq将下面的代码翻译成数据库查询没有问题,尽管它很复杂。

customers
    .where( c -> c.getCountry().equals("Belgium") )
    .where( c -> {
        if (c.getSalary() < 100000)
            return c.getSalary() < c.getDebt();
        else
            return c.getSalary() < 2 * c.getDebt();
} );

正如您所看到的,Java8中的函数式编程支持非常适合编写数据库查询。查询是紧凑的,并且支持复杂的查询。

内部工作

但这一切是如何运作的呢?一个普通的Java编译器如何将Java代码翻译成数据库查询?Java8有什么特别之处使这成为可能吗?

支持这些新的函数式数据库API的关键是一种名为符号执行的字节码分析。尽管您的代码是由普通Java编译器编译的,并在普通Java虚拟机中运行,但Jinq能够在运行编译后的Java代码时对其进行分析,并从它们构造数据库查询。符号执行在分析小函数时效果最好,这在使用Java8 Streams API时很常见。

理解这种符号执行是如何工作的最简单的方法是用一个例子。让我们检查一下下面的查询是如何被Jinq转换成SQL查询语言的:

customers
    .where( c -> c.getCountry().equals("Belgium") )

最初,customers变量是表示此数据库查询的集合

SELECT *
FROM Customers C

然后,where()方法,并向其传递函数。在这方面where()方法时,Jinq打开.class文件,并获取要分析的函数的编译字节码。在这个例子中,我们不使用真正的字节码,而是只使用一些简单的指令来表示函数的字节码:

  1. d=C.GetCountry()
  2. e=“比利时”
  3. e=D等于(e)
  4. 返回e

在这里,我们假设函数已经被Java编译器编译成四条指令。这就是Jinq在where()方法。Jinq如何理解这些代码?

Jinq通过执行代码来分析代码。不过,Jinq并不直接运行代码。它“抽象地”运行代码。而不是使用实变量和实值,Jinq在执行代码时使用符号来表示所有的值。这就是为什么分析被称为符号执行

Jinq执行每个指令并跟踪所有副作用或者代码在程序状态中更改的所有内容。下面是一个图表,显示了当Jinq使用符号执行来执行这四行代码时所发现的所有副作用。

符号执行示例

在图中,您可以看到在第一条指令运行后,Jinq如何发现两个副作用:变量d已经改变,方法Customer.getCountry()已经被调用。通过符号执行,变量d没有像“USA”或“Denmark”那样给出真正的值。它的符号值为c.getCountry()

在象征性地执行了所有指令之后,Jinq将清除副作用。因为变量de是局部变量,在函数退出后,对它们的任何更改都将被丢弃,因此这些副作用可以被忽略。Jinq也知道Customer.getCountry()String.equals()不要修改任何变量或显示任何输出,因此那些方法调用也可以被忽略。由此,Jinq可以得出结论,执行函数只产生一个效果:它返回c.getCountry().equals("Belgium")

一旦Jinq理解了函数在where()方法,然后它可以将此知识与customers集合创建新的数据库查询。

生成数据库查询

这就是Jinq如何从代码中生成数据库查询。符号执行的使用意味着这种方法对不同Java编译器输出的不同代码模式相当健壮。如果Jinq遇到了不能使用数据库查询模拟的副作用代码,那么Jinq将不会影响您的代码。由于所有内容都是使用普通Java代码编写的,因此Jinq可以直接运行这些代码,您的代码将产生预期的结果。

这个简单的翻译示例应该已经让您了解了查询翻译是如何工作的。您应该确信这些算法可以正确地从您的代码生成数据库查询。

令人兴奋的未来

我希望我已经让您初步了解了Java8是如何支持在Java中使用数据库的新方法的。Java8中的函数式编程支持允许您以类似于编写处理Java集合的代码的方式编写数据库代码。希望现有的数据库API将很快得到扩展,以支持这些类型的查询。

要使用这些新类型查询的原型,可以访问http://www.jinq.org