四类 JAVA 计算层的深度对比

有些 JAVA 项目不方便用数据库完成计算任务,需要交由 JAVA 计算层完成,本文深度对比了四类 JAVA 计算层工具(类库),包括 scala\ 集算器 SPL\ SQLite\ CSVJDBC\ Tablesaw,重点考察这些工具的结构化数据计算能力,以及集成部署、源数据、热部署、调试等方面等基础功能,详情点击四类 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.txttab分隔的文本文件,部分内容如下:

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语句。

虽然CSVJDBCJAVA集成和热部署方面可圈可点,但这不是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");

从上面代码可以看出来,对于排序、分组汇总这类涉及要素较少的计算,TablesawSQL差距不大;对于条件查询和关联这类要素较多的计算,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规范,比如:打印外存数据库ex1orders表的所有记录。

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:sqlited/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集算器 SPLlinq4j,由于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也均有出色表现,这里不再详细展开。