四类 JAVA 计算层的深度对比
大多数情况下,Java程序员会在数据库中用SQL来完成结构化数据的计算,但有时没有或不能使用数据库,就需要用Java来完成。硬编码的工作量太大,更简单的做法是用Java计算层工具(包括库函数)来实现,由计算层负责计算并返回结果。下面将深度对比一些常见的Java计算层,尤其是结构化数据计算能力的差异。
文件SQL引擎
此类工具以csv\xls等文件为物理表,向上提供JDBC接口,允许程序员用SQL语句实现计算。这类工具的数量很多,比如CSVJDBC/XLSJDBC/CDATA Excel JDBC/xlSQL,但都不够成熟,下面从矮子里拔将军,讲讲相对成熟的CSVJDBC。
CSVJDBC是开源免费的JAVA类库,这就决定了它在集成方面非常优秀,你只需下载一个jar包,即可通过JDBC接口与JAVA程序集成。比如d:\data\Orders.txt是tab分隔的文本文件,部分内容如下:
OrderID Client SellerId Amount OrderDate 26 TAS 1 2142.4 2009-08-05 33 DSGC 1 613.2 2009-08-14 84 GC 1 88.5 2009-10-16 133 HU 1 1419.8 2010-12-12 …
下面代码将读取该文件的全部记录,并在控制台打印:
package csvjdbctest; import java.sql.*; import org.relique.jdbc.csv.CsvDriver; import java.util.Properties; public class Test1 { public static void main(String[] args)throws Exception { Class.forName("org.relique.jdbc.csv.CsvDriver"); // Create a connection to directory given as first command line String url = "jdbc:relique:csv:" + "D:\\data" + "?" + "separator=\t" + "&" + "fileExtension=.txt"; Properties props = new Properties(); //Can only be queried after specifying the type of column props.put("columnTypes", "Int,String,Int,Double,Date"); Connection conn = DriverManager.getConnection(url,props); // Create a Statement object to execute the query with. Statement stmt = conn.createStatement(); //SQL:Conditional query ResultSet results = stmt.executeQuery("SELECT * FROM Orders"); // Dump out the results to a CSV file/console output with the same format CsvDriver.writeToCsv(results, System.out, true); // Clean up conn.close(); } }
在热部署方面,CSVJDBC的表现也不错,因为它采用SQL语句实现计算,只需通过简单方法就能使SQL语句外置于JAVA代码。届时无需编译或重启应用程序,就可以直接修改SQL语句。
虽然CSVJDBC在JAVA集成和热部署方面可圈可点,但这不是JAVA计算层的核心能力。JAVA计算层的核心在于结构化数据计算,CSVJDBC在这方面就很差了。
CSVJDBC只支持有限的几种基本计算,比如条件查询、排序、分组汇总:
//SQL:Conditional query ResultSet results = stmt.executeQuery("SELECT * FROM Orders where Amount>1000 and Amount<=3000 and Client like'%S%' "); //SQL:order by results = stmt.executeQuery("SELECT * FROM Orders order by Client,Amount desc"); //SQL:group by results = stmt.executeQuery("SELECT year(Orderdate) y,sum(Amount) s FROM Orders group by year(Orderdate)");
基本运算还应包括集合计算、子查询、关联查询等,CSVJDBC计算能力不足,这些全都不支持。即使前面有限的几种基本计算,CSVJDBC也存在很多缺陷,比如排序和分组汇总时必须将文件全部读入内存,因此文件不能太大。
SQL语句天然不支持调试,表现也很差。源数据支持方面,CSVJDBC只支持CSV格式,其他无论数据库、Excel还是json,都必须转化为CSV文件。转化时只能硬编码或使用第三方工具,实现成本非常高。
dataFrame类函数库
此类工具以Python Pandas为模仿目标,一般会提供类似dataFrame的通用数据对象,向下对接各种数据源,向上提供函数式的计算接口。此类工具数量也较多,如Tablesaw/ Joinery/ Morpheus/ Datavec/ Paleo/ Guava。由于Pandas发力较早且较为成功,导致此类工具乏人问津,完成度普遍较低。下面重点讲解完成度相对较高的Tablesaw(最新版本0.38.2)。
Tablesaw是开源免费的JAVA类库,只需部署核心jar包和依赖包即可完成集成工作。基础代码也很简单,比如读取并打印Orders.txt的全部记录:
package tablesawTest; import tech.tablesaw.api.Table; import tech.tablesaw.io.csv.CsvReadOptions; public class TableTest { public static void main(String[] args) throws Exception{ CsvReadOptions options_Orders = CsvReadOptions.builder("D:\\data\\Orders.txt").separator('\t').build(); Table Orders = Table.read().usingOptions(options_Orders); System.out.println(orders.print()); } }
除了CSV文件,Tablesaw还支持RDBMS/ Excel/ JSON/ HTML等数据源,基本满足日常使用。由于Tablesaw采用函数式计算,因此调试体验良好,可支持断点/单步/进入/跳出等多种功能。函数式计算对调试有利,但热部署却不利,任意对运算的改动都要重新编译。
下面重点考察结构化数据计算,先看几种基本计算:
//条件查询 Table query= Orders.where( Orders.stringColumn("Client").containsString("S").and( Orders.doubleColumn("Amount").isGreaterThan(1000).and( Orders.doubleColumn("Amount").isLessThanOrEqualTo(3000) ) ) ); //排序 Table sort=Orders.sortOn("Client", "-Amount"); //分组汇总 Table summary = Orders.summarize("Amount", sum).by(t1.dateColumn("OrderDate").year()); //关联 CsvReadOptions options_Employees = CsvReadOptions.builder("D:\\data\\Employees.txt").separator('\t').build(); Table Employees = Table.read().usingOptions(options_Employees); Table joined = Orders.joinOn("SellerId").inner(Employees,true,"EId"); joined.retainColumns("OrderID","Client","SellerId","Amount","OrderDate","Name","Gender","Dept");
从上面代码可以看出来,对于排序、分组汇总这类涉及要素较少的计算,Tablesaw和SQL差距不大;对于条件查询和关联这类要素较多的计算,Tablesaw代码就比SQL繁琐多了。之所以产生这种现象,主要是因为JAVA并非专业的结构化计算语言,只有付出代码繁琐的代价,才能获得SQL同等的计算能力。好在JAVA支持lambda语法,理解起来会直观些(但比SQL仍有较大差距),比如条件查询也可以改写成下面这样:
Table query2=Orders.where( and(x->x.stringColumn("Client").containsString("S"), and( x -> x.doubleColumn("Amount").isGreaterThan(1000), x -> x.doubleColumn("Amount").isLessThanOrEqualTo(3000))));
小型数据库
小型数据库的特点是体积小巧、部署简单、方便集成。常见的小型数据库有SQLite/ Derby/ HSQLDB等,下面重点介绍SQLite。
SQLite是开源免费的数据库,只需一个jar包即可完成集成部署。SQLite不适合独立运行,一般以API接口的形式运行于JAVA等宿主程序中。当SQLite以外存模式运行时,可支持较大的数据量,以内存模式运行时,性能较好但数据量受限。SQLite的用法遵循JDBC规范,比如:打印外存数据库ex1的orders表的所有记录。
package sqliteTest; import java.sql.Connection; import java.sql.DriverManager; import java.sql.ResultSet; import java.sql.Statement; public class Test { public static void main(String[] args)throws Exception { Connection connection =DriverManager.getConnection("jdbc:sqlite/data/ex1"); Statement statement = connection.createStatement(); ResultSet results = statement.executeQuery("select * from Orders"); printResult(results); if(connection != null) connection.close(); } public static void printResult(ResultSet rs) throws Exception{ int colCount=rs.getMetaData().getColumnCount(); System.out.println(); for(int i=1;i<colCount+1;i++){ System.out.print(rs.getMetaData().getColumnName(i)+"\t"); } System.out.println(); while(rs.next()){ for (int i=1;i<colCount+1;i++){ System.out.print(rs.getString(i)+"\t"); } System.out.println(); } } }
SQLite对源数据支持很差,无论何种数据类型,都必须入库才能使用 (作为对比,有些小型数据库可将CSV/EXCEL直接识别为库表)。为了让CSV文件Orders.txr入库,可使用两种办法。第一种办法代码量较大,即用JAVA读入CSV,并将每行数据都拼成insert语句,再执行语句。第二种办法手工操作,即下载官方的维护工具sqlite3.exe,并在命令行执行下列命令:
sqlite3.exe ex1 .headers on .separator "\t" .import D:\\data\\Orders.txt Orders
虽然源数据方面失分了,但在重点考察的结构化数据计算方面,SQLite表现良好,各种基本运算都可以轻松实现:
//条件查询 results = statement.executeQuery("SELECT * FROM Orders where Amount>1000 and Amount<=3000 and Client like'%S%' "); //排序 results = statement.executeQuery("SELECT * FROM Orders order by Client,Amount desc"); //分组汇总 results = statement.executeQuery("SELECT strftime('%Y',Orderdate) y,sum(Amount) s FROM Orders group by strftime('%Y',Orderdate) "); //关联 results = statement.executeQuery("SELECT OrderID,Client,SellerId,Amount,OrderDate,Name,Gender,Dept from Orders inner join Employees on Orders.SellerId=Employees.EId
最后,提一下SQLite作为SQL引擎所固有的特点:在热部署方面表现良好,但在调试上较差。
专业结构化计算语言
此类语言专为结构化计算而设计,以提高运算的表达效率和执行效率为目标,以多数据源、方便集成、脚本热部署、代码调试为基础。结构化计算语言不多,常见的只有Scala、集算器 SPL、linq4j,由于linq4j成熟度不高,下面主要讲前两种。
Scala的设计初衷是通用开发语言,但真正引起人们注意的,是它专业的结构化数据计算能力,这既包括Spark架构的分布式计算,也包括无框架无服务的本地计算。Scala运行于JVM之上,天生就容易被JAVA集成,比如读取并打印Orders.txt的全部记录这个任务,可以先编写TestScala.Scala程序:
package test import org.apache.spark.sql.SparkSession import org.apache.spark.sql.DataFrame object TestScala{ def readCsv():DataFrame={ //本地执行时只需Jar包,无须配置/启动spark val spark = SparkSession.builder() .master("local") .appName("example") .getOrCreate() val Orders = spark.read.option("header", "true").option("sep","\t") //必须自动解析数据类型,才能进行查询等后续计算 .option("inferSchema", "true") .csv("D:/data/Orders.txt") //必须额外指定日期类型,才能进行后续的日期计算 .withColumn("OrderDate", col("OrderDate").cast(DateType)) return Orders }
将上述Scala文件编译为可执行程序(JAVA class),就可以在JAVA代码中调用了,如下:
package test; import org.apache.spark.sql.Dataset; public class HelloJava { public static void main(String[] args) { Dataset ds= TestScala. readCsv (); ds.show(); } }
基本的结构化计算方面,Scala代码还算简单:
//条件查询 val condtion=Orders.where("Amount>1000 and Amount<=3000 and Client like'%S%' ") //排序 val orderBy=Orders.sort(asc("Client"),desc("Amount")) //分组汇总 val groupBy=Orders.groupBy(year(Orders("OrderDate"))).agg(sum("Amount")) //关联 val Employees = spark.read.option("header", "true").option("sep","\t") .option("inferSchema", "true") .csv("D:/data/Employees.txt") val join=Orders.join(Employees,Orders("SellerId")===Employees("EId"),"Inner") .select("OrderID","Client","SellerId","Amount","OrderDate","Name","Gender","Dept") //关联后记录顺序会乱,这里排序是为了与其他工具的结果保持一致 .orderBy("SellerId")
在源数据方面,Scala支持的数据格式种类繁多,同样的代码可适用于不同的源数据。Scala可以看作某种改进的JAVA语言,在调试方面自然同样优秀。不足之处,作为编译型语言,Scala很难热部署。
Scala的专业性已经不错了,但集算器 SPL的专业性更强,因为它的设计目标就是结构化计算语言。集算器 SPL提供了JDBC接口,可以方便地集成到JAVA代码中。比如读取并打印Orders.txt的全部记录,可使用如下代码:
package Test; import java.sql.Connection; import java.sql.DriverManager; import java.sql.ResultSet; import java.sql.Statement; public class test1 { public static void main(String[] args)throws Exception { Class.forName("com.esproc.jdbc.InternalDriver"); Connection connection =DriverManager.getConnection("jdbc:esproc:local://"); Statement statement = connection.createStatement(); String str="=file(\"D:/data/Orders.txt\").import@t ()"; ResultSet result = statement.executeQuery(str); printResult(result); if(connection != null) connection.close(); } public static void printResult(ResultSet rs) throws Exception{ int colCount=rs.getMetaData().getColumnCount(); System.out.println(); for(int i=1;i<colCount+1;i++){ System.out.print(rs.getMetaData().getColumnName(i)+"\t"); } System.out.println(); while(rs.next()){ for (int i=1;i<colCount+1;i++){ System.out.print(rs.getString(i)+"\t"); } System.out.println(); } } }
基本的结构化计算方面,SPL代码简单易懂:
//条件查询 str="=T(\"D:/data/Orders.txt\").select(Amount>1000 && Amount<=3000 && like(Client,\"*S*\"))"; //排序 str ="=T(\"D:/data/Orders.txt\").sort(Client,-Amount)"; //分组汇总 str ="=T(\"D:/data/Orders.txt\").groups(year(OrderDate);sum(Amount))"; //关联 str ="=join(T (\"D:/data/Orders.txt\"):O,SellerId; T(\"D:/data/Employees.txt\"):E,EId).new(O.OrderID,O.Client,O.SellerId,O.Amount,O.OrderDate, E.Name,E.Gender,E.Dept)";
对于熟悉SQL的程序员,SPL也提供了对应的SQL语法,比如上面的分组汇总运算可写作下面这样:
str="$SELECT year(OrderDate),sum(Amount) from Orders.txt group by year(OrderDate)"
SPL除了可以内嵌于JAVA代码,也可以外置于脚本文件,这种方式可以进一步降低代码耦合性。下面用连续值班的例子加以说明。
Duty.xlsx记录着每日值班情况,一个人通常会持续值班几个工作日,之后再换人,现在要根据duty依次计算出每个人连续的值班情况。数据结构示意如下:
处理前(Duty.xlsx)
Date | Name |
2018-03-01 | Emily |
2018-03-02 | Emily |
2018-03-04 | Emily |
2018-03-04 | Johnson |
2018-04-05 | Ashley |
2018-03-06 | Emily |
2018-03-07 | Emily |
… | … |
处理后
Name | Begin | End |
Emily | 2018-03-01 | 2018-03-03 |
Johnson | 2018-03-04 | 2018-03-04 |
Ashley | 2018-03-05 | 2018-03-05 |
Emily | 2018-03-06 | 2018-03-07 |
… | … | … |
要实现该运算,可以先编写SPL脚本文件con_days.dfx,如下:
A | |
1 | =T("D:/data/Duty.xlsx") |
2 | =A1.group@o(name) |
3 | =A2.new(name,~.m(1).date:begin,~.m(-1).date:end) |
之后可在JAVA代码中以存储过程的方式调用SPL脚本文件:
… Class.forName("com.esproc.jdbc.InternalDriver"); Connection connection =DriverManager.getConnection("jdbc:esproc:local://"); Statement statement = connection.createStatement(); ResultSet result = statement.executeQuery("call con_days()"); ...
应该注意到,上述运算的计算逻辑比较复杂,用scala或小型数据库都比较难写,而SPL提供了更丰富的计算函数和语法,复杂计算逻辑也很容易实现。
除了降低耦合性,脚本外置还允许程序员使用专用的IDE进行编辑调试,适合实现逻辑更复杂的计算,下面以过滤累计值为例进行说明。
库表sales存储客户的销售额数据,主要字段有客户client、销售额amount,请找出销售额累计占到一半的前n个大客户,并按销售额从大到小排序。
要实现该运算,只需编写如下SPL脚本并在JAVA中调用:
A | B | |
1 | =demo.query(“select client,amount from sales”).sort(amount:-1) | 取数并逆序排序 |
2 | =A1.cumulate(amount) | 计算累计序列 |
3 | =A2.m(-1)/2 | 最后的累计值即是总和 |
4 | =A2.pselect(~>=A3) | 超过一半的位置 |
5 | =A1(to(A4)) | 按位置取值 |
在结构化计算能力方面,集算器 SPL明显比其它工具要更胜一筹,而且在代码调试、数据源种类以及大数据和并行计算等方面,集算器 SPL也均有出色表现,这里不再详细展开。