开源 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是专业的结构化数据处理语言,数据对象更强大,语法更灵活,函数更丰富,可以更容易地进行数据库读写,可以简化复杂的流程处理和数据计算,可以真正提高业务逻辑的开发效率。