ORM 还是存储过程
业务逻辑由数据库读写、结构化数据计算、流程处理组成。SQL的设计初衷就是数据库读写和结构化数据计算,解决这两部分相对轻松,但因为缺乏流程处理语句(循环、判断),SQL难以独自实现完整的业务逻辑。为了解决这个问题,数据库厂商又提供了由SQL和流程处理语句组成的新语言,也就是存储过程。
存储过程功能全面,可以实现完整的业务逻辑
SQL和流程处理语句统一在一门语言里,两者直接配合,给存储过程提供了全面的功能,可实现完整的业务逻辑。比如:
SELECT SFIEDL INTO SIGNS FROM DTABLE WHERE ID=1;
SI_MAX := SIGNS;
WHILE SIGNS > 0 LOOP
…
IF CALCULATED = 0 THEN
IF SIGNS = SI_MAX THEN
CONDITIONS := CONDITIONS || F_ALI || L_ID || '.' || CURRENT_VALUE;
ELSE
CONDITIONS := CONDITIONS || 'AND ' || F_ALI || L_ID || '.' || CURRENT_VALUE;
END IF;
END IF;
SIGNS := SIGNS - 1;
END LOOP;
上面代码包含SQL查询语句,WHILE循环语句、IF…ELSE判断语句,涉及业务逻辑的大部分内容,完整性较好。除此之外,存储过程还有权限管理细致、性能较好等优点,这里不赘述了。
存储过程存在架构性缺陷
存储过程将SQL和流程处理语句直接合并在一门语言里,功能比较完整,但也因此产生了一些严重的架构性缺陷。
存储过程是与工具厂商深度绑定的语言,每种数据库都有自己的存储过程语法,互相不通用,难以移植。存储过程理论上为前端应用提供服务,但物理位置却存在于数据库,维护时需要兼顾两处,这就造成了应用和数据库的紧耦合。存储过程写在数据库中,无法用第三方工具进行代码版本控制,难以进行团队协作。存储过程需要数据库管理员的授权,管理成本高。存储过程通常涉及表的创建、修改、读取、写入等,影响数据安全。此外,存储过程还存在风格不统一,调试困难等缺点。
存储过程的这些架构性缺陷,不符合现代编程理念。新一代的应用系统通常会引入库外的高级语言如JAVA\.NET来实现业务逻辑,让高级语言中的流程处理语句与SQL配合。但是,高级语言原生类库与SQL的语法风格和数据结构差异较大,互相配合困难,类型转换麻烦,导致开发效率低且学习成本和管理成本高。在这种情况下,ORM技术适时而生。
ORM用统一的语法风格和数据结构实现业务逻辑
ORM是一种将结构化数据(表/记录)映射为高级语言的对象的技术,常见的有Hibernate、QueryDSL、JOOQ等。ORM(以Hibernate为例)通过实体类读写数据库,通过JAVA的for/while/if语句进行流程处理,通过HQL或JAVA 语法进行结构化数据计算,从而在JAVA体系下实现完整的业务逻辑。其中,HQL是面向对象的查询语言,比JAVA 的计算能力强得多,且与具体数据库无关。比如模糊查询:
String hql="select orderId,client,amount from OrdersEntity where (amount between 2000 and 3000) and client like '%s%'";
Query query = session.createQuery(hql );
ORM继承了JAVA的优点,包括可移植、低耦合、方便团队协作、不影响数据安全、调试方便。更重要的是,ORM可以在JAVA体系下用统一的语法风格和数据结构实现业务逻辑,学习成本和代码管理成本有效降低,开发效率显著提高。
ORM计算能力不足
ORM在数据库读写、流程处理方面表现优秀,但结构化数据计算能力不足,很多计算无法用HQL描述,很多常见的函数HQL也不支持,复杂的业务逻辑难以实现。
ORM的HQL比JAVA计算能力强,但远不如SQL,很多计算都无法用HQL描述,比如FROM子查询,涉及窗口函数和行号的计算等。比如下面的查询:
select orderId, m from (select orderId, month(orderDate) m from OrdersEntity) t1
很多常见的日期函数和字符串函数HQL都不支持,包括日期增减、求年中第几天、求季度数,以及替换、left截取、求ASCII码等。比如下面的日期增加函数:
select date_add(OrderDate,INTERVAL 3 DAY) from OrdersEntity
想实现类似的功能,只有两种办法,或者引入方言SQL,或者用JAVA硬编码。前者会破坏ORM统一的语法风格,使ORM失去意义,后者虽然维护了统一的语法风格,但代码量巨大。为了弥补JAVA的结构化计算能力,Hibernate推出了Criteria, JAVA官方推出了Stream,但这些类库的计算能力还不如HQL,并不能解决面临的问题。
SPL功能全面,能用统一的语法风格和数据结构实现业务逻辑
看来,存储过程和ORM既有优点也有不足。那么,能不能把两者的优点结合起来,同时获得ORM在架构上的优势和存储过程在计算上的优势呢?
esProc SPL就是这么一项技术。
esProc SPL是基于JVM的开源结构化数据计算语言,具有专业的数据库读写能力、结构化数据计算能力、流程控制能力,可以用统一的语法风格和数据结构实现完整的业务逻辑。
数据库读写能力
将外部数据库的记录读为内部结构化数据对象,ORM主要通过主键查询、HQL查询等方式。类似地,SPL提供了query函数执行SQL的方式。
取单条记录:
=r=db.query("select * from sales where orderid=?",201)
取序表(SPL记录集合):
=T=db.query("select * from salesR where SellerID=?",10)
将内部结构化数据对象持久化到数据库,ORM主要通过save(新增实体)、update(修改实体)、delete(删除实体)等方式。类似地,SPL使用update函数。
比如,原序表为 T,经过增删改之后的序表为 NT, 将变化结果持久化到数据库:
=db.update(NT:T,sales;ORDERID)
结构化数据计算能力
业务逻辑围绕结构化数据对象展开,存储过程、ORM、SPL都内置专业的结构化数据对象,并据此提供了各自的结构化数据计算函数。类似地,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计算能力不足的问题。
流程控制能力
业务逻辑的难点在于复杂的流程控制。类似JAVA的for/while/if语句,和存储过程的loop/if语句,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 |
… |
SPL还可用break关键字跳出(中断)当前循环体,或用next关键字跳过(忽略)本轮循环,不展开说了。
SPL实现了更优化的架构性
SPL提供了JDBC接口,具有解释执行的特性,支持库外计算和代码移植,彻底解决了存储过程的架构性缺陷。
JDBC接口和低耦合
SPL提供了JDBC接口,可被JAVA代码无缝集成。比如,将前面的SPL代码存为脚本文件,再在JAVA中以存储过程的形式调用文件名:
Class.forName("com.esproc.jdbc.InternalDriver");
Connection connection =DriverManager.getConnection("jdbc:esproc:local://");
Statement statement = connection.createStatement();
ResultSet result = statement.executeQuery("call getClient()");
SPL代码外置于JAVA,通过文件名被调用,既不依赖数据库,也不依赖JAVA,业务逻辑和前端代码分离,比ORM耦合性更低。
解释执行和热切换
业务逻辑数量多,复杂度高,变化是常态。良好的系统构架,应该有能力应对变化的业务逻辑。ORM的本质是JAVA代码,需要先编译再执行,一般都要停机才能部署,应对变化的业务逻辑时非常繁琐。SPL是基于JAVA的解释型语言,无须编译就能执行,脚本修改后立即生效,支持不停机的热切换,适合应对变化的业务逻辑。
库外计算
存储过程在数据库内进行计算,由此导致了团队协作困难、管理成本高、影响数据库安全。SPL在数据库外进行计算,代码可被第三方工具管理,方便团队协作;SPL脚本可以按文件目录进行存放,方便灵活,管理成本低;SPL对数据库的权限要求类似JAVA,不影响数据安全。
代码移植
存储过程绑定了数据库,移植性非常差。SPL不绑定任何数据库,可以方便地移植代码。
观察前面的代码可以发现,SPL是通过数据源名从数据库取数的,如果需要移植,只要改动配置文件中的数据源配置信息,而不必修改SPL代码。SPL支持动态数据源,可通过参数或宏切换不同的数据库,从而进行更方便的移植。为了进一步增强可移植性,SPL还提供了与具体数据库无关的标准SQL语法,使用sqltranslate函数可将标准SQL转为主流方言SQL,仍然通过query函数执行。
SPL计算能力更强
SPL提供了更多计算函数、更方便的集合运算和有序运算、更专业的结构化数据类型、更方便的数据库读写方法,这些特性使SPL克服了ORM的缺点,具有更强的计算能力,更擅长简化复杂的业务逻辑。
更多计算函数
ORM(HQL)的计算函数不足,很多功能无法实现,或需要编写冗长的JAVA代码。SPL的计算函数在数量和功能上都远超ORM,可直接实现相应的功能,代码量大幅缩短。
比如,时间类函数,日期增减: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),比如支持年份增减、求年中第几天、求季度、按正则表达式拆分字符串、拆出SQL的where或select部分、拆出单词、按标记拆HTML等功能,数量较多,不再赘述。
更强大的集合和有序运算
对结构化数据对象(实体集合)进行集合运算时,ORM通常要用循环语句硬写代码,冗长繁琐,很不方便。SPL提供了简洁易懂的解释型Lambda语法,以及数量众多的集合函数,不仅远远超过ORM,而且比SQL也更方便。
比如,批量修改记录: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没有为跨行运算做优化,代码非常繁琐,SQL虽然可以用关联或窗口函数实现跨行,但代码比较难懂。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、蛇形取值、有序关联等,序表的专业性也超过了SQL的数据表。
更方便的数据库读写方法
大量的业务逻辑要读写批量记录,这种情况下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"), |
/批量追加 |
7 |
=db.update(NT:T,salesR;ORDERID) |
/持久化到数据库 |
总结
存储过程存在架构性缺陷,ORM计算能力不足,两者各有优点和不足,都无法顺畅高效地实现业务逻辑。SPL是专业的结构化数据处理语言,语法灵活,函数丰富,擅长简化复杂业务逻辑,SPL还是解释型语言,支持热切换和低耦合,支持库外计算和代码移植,实现了更优化的架构性。SPL集合了存储过程及ORM的共同优点,可以全面提升开发效率。
英文版