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