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较简单,其他三种也不难。
