在Java程序中使用数据库的新方法
java 8终于到来了! 经过几年的等待, java程序员终于能在java中得到函数式编程的支持了. 函数式编程的支持能流程化现有的代码并且为java提供强大的能力.在这些新特性中最瞩目的是java程序员对数据库的操作方式.函数式编程带来了令人激动的简便高效的数据库api. java 8 将会支持可与像c#的linq等语言竞争的新的数据库访问方式.
处理数据的函数式方式
java 8 不仅仅添加了函数式支持,它也通过新的函数式处理数据的方式扩展了集合(collection)类. 而通常情况下java处理大量数据时需要大量的循环和迭代器.
例如, 假设你有一个存储客户(customer)对象的collection:
collection<customer> customers;
如果你只对来自belgium的客户感兴趣, 你将不得不迭代所有的customer对象并只保存你需要的.
collection<customer> belgians = new arraylist<>(); for (customer c : customers) { if (c.getcountry().equals("belgium")) belgians.add(c); }
这不仅花费了5行代码,而且它也不怎么抽象.假使你有1千万个对象时会怎样呢?你会通过两个线程并发过滤所有对象来提速么?那你将不得不使用大量危险的多线程代码来重写所有代码.
而通过java 8,仅仅只需要一行代码就能实现相同的功能.通过对函数式编程的支持, java 8 能让你只写一个函数表明你对哪些客户(对象)感兴趣然后使用那个函数对集合做过滤就可以了. java 8 的新 steams api 支持你这样做:
customers.stream().filter( c -> c.getcountry().equals("belgium") );
上面java 8 版本的代码不仅更短,而且更容易理解.它几乎没有什么 陈词滥调(循环或迭代器等).代码调用了filter()方法,那很明显这段代码是用来过滤客户(对象)的.你不需要再把时间浪费在解读循环中的代码来理解它在对它的数据做什么.
假使你想并发执行这段代码该怎么办呢?你只需使用另一个类型的stream
customers.parallelstream().filter( c -> c.getcountry().equals("belgium") );
更另人激动的是这种函数式风格的代码也同样适用于数据库
在数据库上使用函数式方式
传统上来说, 程序员需要用特殊数据库查询语句去访问数据库的数据. 例如,下面就是用 jdbc 代码去查找来自belgium的客户:
preparedstatement s = con.preparestatement( "select * " + "from customer c " + "where c.country = ? "); s.setstring(1, "belgium"); resultset rs = s.executequery();
大部分这些代码都是字符串, 这样会使编译器不能发现错误而且这草率的代码会导致安全问题. 还有这些大量的样板代码使得写数据访问代码变得十分冗余. 一些工具例如 jooq ,通过使用特殊的java库去提供数据库查询语言可以解决错误检查和安全问题。 或者使用对象关系映射工具可以免去大量的无趣的代码,可它们只能用在通用访问查询, 如果需要复杂的查询,还是需要用特殊的数据库查询语言。
使用java 8,借助流式api就可以用函数式方式去查询数据库了。例如, jinq 是一个开源的项目,它探索怎样的未来数据库api可以令函数式编程成为可能。这里就是一个使用jinq的数据库查询:
customers.where( c -> c.getcountry().equals("belgium") );
这代码几乎跟跟使用流式api的代码一样. 事实上,未来的jinq版本可以让你用流式api直接写数据库查询。 当代码运行的时候,jinq将自动翻译成数据库查询代码,正如之前jdbc查询一样。
这样的话,就算没有学过一些新的数据库查询语言,你也可以写出有效率的数据库查询。你可以用同样样式的代码用在java集合上。你也不需要特殊的java编译器或者虚拟机。所有的代码编译和运行在普通的java 8 jdk上。如果你的代码有错误,编译器将找出它们并且报告给你,就像普通的java代码。
jinq 支持跟sql92一样的复杂查询. selection(选择), projection(投影), joins(连接), 和子查询 它都支持。翻译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(); } );
正如你看到的,java 8 的函数式编程非常适合数据库查询。而且查询紧凑,甚至复杂的查询也能够胜任。
内部运作
但这都是如何工作的呢?怎么能让普通的java编译器将java代码转换成数据库查询?java 8 有什么特别之处使这个成为可能?
支持这些函数性风格的新的数据库pi的关键是一种叫做“象征性执行”的字节码分析手段。虽然你的代码是被一个普通的java编译器编译的并运行在一个普通的java虚拟机中,但 jinq 能够在你被编译的java代码运行时进行分析并从中构建数据库查询。使用 java 8 streams api 时,常会发现分析短小的函数时,象征性执行的工作效果最好。
要了解这个象征性执行是如何工作的,最简单的方法是用一个例子。让我们检查一下下面的查询是如何被 jinq 转换为sql查询语言的:
customers .where( c -> c.getcountry().equals("belgium") )
初始时, 变量 customers 是一个集合,其对应的数据库查询是:
select * from customers c
然后,where() 方法被调用,一个函数被传递给它。在 where() 方法中,jinq 打开这个函数的 .class 文件,得到这个函数被编译成的字节码进行分析。在这个例子中,不使用真正的字节码,让我们用一些简单的指令来代表这个函数的字节码:
d = c.getcountry() e = “belgium” e = d.equals(e) return e
在这里,我们假设函数已被java编译器编译成这四条指令。当调用 where() 方法时,jinq 看到的就是这些。如何才能使jinq理解这些代码呢?
jinq 通过执行代码来分析。但 jinq 不直接运行代码。它是“抽象”地运行代码:不使用真实的变量和真实的值,jinq 使用符号来表示执行代码时的所有值。这就是这个分析为什么被称为“象征性执行”。
jinq 执行每条指令,并跟踪所有的副作用或代码在程序状态时改变的所有东西。下面是一个图表,显示出 jinq 用象征性执行方式执行这四行代码时发现的所有副作用。
象征性执行的例子
在图中,你可以看到第一条指令运行后,jinq 发现了两个副作用:变量d已经发生了变化,方法 customer.getcountry() 被调用。由于是象征性执行,变量d没有给出一个真正的比如是“usa”或“denmark”的值,它被分配为 c.getcountry() 的象征性的值。
在所有这些指令被象征性执行之后,jinq 对副作用作精简。由于变量 d 和 e 是局部变量,它们的任何变化在函数退出后都会被丢弃,所以这些副作用可以忽略不计。jinq也知道 customer.getcountry() and string.equals() 方法没修改任何变量或显示任何输出,因此这些方法调用也可以被忽略。由此,jinq 可以得出这样的结论:执行这个函数只会产生一个作用,它会返回 c.getcountry().equals("belgium")。
一旦jinq已明白在 where()方法中传递给它的函数,它可以混合数据库查询方面的知识,优先于 customers 集合来创建一个新的数据库查询。
生成数据库查询
这就是 jinq 如何从你的代码生成数据库查询的。象征性执行的使用意味着,这种方法对于不同的java编译器输出的不同的代码模式都是相当强大的。如果 jinq 遇到的代码有不能转化为数据库查询的副作用,jinq 将保持你的这些代码不变。因为一切都是用正常的java代码写的,jinq 可以直接运行那些代码,您的代码将产生预期的结果。
这个简单的翻译实例应该让你明白了怎样查询翻译作品。你可以确信,这些算法可以正确地从你的代码生成数据库查询。
美好前景
我希望我已经让你品尝到了java 8带来的在java中进行数据库工作的新方式。java 8 支持的函数式编程允许你用和为java集合编写代码同样的方式来为数据库写代码。希望不久现有的数据库api都能被扩展以支持这些类型的查询。