从 JsonPath 到 SPL

JSON 的多层结构比二维结构格式复杂,计算起来难度很大,早期的类库只能解析 JSON,没有计算能力;JsonPath 虽然提供了原始的计算语言,但计算能力较弱;SPL 是专业的计算语言,支持各种基础计算,可以简化多层 JSON 的计算和复杂计算目标,提供了多种数据源接口和 JDBC 集成接口。点击从 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计算语言。

 

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

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

……

ArrayList orders=JsonPath.read(document,   "$[*].Orders[*]");

Comparator<HashMap> comparator = new   Comparator<HashMap>() {

    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<HashMap> result=new   ArrayList<HashMap>();

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,特殊些的会来自MongoDBelasticSearch,只有引入第三方类库或硬编码才能从这些接口取数,这导致架构复杂、不稳定因素增大、开发效率降低。

JsonPathJSON计算能力很弱,本质是因为其计算语言过于原始。要想提高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

这段代码可在SPLIDE中调试/执行,也可存为脚本文件(比如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.jsonrunners字段是子文档,子文档有3个字段:horseIdownerColourstrainer,其中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,还支持MongoDBelsticSearch等,详情参考官网。

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