开源 SPL,ORM 的终结者?

业务逻辑经常包含较复杂的流程和计算,同时涉及数据库的读写。由于授权麻烦、影响数据库安全、无法迁移、技术要求高、编写困难等原因,很多场景不适合用存储过程实现业务逻辑。因为不擅长复杂的流程处理,SQL也不适合单独实现业务逻辑,必须与JAVA等高级语言配合才行。但SQL和高级语言的语法风格迥然不同,数据结构差异巨大,导致两者难以配合,开发效率始终无法提高。在这种情况下,ORM适时而生。

ORM是一种将结构化数据(表/记录)映射为高级语言的对象的技术,这样就可以用统一的数据结构和语法风格实现业务逻辑其根本目标还是提高开发效率。常见的ORM技术有Hibernate、QueryDSL、JOOQ等。

在数据结构和语法风格的统一方面,ORM表现优秀,已经在大量项目中得到广泛应用。但ORM仍有很多不足,主要表现在:缺乏专业的结构化数据类型,集合运算不够方便,读写数据库时代码繁琐,不支持热部署,库函数不够丰富,复杂计算难以实现。ORM的这些缺点导致业务逻辑的开发效率没有明显提升,有时甚至大幅降低。

作为JAVA下开源的结构化数据处理类库,SPL可以解决ORM期望目标,甚至有更好的表现。

SPL用统一数据结构和语法风格实现业务逻辑

比如一个常见的业务逻辑:根据规则计算出奖金,向数据库插入包含奖金字段的记录。可用下面的SPL代码实现:

A B C
1 =db=connect@e("dbName") /连接数据库,开启事务
2 =db.query@1("select   sum(Amount)  from sales where
sellerID=? and year(OrderDate)=? and month(OrderDate)=?",
p_SellerID,year(now()),month(now()))
/查询当月销售额
3 =if(A2>=10000   :200, A2<10000 && A2>=2000 :100, 0) /本月累计奖金
4 =p_Amount*0.05 /本单固定奖金
5 =BONUS=A3+A4 /总奖金
6 =create(ORDERID,CLIENT,SELLERID,AMOUNT,BONUS,ORDERDATE) /创建订单的数据结构
7 =A6.record([p_OrderID,p_Client,p_SellerID,p_Amount,BONUS,
date(now())])
/生成一条订单记录
8 >db.update@ik(A7,sales;ORDERID) /尝试写入库表
9 =db.error() /入库结果
10 if A9==0 >A1.commit() /成功,则提交事务
11 Else >A1.rollback() /失败,则回滚事务
12 >db.close() /关闭数据库连接
13 return   A9 /返回入库结果

上面的SPL代码使用统一的数据结构、语法、函数,就可以完成整个业务逻辑的开发,包括数据库读写、事务处理、流程处理、数据计算。

JAVA代码本身不包含业务逻辑,通过JDBC驱动引用上面的SPL脚本,就可以实现业务逻辑。

…
//省略参数的获取过程
Class.forName("com.esproc.jdbc.InternalDriver");
Connection conn =DriverManager.getConnection("jdbc:esproc:local://");
CallableStatement statement = conn.prepareCall("{call InsertSales(?, ?,?,?)}");
statement.setObject(1, d_OrderID);
statement.setObject(2, d_Client);
statement.setObject(3, d_SellerID);
statement.setObject(4, d_Amount);
statement.execute();
...

JAVA调用SPL的形式与调用存储过程相同,且无须数据库管理员授权,不影响数据库安全,便于迁移,方便调试,处理能力更强,代码更简单。

 

SPL提供了专业的结构化数据对象,具备完整的内外数据交换能力和流程控制能力。

结构化数据对象

业务逻辑围绕结构化数据对象展开,SQL、ORM、SPL都内置专业的结构化数据对象,并据此提供了各自的结构化数据计算函数。类似ORM的实体/List<实体>,SPL的结构化数据对象是记录/序表。

取记录的字段值:=r.AMOUNT*0.05
修改记录的字段值:=r.AMOUNT= T.AMOUNT*1.05
序表取一列:T.(AMOUNT)
序表追加记录:T.insert(0,31,"APPL",10,2400.4)
基于序表,SPL提供了丰富的SQL式计算函数。
过滤:T.select(Amount>1000 && Amount<=3000 && like(Client,"*bro*"))
排序:T.sort(-Client,Amount)
去重:T.id(Client)
汇总:T.max(Amount)
分组汇总后过滤: T.groups(year(OrderDate),Client; avg(Amount):amt).select(amt>2000)
关联:join(Orders:o,SellerId ; Employees:e,EId).groups(e.Dept; sum(o.Amount))
交集:T1.id(Client) ^ T2.id(Client)
TopN:T.top(-3;Amount)
分组topN:T.groups(Client;top(3,Amount))

