json 的计算类库

Json不仅体积小巧,而且能用多层结构灵活表达数据关系,但多层结构比二维结构复杂,计算起来不太方便,为了解决这个矛盾,json计算类库应运而生。下面将比较几类常见的json计算类库,重点是语法表达、部署配置、数据源方便的区别。需要说明的是,Gson/ fastjson/ jackson等类库侧重解析维护,缺乏计算能力,不在此次比较之列。

JsonPath

JsonPath的目标是做json上的“XPath”,虽然当下离目标尚有距离,但已经实际应用在不少项目中,且经常和维护类的json类库搭配使用。

下面举例说明JsonPath的语法表达能力。文件EO.json存储一批员工信息,以及属于员工的多个订单,部分数据如下:

[{
"_id": {"$oid":   "6074f6c7e85e8d46400dc4a7"},
"EId": 7,"State":   "Illinois","Dept": "Sales","Name":   "Alexis","Gender": "F","Salary":   9000,"Birthday": "1972-08-16",
"Orders": [
{"OrderID":   70,"Client": "DSG","SellerId":   7,"Amount": 288,"OrderDate": "2009-09-30"},
{"OrderID":   131,"Client": "FOL","SellerId":   7,"Amount": 103.2,"OrderDate": "2009-12-10"}
]
}
{
"_id": {"$oid":   "6074f6c7e85e8d46400dc4a8"},
"EId": 8,"State": "California", ...
}]

针对该文件,用JsonPath查询出所有价格在500-2000,且客户名包含bro字样的订单。JAVA代码如下:

package org.example;
import com.jayway.jsonpath.Configuration;
import com.jayway.jsonpath.JsonPath;
import java.io.File;
import java.io.FileInputStream;
import java.util.ArrayList;
public class App1
{
public   static void main(String[] args )throws Exception
{
String   str=file2str("D:\\json\\EO.json");
Object   document = Configuration.defaultConfiguration().jsonProvider().parse(str);
ArrayList l=JsonPath.read(document, "$[*].Orders[?(@.Amount>500   && @.Amount<2000 && @.Client =~ /.*?bro.*?/i)]");
System.out.println(l);
}
public   static String file2str(String fileName)throws Exception{
File   file = new File(fileName);
Long   fileLength = file.length();
byte[]   fileContent = new byte[fileLength.intValue()];
FileInputStream in = new FileInputStream(file);
in.read(fileContent);
in.close();
return   new String(fileContent, "UTF-8");
}
}

代码中,@.Amount>500 && @.Amount<2000是区间查询条件,@.Client =~ /.*?bro.*?/i是模糊查询条件,可以看出JsonPath的优点是查询语句较短,缺点是不太成熟,比如模糊查询还要借助正则表达式,而不是更易用的函数(比如SQL里的like)。事实上,JsonPath只支持很简单的计算,比如条件查询和聚合,其他大部分常用计算都不支持,包括分组汇总、关联、集合计算等。

JsonPath的不成熟还体现在数据源方面。从上述代码可以看出,即使基本的文件数据源,JsonPath也需要硬编码访问,其他数据源就更不支持了。

部署配置方面是JsonPath唯一的优点,只需在Maven加入json-path即可。

类似的计算库还有几个,虽然功能略有区别,但由于底层原理类似,导致成熟度都不高。比如fastJson在JsonPath的基础上补充了like函数,提高了易用性但降低了稳定性。

SQLite

SQLite是嵌入式内存数据库,由于轻量小巧集成方便,常被嵌入编程语言中。虽然体积很小,但SQLite的能力并不差,支持json计算就是其中之一。

比如前面的条件查询,可用如下JAVA代码实现:

package test;

import java.io.File;
import java.io.FileInputStream;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.Statement;

