从 JsonPath 到 SPL

JSON的多层结构可存储丰富的信息,再加上体积小传输效率高,因此被广泛应用于微服务、程序间通讯、配置文件等场景。但多层结构比二维结构格式复杂,计算起来难度很大,这就对JSON类库提出了较高的要求。其中,Gson\Jackson等类库不支持JSON计算语言,只提供了将JSON串解析为JAVA\C#对象的函数。这些类库没有计算能力,即使实现很简单的条件查询,也要编写大量代码,开发效率低且实用性差。

好消息是,JsonPath出现了。

与前面提到的类库不同,JsonPath仿照XPath语法,提供了原始的JSON计算语言,可以用表达式查询出符合条件的节点,并支持一些聚合计算。下面试举几例。

文件data.json存储员工记录以及员工的订单,部分数据如下:

[  {"EId":2,"State":"NewYork  ","Dept":"Finance","Name":"Ashley","Gender":"F",
"Salary":11000,"Birthday":"1980-07-19",
"Orders":[]
},
{"EId":3,"State":"New   Mexico","Dept":"Sales","Name":"Rachel","Gender":"F",
"Salary":9000,"Birthday":"1970-12-17",
"Orders":[
{"OrderID":32,"Client":"JFS","SellerId":3,"Amount":468.0,"OrderDate":"2009-08-13"},
…
{"OrderID":99,"Client":"RA","SellerId":3,"Amount":1731.2,"OrderDate":"2009-11-05"}
]
},
…
]

条件查询:找到员工Rachel的所有订单。JsonPath代码如下:

File file = new File("D:\\json\\data.json");
Long fileLength = file.length();
byte[] fileContent = new byte[fileLength.intValue()];
FileInputStream in = new FileInputStream(file);
in.read(fileContent);
in.close();
String JsonStr= new String(fileContent, "UTF-8")
Object document = Configuration.defaultConfiguration().jsonProvider().parse(JsonStr);
ArrayList l=JsonPath.read(document,   "$[?(@.Name=='Rachel')].Orders");

上面代码先从文件读入字符串,再转为JSON对象,最后进行查询。具体的查询表达式是 $[?(@.Name=='Rachel')].Orders,其中$代表根节点,即员工记录(含订单字段),Orders代表下层的订单记录(即订单字段),上下层之间用点号隔开。每层节点后面可跟随查询条件,形如[?(…)],具体用法参考官网。

组合查询:找出所有价格在1000-2000,且客户名包含business字样的订单。关键代码如下:

……//省去JSON对象生成过程
l=JsonPath.read(document,   "$[*].Orders[?((@.Amount>1000 && @.Amount<2000) &&   @.Client =~ /.*?business.*?/i )]");

代码中的(@.Amount>1000 && @.Amount<2000)是区间查询条件,@.Client =~ /.*?business.*?/i是正则表达式的模糊查询条件,&&是逻辑运算符“与”(|| 是“或”)。

聚合计算:统计所有订单的总金额。关键代码如下:

……
Double   d=JsonPath.read(document, "$.sum($[*].Orders[*].Amount)");

代码中的sum是求和函数,类似的函数还有平均、max、min、计数。

从这些例子可以看出来,JsonPath的语法直观易懂,可以用点号方便地访问多层结构,可以用较短的代码实现条件查询,还能进行简单的聚合计算。Gson\JacksonJsonPath实现了计算能力从无到有的突破,这要归功于JSON计算语言。

 

有不表示强。实际上,JsonPath的JSON计算语言还比较原始,计算能力很弱。JsonPath只支持查询和聚合这两种很简单的计算,不支持其他大多数基础计算,离任意和自由的计算更是遥远。要实现大多数基础计算,JsonPath仍然要硬编码实现。

分组汇总为例:对所有订单按客户分组,统计各组的订单金额。关键代码如下:

……
ArrayList orders=JsonPath.read(document,   "$[*].Orders[*]");
Comparator comparator = new   Comparator() {
    public int   compare(HashMap record1, HashMap record2) {
        if   (!record1.get("Client").equals(record2.get("Client"))) {
             return   ((String)record1.get("Client")).compareTo((String)record2.get("Client"));
        } else {
              return   ((Integer)record1.get("OrderID")).compareTo((Integer)record2.get("OrderID"));
        }
    }
};
Collections.sort(orders, comparator);
ArrayList result=new   ArrayList();
HashMap currentGroup=(HashMap)orders.get(0);
double sumValue=(double) currentGroup.get("Amount");
for(int i = 1;i < orders.size(); i ++){
    HashMap   thisRecord=(HashMap)orders.get(i);
      if(thisRecord.get("Client").equals(currentGroup.get("Client"))){
          sumValue=sumValue+(double)thisRecord.get("Amount");
    }else{
        HashMap   newGroup=new HashMap();
          newGroup.put(currentGroup.get("Client"),sumValue);
          result.add(newGroup);
        currentGroup=thisRecord;
          sumValue=(double) currentGroup.get("Amount");
    }
}
System.out.println(result);

