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是一种将结构化数据(表/记录)映射为高级语言的对象的技术,常见的有HibernateQueryDSLJOOQ等。ORM(以Hibernate为例)通过实体类读写数据库,通过JAVAfor/while/if语句进行流程处理,通过HQLJAVA 语法进行结构化数据计算,从而在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也不支持,复杂的业务逻辑难以实现。

ORMHQLJAVA计算能力强,但远不如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)

结构化数据计算能力

业务逻辑围绕结构化数据对象展开,存储过程、ORMSPL都内置专业的结构化数据对象,并据此提供了各自的结构化数据计算函数。类似地,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)

TopNT.top(-3;Amount)

分组topNT.groups(Client;top(3,Amount))

此外,SPL还提供了字符串、日期、数学等多种函数,满足业务逻辑中的计算需求,弥补ORM计算能力不足的问题。

流程控制能力

业务逻辑的难点在于复杂的流程控制。类似JAVAfor/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的缺点,具有更强的计算能力,更擅长简化复杂的业务逻辑。

更多计算函数

ORMHQL)的计算函数不足,很多功能无法实现,或需要编写冗长的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),比如支持年份增减、求年中第几天、求季度、按正则表达式拆分字符串、拆出SQLwhereselect部分、拆出单词、按标记拆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)

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


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"),
221,"BTCH",9,4700,200,date("2022-01-03")])

/批量追加

7

=db.update(NT:T,salesR;ORDERID)

/持久化到数据库

总结

存储过程存在架构性缺陷,ORM计算能力不足,两者各有优点和不足,都无法顺畅高效地实现业务逻辑。SPL是专业的结构化数据处理语言,语法灵活,函数丰富,擅长简化复杂业务逻辑,SPL还是解释型语言,支持热切换和低耦合,支持库外计算和代码移植,实现了更优化的架构性。SPL集合了存储过程及ORM的共同优点,可以全面提升开发效率。