此外,SPL还提供了字符串、日期、数学等多种函数,充分满足业务逻辑中的计算需求。

内外数据交换能力

将外部数据库的记录读为内部结构化数据对象,ORM主要通过主键查询、HQL查询、SQL查询、链式编程等方式。类似地,SPL提供了query函数执行SQL的方式。

取单条记录:
=r=db.query("select * from sales where orderid=?",201)

取序表(记录集合):
=T=db.query("select * from salesR where SellerID=?",10)

为了增强可移植性,SPL提供了通用SQL,使用sqltranslate函数可将通用SQL转为主流方言SQL,仍然通过query函数执行。这不是本文重点,不展开说了。

将内部结构化数据对象持久化到数据库,ORM主要通过save(新增的实体)、update(修改的实体)、delete(删除的实体)等方式。类似地,SPL使用update函数。

比如,原序表为 T,经过增删改之后的序表为 NT, 将变化结果持久化到数据库:
=db.update(NT:T,sales;ORDERID)

 

流程控制能力

业务逻辑的难点在于复杂的流程控制(判断和循环)。SQL缺乏流程控制能力,ORM把数据库表映射为JAVA对象,主要目的就是为了更方便地利用JAVA的流程控制能力。类似JAVA,SPL具备完整的流程控制能力。

分支判断语句:

A B
2
3 if T.AMOUNT>10000 =T.BONUS=T.AMOUNT*0.05
4 else if   T.AMOUNT>=5000 && T.AMOUNT<10000 =T.BONUS=T.AMOUNT*0.03
5 else if   T.AMOUNT>=2000 && T.AMOUNT<5000 =T.BONUS=T.AMOUNT*0.02

循环语句:

A B
1 =db=connect("db")
2 =T=db.query@x("select   * from sales where SellerID=? order by OrderDate",9)
3 for T =A3.BONUS=A3.BONUS+A3.AMOUNT*0.01
4 =A3.CLIENT=CONCAT(LEFT(A3.CLIENT,4),   "co.,ltd.")
5  …

与Java的循环类似,SPL还可用break关键字跳出(中断)当前循环体,或用next关键字跳过(忽略)本轮循环,不展开说了。

 

SPL超越ORM获得更高的开发效率

SPL不仅能用统一的数据结构和语法风格实现业务逻辑,还能够达到ORM的根本目标,显著提高业务逻辑的开发效率。

更多计算函数

ORM的计算函数不足,很多功能都无法实现,或需要编写冗长的代码。SPL提供了更多的计算函数,可直接实现这些功能,代码量大幅缩短。

比如,时间类函数,日期增减:elapse("2020-02-27",5)  //返回2020-03-03
星期几:day@w("2020-02-27")             //返回5,即星期6
N个工作日之后的日期:workday(date("2022-01-01"),25)   //返回2022-02-04
字符串类函数,判断是否全为数字:isdigit("12345")    //返回true
取子串前面的字符串:substr@l("abCDcdef","cd")      //返回abCD
按竖线拆成字符串数组:"aa|bb|cc".split("|")     //返回["aa","bb","cc"]

SPL还支持年份增减、求年中第几天、求季度、按正则表达式拆分字符串、拆出SQL的where或select部分、拆出单词、按标记拆HTML等功能,数量较多,不再赘述。

 

更方便的集合运算

对结构化数据对象(实体集合)进行集合运算时,ORM有两种方法:循环语句+硬编码,Stream等Lambda表达式+集合函数。前者代码量显然巨大,后者似乎可以少写代码,但因为编译型Lambda表达式繁琐、集合运算函数少且功能弱等原因,导致大多数运算都要辅以硬编码。比如简单的分组汇总,就要用到groupingBy、Collectors、summarizingDouble、DoubleSummaryStatistics等多个类和方法。

SPL提供了简洁易懂的解释型Lambda语法,以及数量众多的集合函数,可以方便地进行集合运算。