上述代码先用JsonPath取出订单列表,再将订单列表按Client排序,取出第1条作为当前组的初值,然后依次循环剩余的订单。如果当前订单与当前组相比Client不变,则将当前订单的Amount累加到当前组;如果Client改变,则说明当前组已汇总完成。

JsonPath的计算能力很弱,不支持分组汇总,只能硬编码完成大部分计算,这就要求程序员控制所有细节,代码冗长且容易出错。如果换一个分组字段或汇总字段,则要修改多处代码,如果对多个字段分组或汇总,代码还需大量修改,这就很难写出通用代码。除了分组汇总,JsonPath不支持的基础计算还有:重命名、排序、去重、关联计算、集合计算、笛卡尔积、归并计算、窗口函数、有序计算等。JsonPath也不支持将大计算目标分解为基础计算的机制,比如子查询、多步骤计算等。实际上,对大多数计算来说,JsonPath都要硬编码完成。

除了计算能力之外,Jsonpath还有个问题,就是没有自己的数据源接口,即使很简单的文件JSON,也需要硬编码实现。而JSON一般来自http restful,特殊些的会来自MongoDB或elasticSearch,只有引入第三方类库或硬编码才能从这些接口取数,这导致架构复杂、不稳定因素增大、开发效率降低。

JsonPath的JSON计算能力很弱,本质是因为其计算语言过于原始。要想提高JSON计算能力,必须使用更专业的计算语言。

集算器 SPL是个更好的选择。

 

集算器 SPL是开源的结构化数据\半结构化数据计算语言,提供了丰富的类库和精炼的语法,可以用简短的代码实现所有的基础计算,可将大计算目标拆分为基础计算,支持多种数据源接口,同时提供JDBC集成接口。

同样的条件查询,SPL代码如下:

A
1 =json(file("d:\\json\\data.json").read())
2 =A1.select(Name=="Rachel").Orders

A1代码从文件读字符串,并转为序表。序表是通用的结构化\半结构化数据对象,JSON是半结构化数据的一种。A2中函数select查询出符合条件的员工记录,Orders表示记录的订单字段(订单列表),上下层之间用.号隔开。

同样的组合查询,SPL代码如下:

A
1 …. //省去JSON对象/序表生成过程
2 =A1.conj(Orders)
3 =A2.select((Amount>1000 &&   Amount<=2000) && like@c(Client,"*business*"))

A2合并所有员工的订单,A3进行条件查询,like函数用于模糊查询字符串,@c表示不区分大小写。这里用到了多步骤计算,代码逻辑更清晰,也可将A2A3合并为一句。

同样的聚合计算,SPL代码如下:

A
1 ….
2 =A1.conj(Orders).sum(Amount)

代码中的sum是求和函数,类似的函数还有avg\sum\min\count。

这段代码可在SPL的IDE中调试/执行,也可存为脚本文件(比如getSum.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 getSum()");
printResult(result);
if(connection != null)   connection.close();
}
…
}

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

…
ResultSet result =   statement.executeQuery("=json(file(\"D:\\data\\data.json\").read()).conj(Orders).sum(Amount)");
…

 

SPL提供了丰富的库函数,支持各种基础计算,上面的查询和聚合只是其中一部分,更多基础计算如下:

A
1 ….
2 =A1.conj(Orders).groups(Client;sum(Amount)) 分组汇总
3 =A1.groups(State,Gender;avg(Salary),count(1)) 多字段分组汇总
45 =A1.new(Name,Gender,Dept,Orders.OrderID,Orders.Client,Orders.Client,Orders.SellerId,Orders.Amount,Orders.OrderDate) 关联
6 =A1.sort(Salary) 排序
7 =A1.id(State) 去重

 

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计算。比如通过http restful可取得按时间排序的每日值班情况,部分数据如下:

[{"Date":"2018-03-01","name":"Emily"},
{"Date":"2018-03-02","name":"Emily"},
{"Date":"2018-03-04","name":"Emily"},
{"Date":"2018-03-04","name":"Johnson"},
{"Date":"2018-04-05","name":"Ashley"},
{"Date":"2018-03-06","name":"Emily"},
{"Date":"2018-03-07","name":"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

要获得上述结果,应先将数据按照name进行有序分组,即如果连续几条记录的name都相同,则这几条记录分为同一组,直到name发生变化。注意这种分组可能会使同一个员工分出多组数据,比如Emily。分组后,应按日期取各组的首尾两条,即所求的开始值班日期和结束值班日期。这里涉及有序分组、分组后计算(即窗口函数)、按位置取值等难度较高的计算,常见的计算语言写起来会很繁琐,改用SPL就简单多了。代码如下:

A
1 =json(httpfile("http://127.0.0.1:6868/api/getDuty").read())
2 =duty.group@o(name)
3 =A2.new(name,~.m(1).date:begin,~.m(-1).date:end)

除了较强的计算能力之外,SPL还提供了丰富的数据源接口,除了上面提到的文件、restful,还支持MongoDB、elsticSearch等,详情参考官网。

从Gson\Jackson到JsonPath,JSON计算语言从无到有,从JsonPath到SPL,JSON计算能力由弱到强,每一次质的飞跃,都驱动着开发效率的大幅提升。