public class Main {
public   static void main(String[] args)throws Exception {
Connection connection =   DriverManager.getConnection("jdbc:sqlite:D:/ex1");
Statement statement = connection.createStatement();
statement.execute("create table datatable ( path string , data   json1)");
String sql="insert into datatable values('1',   json('"+file2str("D:\\json\\EO.json") +"'))";
statement.execute(sql);
sql="select value from(" +
"select value" +
"from datatable, json_tree(datatable.data,'$')" +
"where type ='object'and parent!=0" +
")where json_extract( value,'$.Amount') >500  and json_extract(value,'$.Amount') <2000   and json_extract(value,'$.Client') like'%bro%'";
ResultSet   results  = statement.executeQuery(sql);
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中建立表datatable,之后从文件读入json,并当做一条记录插入datatable,最后用SQL语句进行条件查询。SQL中的json_tree函数可将多层结构的json解析为二维结构(类似表),json_extract用来从二维结构的json(类似记录)中取字段。

类似地,SQLite也可以实现分组汇总,SQL如下:

select strftime('%Y',Orderdate),sum(Amount) from(
select json_extract(  value,'$.OrderDate')OrderDate,json_extract(value,'$.Amount')Amount
from datatable, json_tree(  datatable.data, '$')
where type = 'object' and   parent!=0
)group by   strftime('%Y',Orderdate)

也可实现员工和订单之间的关联计算,SQL如下:

with base as (
select   value,id,parent,type
from   datatable, json_tree(datatable.data, '$')
),emp_orders as(
select   orders.value o,emp.value e from base ordersArr,base orders,base emp
where   ordersArr.parent=emp.id and orders.parent=ordersArr.id and emp.parent=0 and   emp.type='object'
)select json_extract( o,'$.OrderID'),json_extract(  o,'$.Client'),json_extract(o,'$.Amount'),json_extract(o,'$.OrderDate'), json_extract(  e,'$.Name'), json_extract(e,'$.Gender'),json_extract(e,'$.Dept')
from emp_orders

从上面代码可以看出,SQLite语法表达能力较强,可以完成常用的计算。同时也应该看出来,SQLite代码冗长难懂,掌握起来难度较大。比如”select … from 表名,函数 where…”与常见的SQL语句结构上不同,程序员不易理解。再比如关联查询的代码很长,表之间的关系较为复杂,程序员很难看懂。

SQLite的代码之所以冗长难懂,是因为Json是多层数据,而SQL只擅长计算二维结构化数据,并不能直接计算Json。为了计算Json,必须把多层Json先降为二维结构才行,也就是用json_tree函数(包括代码中未出现的json_each)。用二维数据和二维计算语言(SQL)去模拟多层数据的计算,冗长难懂在所难免。

在数据源方面,SQLite表现很弱,需要硬编码才能读取基本的文件数据源,并在建表入库之后才能计算。

在配置部署方面,SQLite还是非常方便的,只需引入一个jar包即可实现。

Scala

Scala是比较流行的结构化计算语言,也是较早支持Json计算的语言之一。Scala先从数据源读取Json,存储为DataFrame数据对象(或RDD),再用DataFrame的通用计算能力完成计算。

对于前面的条件查询,可用如下Scala代码实现:

>package test
import scala.io.Source
import org.apache.spark.sql.SparkSession
import org.apache.spark.sql.functions.{asc, desc}
import org.apache.spark.sql.types._
import org.apache.spark.sql.functions._
import org.apache.spark.sql.DataFrame
object JTest {
def main(args: Array[String]): Unit =   {
val spark = SparkSession.builder()
.master("local")
.getOrCreate()
val df=spark.read.json("D:\\data\\EO.json")
val Orders =   df.select(explode(df("Orders"))).select("col.OrderID","col.Client","col.SellerId","col.Amount","col.OrderDate")
val condition=Orders.where("Amount>500   and Amount<2000 and Client like'%bro%' ")
condition.show()
}
}

上面代码先将Json读为多层的DataFrame对象,再用explode函数取出所有订单,之后用where函数完成条件查询。

类似地,Scala可以实现分组汇总,代码如下:

val   groupBy=Orders.groupBy(year(Orders("OrderDate"))).agg(sum("Amount"))

同样地,可实现员工和订单之间的关联计算,代码如下:

val   df1=df.select(df("Name"),df("Gender"),df("Dept"),explode(df("Orders")))
val   relation=df1.select("Name","Gender","Dept","col.OrderID","col.Client","col.SellerId","col.Amount","col.OrderDate")

从上面代码可以看出,Scala语法表达能力较强,可以完成常用的计算,且代码简短易懂,比SQLite容易掌握。在实现关联计算时,Scala并没有特意使用关联函数(虽然Scala有join函数),而是直接从多层数据取值,这就使逻辑关系变得简单,代码长度显著缩短。

Scala的代码之所以简短易懂,主要因为DataFrame支持多层数据,方便表达Json的结构,基于DataFrame的函数也更容易进行多层数据的计算。

在数据源方面,Scala同样表现优秀,不仅有专用函数读取文件中的json,也支持读取MongoDB、Elasticsearch、WebService等多种数据源中的Json。

在配置部署方面,Scala的基本类库就支持json计算,无需额外配置(MongoDB等数据源取数须额外配置)。

集算器 SPL

集算器 SPL是专业的开源结构化计算语言,原理和Scala类似,可以用统一的语法和数据结构计算各类数据源,其中就包括json。但集算器 SPL更“轻”,语法更简单,且提供耦合性较低的JDBC接口。

对于前面的条件查询,可用如下SPL代码实现:

A
1 =json(file("D:\\data\\EO.json").read())
2 =A1.conj(Orders)
3 =A2.select(Amount>500 &&   Amount<=2000 && like@c(Client,"*bro*"))

上面代码先将Json读为多层的序表对象(类似Scala的DataFrame),再用conj函数合并所有订单,之后用select函数完成条件查询。

这段代码可在集算器的IDE中调试/执行,也可存为脚本文件(比如condition.dfx),通过JDBC接口在JAVA中调用,具体代码如下:

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();
ResultSet result =   statement.executeQuery("call condition()");
printResult(result);
if(connection != null)   connection.close();
}

…

}