比如,批量修改记录:T.run(BONUS+AMOUNT*0.01: AMOUNT, concat(left(CLIENT,4), "co.,ltd."): CLIENT)
过滤:T.select(Amount>1000 && Amount<=3000)
对有序序表按二分法进行过滤:T.select@b(Amount>1000 && Amount<=3000)
分组汇总:T.groups(Client;sum(Amount))
有序分组(相邻且字段值相同的记录分为一组):T.groups@b(Client;sum(Amount))

涉及跨行的集合运算,通常都有一定的难度,比如比上期和同期比。ORM没有为跨行运算做优化,代码通常很繁琐。SPL使用"字段[相对位置]"引用跨行的数据,可显著简化代码,还可以自动处理数组越界等特殊情况。比如,追加一个计算列rate,计算每条订单的金额增长率:

=T.derive(AMOUNT/AMOUNT[-1]-1: rate)

灵活运用SPL的Lambda语法和集合函数,可大幅简化复杂的集合计算。比如,在各部门找出比本部门平均年龄小的员工:

A
1 …//省略序表Employees的生成过程
2 =Employees.group(DEPT;   (a=~.avg(age(BIRTHDAY)),~.select(age(BIRTHDAY)<a)):YOUNG)
3 =A2.conj(YOUNG)

计算某支股票最长的连续上涨天数:

A
1 …//省略序表AAPL的生成过程
2 =a=0,AAPL.max(a=if(price>price[-1],a+1,0))

 

更专业的结构化数据类型

ORM的结构化数据类型是实体/List<实体>,通用性较强,但专业性不足,很多常用的访问方法都不支持,比如按字段名取某一个或某几个列。SPL的结构化数据类型是记录和序表,更加专业,功能也更强,常见的访问方法都支持。

比如,在序表T的基础上,按字段名取一列,返回简单集合:T.(AMOUNT)。

取几列,返回集合的集合:T.([CLIENT,AMOUNT])

取几列,返回新序表:T.new(CLIENT,AMOUNT)

按序号访问通常难度较大,序表天然有序,很容易处理此类问题。比如,按列号取几列,返回新序表:T.new(#2,#4)

按序号倒数取记录:T.m(-2)

按序号取某几条记录形成序表:T([3,4,5])

按范围取记录形成序表:T(to(3,5))

先按字段取再按记录序号取:T.(AMOUNT)(2);等价于先按记录序号取再按字段取:T(2).AMOUNT

除此之外,SPL还有很多基于序表的高级功能,如TopN、蛇形取值、有序关联等。

 

解释执行和热部署

业务逻辑数量多,复杂度高,变化是常态。良好的系统构架,应该有能力应对变化的业务逻辑。ORM的本质是JAVA代码,需要先编译再执行,一般都要停机才能部署,应对变化的业务逻辑时非常繁琐。

SPL是基于JAVA的解释型语言,无须编译就能执行,脚本修改后立即生效,支持不停机的热部署,适合应对变化的业务逻辑。

 

更方便的数据库读写方法

大量的业务逻辑要读写批量记录,这种情况下ORM只能循环ArrayList,并单独处理每条记录,代码冗长繁琐。遇到既有新增,又有修改和删除的批量写库的情况,ORM的代码就更复杂了。

基于序表,SPL提供了更方便的方法读写批量记录,可大幅简化代码。比如批量修改记录:

A B
1
2 =T=db.query("select   * from salesR where SellerID=?",10) /批量查询,序表 T
3 =NT=T.derive() /复制出新序表 NT
4 =NT.field("SELLERID",9) /批量修改
5 =db.update(NT:T,sales;ORDERID) /持久化到数据库

上面代码中,函数update实现批量修改,无须繁琐的循环语句。函数update经过精心设计,可以统一处理多种批量写库方法,遇到既有新增,又有修改和删除的批量写库的情况,SPL的优势更加明显。

A B
1
2 =T=db.query("select   * from salesR where SellerID=?",10) /查出一批记录
3 =NT=T.derive() /复制出新序表
4 =NT.delete(NT.select(ORDERID==209   || ORDERID==208)) /批量删除
5 =NT.field("SELLERID",9) /批量修改
6 =NT.record([220,"BTCH",9,5200,100,date("2022-01-02"),
221,"BTCH",9,4700,200,date("2022-01-03")])
/批量追加
7 =db.update(NT:T,salesR;ORDERID) /持久化到数据库

与ORM相比,SPL是专业的结构化数据处理语言,数据对象更强大,语法更灵活,函数更丰富,可以更容易地进行数据库读写,可以简化复杂的流程处理和数据计算,可以真正提高业务逻辑的开发效率。