上面的用法类似存储过程,其实SPL也支持类似SQL的用法,即无须脚本文件,直接将SPL代码嵌入JAVA,代码如下:

…
ResultSet result = statement.executeQuery("=json(file(\"D:\\data\\EO.json\").read()).conj(Orders).select(Amount>500   && Amount<=3000 && like@c(Client,\"*bro*\"))");
…

类似地,SPL可以实现分组汇总和关联计算,代码如下:

A B
1 =json(file("D:\\data\\EO.json").read())
2 =A1.conj(Orders)
3 =A2.select(Amount>1000 &&   Amount<=3000 && like@c(Client,"*s*")) /条件查询
4 =A2.groups(year(OrderDate);sum(Amount)) /分组汇总
5 =A1.new(Name,Gender,Dept,Orders.OrderID,Orders.Client,Orders.Client,Orders.SellerId,Orders.Amount,Orders.OrderDate) /关联计算

从上面代码可以看出,SPL语法表达能力更强,不仅可以完成常用的计算,且代码简短易懂,比Scala更容易集成。SPL对点操作符的支持更直观,在实现关联计算时可直接从多层数据取值,代码更加简练。

SPL语法表达能力更强,经常可以简化多层json的计算,比如:文件JSONstr.json的runners字段是子文档,子文档有3个字段:horseId、ownerColours、trainer,其中trainer含有下级字段trainerId ,ownerColours是逗号分割的数组。部分数据如下:

[

   {
      "race": {
            "raceId":"1.33.1141109.2",
            "meetingId":"1.33.1141109"
      },
      ...
        "numberOfRunners": 2,
      "runners":   [
          {     "horseId":"1.00387464",
                "trainer": {
                    "trainerId":"1.00034060"
                },
            "ownerColours":"Maroon,pink,dark blue."
            },
            {   "horseId":"1.00373620",
                "trainer": {
                    "trainerId":"1.00010997"
                },
            "ownerColours":"Black,Maroon,green,pink."
            }
      ]
   },
...
]

现在要按 trainerId分组,统计每组中 ownerColours的成员个数。可用下面的SPL实现本计算。

A
1 =json(file("/workspace/JSONstr.json").read())
2 =A1(1).runners
3 =A2.groups(trainer.trainerId; ownerColours.array().count():times)

在数据源方面,集算器 SPL表现优秀,不仅有专用函数读取文件中的json,也支持读取MongoDB、Elasticsearch、WebService等多种数据源中的Json。

最后说下集算器的配置。读写计算Json是SPL的基本功能,无需额外配置(MongoDB等数据源除外)

通过上述比较可以看出:在语法方面,集算器 SPL表达能力较强,可以简化多层Json的计算;Scala的表达能力较强,可以完成常用的计算;SQLite的表达能力虽然够用,但代码难写难读;JsonPath表达能力不足,无法完成常用计算。在数据源方面,集算器 SPL和Scala较为丰富, JsonPath表现较差,SQLite还不如JsonPath。在部署配置方面,SQLite较简单,其他三种也不难。