Java 下数据业务逻辑开发技术:Hibernate 和 SPL

在 Java 下实现以数据库为核心的业务逻辑,需要具备三项要素:数据库表的对象化、结构化数据计算能力、流程处理能力。Hibernate 是老牌技术,很早就具备了三项要素,已经在众多项目中实现了大量的数据业务逻辑。SPL 是新晋的数据计算语言,同样具备三项要素,也可以用于实现数据业务逻辑。下面对二者进行多方面的比较,从中找出开发效率更高的数据业务逻辑开发技术。至于其他相关的技术(如 MyBatis、Querydsl、JOOQ 等),出于代码耦合度、计算能力、成熟度等原因的考虑,不在此次讨论之列。

基本特征

编程风格

Hibernate 有两种差异较大的编程风格:结构化数据对象和流程处理使用 Java 的 EntityBean 和 if/switch/while/for 语句,属于面向对象编程;结构化数据计算用 HQL,风格接近 SQL。SPL 对面向对象进行了大幅简化,有对象的概念,可以用点号访问属性并进行多步骤计算,但没有继承重载这些内容,不算彻底的面向对象编程。

运行模式

Hibernate 有两种差异较大的运行模式:Java 代码属于编译型执行,HQL 属于解释执行。SPL 是解释型语言,编码更灵活,但相同代码性能会差一点。不过 SPL 有丰富且高效的库函数,总体性能并不弱,面对大数据量时常常会更有优势。

外部类库

Hibernate 可以引入其他任意的第三方 Java 类库,用来弥补自身的短板,比如用 MyBatis 提高短期的开发效率,用 Stream 弥补 HQL 的计算能力。SPL 内置专业的数据处理函数,提供了大量开发效率更高、时间复杂度更低的基本运算,通常不需要外部 Java 类库,特殊情况可在自定义函数中调用。

IDE 和调试

两者都有图形化 IDE 和完整的调试功能。SPL 的 IDE 专为数据处理而设计,结构化数据对象呈现为表格形式,观察更加方便。Hibernate 使用 Java IDE,好处是更通用,缺点是没有为数据处理做优化,无法方便地观察结构化数据对象。

学习难度

Hibernate 需要学习两种语言,即在 Java 的基础上学习 HQL,两者区别较大,又要配合使用,学习难度因此较大。另外,HQL 试图用面向对象的概念简化 SQL,但有些运算(比如关联、子查询)是 SQL 擅长而对象很难表达的,HQL 为了实现这类运算,设计了比 SQL 更复杂的数据结构和语法规则,实际的学习难度高于 SQL。SPL 的目标是简化 Java 甚至 SQL 的编码,一切都为数据计算服务,刻意简化了许多面向对象的概念,学习难度很低。

代码量

HQL 表达能力不如 SQL,复杂些的结构化数据计算要依靠特殊技巧变相实现,包括硬编码、方言 SQL、高性能计算类库等方式,导致编写、集成、管理的难度都很大。Java 的流程处理语句是为通用计算设计的,不是专门为结构化数据对象(List<EntityBean>)设计的,代码量也不小。SPL 表达能力强于 SQL,可用更低的代码量实现结构化数据计算,SPL 的流程处理语句专为结构化数据对象而设计,代码量低于 Java。

结构化数据对象

结构化数据对象用于将数据库表对象化,这是数据处理和业务逻辑开发的基础,专业的结构化数据对象可以方便地与数据库交换数据,支持丰富的计算函数,并简化流程处理的难度。

Hibernate 并没有专业的结构化数据对象,通常借用 Java 的 List<EntityBean>(本文以此为主进行说明),有时也用 Stream<EntityBean> 或 List<Object[]>。SPL 设计了专业的结构化数据对象,包括序表(本文以此为主进行说明)、内表压缩表、外存 Lazy 游标等。

定义

Hibernate(Java)是强类型的编译型语言,使用结构化数据对象前必须先定义数据结构,过程相当麻烦。以 Orders 表为例,先在 hibernate.cfg.xml 里定义实体配置文件名和实体类名:

<mapping resource="OrdersEntity.hbm.xml"/>
<mapping class="table.OrdersEntity"/>

再在实体配置文件定义数据库表和实体类的元数据对应关系,部分内容:

<class name="table.OrdersEntity" table="orders" schema="test">
<id name="orderId" column="OrderID"/>
<property name="client" column="Client"/>
<property name="sellerId" column="SellerId"/>
<property name="amount" column="Amount"/>
<property name="orderDate" column="OrderDate"/>
</class>

再在实体类里实现具体的属性和方法,部分内容:

@Entity
@Table(name = "orders", schema = "test", catalog = "")
public class OrdersEntity {
private Integer orderId;
private String client;
private Integer sellerId;
private Double amount;
private Date orderDate;
@Basic
@Column(name = "OrderID")
public Integer getOrderId() {
return orderId;
}
…

上面是最简单定义过程,如果遇到多表关联、自增类型、联合主键等情况,定义起来就更麻烦了,比如联合主键就需要定义额外的类。事实上,Hibernate 定义结构化数据对象的过程比 SQL 复杂多了,以至于要用专门的注解来简化,并没有达到提高开发效率的目标。虽然可以用工具将定义的过程自动化,但手工定义总是难以避免的(后面会遇到)。

SPL 是解释型语言,取数和计算后可以方便地推断出新的数据结构,一般不需要事先定义。

读数据库

将外部数据库的记录(集合)读为 Hiberante 内部的 List<EntityBean>,主要通过主键查询、HQL 查询、SQL 查询等方式。以最常用的 HQL 为例:

String hql ="from OrdersEntity where sellerId=10";
Query query = session.createQuery(hql);
List<OrdersEntity> orders = query.list();
for(OrdersEntity order : orders){
System.out.println(order.getOrderId()+"\t"+order.getClient()+"\t"+order.getSellerId()+"\t"+order.getAmount()+"\t"+order.getOrderDate());
}

Hibernate 代码不算长,逻辑也容易看懂。要注意的是,前面必须先定义数据结构,后面的输出代码不是必须的,主要为了解决 IDE 不便观察结果的麻烦。

SPL 主要通过 SQL 查询生成序表:

T=orcl.query("select * from test.Orders where sellerId=?",10)

SPL 代码更简单,不必事先定义数据结构,不必额外输出,可以在 IDE 中直观查看结果。

写数据库

将处理后的结构化数据对象持久化保存到数据库,Hibernate 主要通过 save(新增实体)、update(修改实体)、delete(删除实体) 等函数。以批量新增实体为例:

Transaction tx = session.beginTransaction();
for ( int i=0; i<orders.size(); i++ ) {
session.save(orders.get(i));
}
tx.commit();

Hibernate 代码表面上不算麻烦,但实际上存在隐患。Hibernate 会在 save/update 实体时不断生成缓存,直至内存崩溃或手工清空缓存,这就导致上述代码只适合新增 / 修改少量实体。如果新增 / 修改较多实体,就要解决缓存溢出的问题,上述代码还要改造,每新增一定的实体(比如 100 条,根据内存和并发而定),就将缓存刷新到数据库(sesseion.flush()),并清空缓存(session.clear())。hibernate 用不同的函数分别实现了新增、修改、删除,用法区别较大,如果遇到混合更新的情况,就要分成三种情况分别进行批量处理,代码复杂多了。

SPL 只用一个 update 函数就实现单条或批量记录的新增、修改、删除,且支持混合更新。比如:原序表为 T,经过增删改一系列处理后的序表为 NT, 将变化结果持久化到数据库的 orders 表:

orcl.update(NT:T,orders)
orcl.commit()

SPL 代码简单多了,而且保存时不必手工控制缓存或内存。

访问字段

hibernate 采用 EntityBean 作为结构化数据对象,可以用对象的形式方便地访问字段,比如读取单条记录的 Client 字段:

Orders.get(3).getClient()			

Hibernate 访问单条记录的字段比较方便,但一旦要访问记录集合的字段,代码就不那么好写了。比如取一列,要写成这样:

List<String> Clients=Orders.stream().map(Order->Order.getClient()).collect(Collectors.toList());

为了简化代码,上面借用了 Stream 对象,Java8 以上支持,否则写法更复杂。如果使用 Java16 以上版本,最后转回 List 可以更简单些,但整体还是很麻烦。至于取多个字段、按默认字段名取、按字符串名字取、按字段序号取,Hibernate 实现起来就更麻烦了。就连取字段名列表这种貌似基本的操作,对 Hibernate 来说也是件相当麻烦的事。以上种种麻烦,都是 Hibernate 专业性不足的体现。

SPL 序表是专业的结构化数据对象,同样可以用对象的形式方便地访问字段。比如单条记录的 Client 的字段:

Orders(3).Client								

专业性更多体现在访问记录集合的字段,比如取一列:

Orders.(Client)	

更专业的是,SPL 提供了多种字段访问方法,以提高不同场景下的开发效率:

Orders.([Client,Amount])					//取多个字段
Orders.([#2,#3])						//按默认字段名取
Orders.field(“Client”)						//按字符串名取(外部参数)
Orders.field(2)							//按字段序号取
Orders.fname()							//取字段名列表

有序访问

Hiberante 的 List<EntityBean> 是有序集合,支持顺序相关的功能,但因为专业性不足,只提供了最基础的功能,比如按下标取记录、按区间取记录:

Orders.get(3)
Orders.subList(3,5); 

再进一步的功能,就需要硬编码实现了,比如后 N 条:

Collections.reverse(o1);
List<OrdersEntity> o4= o1.subList(0,3);

至于按位置集合取记录、步进取记录等功能,表达起来就更麻烦了。

SPL 序表同样是有序集合,提供了顺序相关的基本功能,比如按下标取、按区间取:

Orders(3)
Orders.to(3,5)

序表是专业的结构化数据对象,许多顺序相关的高级功 hibernate 没有支持,SPL 则直接提供了,比如按倒数序号取记录,可以直接用负号表示:

Orders.m(-3)						//倒数第3条
Orders.m(to(-3,-5))					//倒数区间

再比如按位置集合取记录、步进取记录:

Orders.m(1,3,5,7:10)					//序号是1、3、5、7-10的记录
Orders.m(-1,-3,-5)					//倒数第1,3,5条
Orders.step(2,1)					//每2条取第1条(等价于奇数位置)

结构化数据计算

结构化数据计算能力是数据业务逻辑的核心要素,下面从简单到复杂选取几个常见题目,比较 Hibernate 和 SPL 的计算代码。

基本计算

基本计算包括选出部分字段、过滤、分组汇总、排序、关联等。这里选用最简单的计算,从现有的结构化数据对象中选出部分字段,形成新的结构化数据对象。等价的 SQL

select client,amount from Orders

用 Hibernate 实现上述功能:

String hql = "select new ClientAmount(client,amount) from Orders";
Query query = session.createQuery(hql);
List<ClientAmount> clientAmounts = query.list();

就计算代码而言,Hiberante 没有比 SQL 复杂太多,真正的麻烦在于事先要手工定义一个新的实体类(ClientAmount),该类应包含选出的字段,并体现在构造函数的参数中。先定义结果再写计算过程,违反正常的思维习惯,严重影响业务逻辑开发的流畅性。
除了常规方法,还有两种可选的做法。第一种,在现有实体类中新增构造函数,这样不用定义新类,但会增加计算代码之间的耦合性,弊大于利。第二种,直接用 HQL"select id,name from Orders" 返回 List<Object[]>,这样也不用定义新类,但会导致结构化数据类型不统一,会带来更多的麻烦。
需要事先定义数据结构的不止选出部分字段,还有分组汇总、关联等。其中,事先定义关联计算的结果尤其麻烦。较复杂的计算需要将多种基本计算组合使用,可以想象实现过程会更加麻烦。这些麻烦体现了 Hibernate 在数据业务逻辑开发方面专业性不足的问题,本质是因为 HQL 是解释型代码,而 EntityBean 是编译型代码,两者虽然勉强统一在 Java 体系内,但风格差异大,接口太复杂,配合不方便。

同样选出部分字段,SPL 就简单多了:

Orders.new(Client,Name)

SPL 不必事先定义计算结果的数据结构。类似地,所有的基础计算和复杂计算都不必定义数据结构。SPL 的计算代码简单易懂,体现出 SPL 在数据业务逻辑开发方面的专业性,本质是因为 SPL 是解释型语言,序表和计算函数经过整体设计,风格一致,接口简单,配合方便。

多步骤计算

在前一个查询结果的基础上继续查询,可以将大的计算目标分解为多个小步骤,通过解决简单问题最终实现复杂问题的求解。SQL 里的多步骤计算是 from 子查询或 with 子查询,形如:

select orderId, m from (select orderId, month(orderDate) m from Orders) t1

Hibernate 不直接支持多步骤计算,只能换其他思路,常见的做法是用上面的方言 SQL,这里就不重复了。
方言 SQL 短期来看的确可以解决问题,但严重违背了 Hibernate 的初衷,丧失了面向对象、(Java)风格一致、移植性等优点。还可以采用其他做法,常见的有 Java 硬编码、SQL 视图、Hibernate 虚拟视图等,但无论哪种做法,开发效率都不会太高,且日后维护非常麻烦。

SPL 代码:

Orders.new(orderId, month(orderDate): m).new(orderId, m)

多步骤计算是 SPL 基本功能,实现起来很容易,且具备对象访问、风格一致、移植性强的优点。

有序运算

先看简单的有序运算,以前 N 条记录为例,对应的 SQL(Oracle):

select orderId,client,amount,orderDate from OrdersEntity where rownum<=3

Hibernate 代码:

session.createQuery("select orderId,client,amount,orderDate from  OrdersEntity").setMaxResults(3);

Hibernate HQL 不支持 rownum,不能独立完成计算,要配合 Java 代码(setMaxResults)才能解决问题。虽然代码不长,但要在两种截然不同的语法间切换,编写体验较差。此外应该发现,EntityBean 是有序的,而 HQL 是无序的,两者区别较大配合困难,却硬被拉在一起完成计算任务,这也是 Hibernate 不专业的体现。
SPL 代码:

Orders.new(orderId,client,amount,orderDate).to(3)

SPL 代码不仅简单,而且只有一种代码风格,编写时无割裂感和切换动作,体验较好。

再看复杂些的有序运算,以分组环比为例,对应的 SQL(Oracle)一般可用窗口函数实现:

select sales,month,v1,v2, (v1-v2)/v2 looprate from(
    select sales, month,amount v1,lag(amount,1) over(partition by sales order by month) v2 
    from tbl)

HQL 不够专业,不支持窗口函数,无法实现较复杂的有序计算,只能用方言 SQL 或硬编码等方式,远离 Hibernate 的初衷,开发效率很低。
SPL 代码:

tbl.sort(sales,month).derive(if(sales==sales[-1],(amount-amount[-1])/amount[-1]):loopRate)

SPL 是专业的业务逻辑开发技术,擅长进行有序计算,上面代码用 [-1] 表示当前记录的上一条记录,这是相对位置的表示方法,比 lag 函数(绝对移位)更易理解。

流程处理

Hibernate 和 SPL 都提供了通用的判断语句和循环语句,包括 if、三目判断、for、case 等,可实现任意复杂的流程处理,这里不多做讨论,下面重点比较针对集合数据的循环结构是否方便。

Hibernate 支持 foreach 循环函数,针对结构化数据对象进行了优化,比如根据一定的规则计算奖金:

Orders.forEach(r->{
    Double amount=r.getAmount();
    if (amount>10000) {
        r.setBonus(amount*0.05);
    }else if(amount>=5000 && amount<10000){
        r.setBonus (amount*0.03);
    }else if(amount>=2000 && amount<5000){
        r.setBonus(amount*0.02);
    }
});

forEach 的参数是 Lambda 表达式,可以简化函数的定义,可以方便地处理集合对象的每个成员(代码中的循环变量 r)。forEach 函数配合 Lambda 语法,整体代码要比传统循环语句简单些。但也应该注意到,forEach 函数里使用字段需要附带循环变量名,对单表计算来说是多余的,同样使用 Lambda 语法的 SQL 就可以省略变量名。实际上,定义循环变量名都是多余的,SQL 就不用定义。这些缺点都说明 Hibernate 在流程处理方面还不够专业。

SPL 也有针对结构化数据对象进行优化的循环函数,直接用括号表示。同样根据一定的规则计算奖金:

Orders.(Bonus=if(Amount>10000,Amount*0.05,
if(Amount>5000 && Amount<10000, Amount*0.03,
if(Amount>=2000 && Amount<5000, Amount*0.02)
)))

SPL 的循环函数同样支持 Lambda 表达式,而且接口更简单,不必定义循环变量,使用字段时不必引用变量名,比 Hibernate 更方便,专业性也更强。除了循环函数,SPL 还有更多专业的流程处理功能,比如:每轮循环取一批而不是一条记录;某字段值变化时循环一轮。

函数和语法

时间和字符串函数

Hibernate 内置的时间和字符串函数较少,很多常用函数都没有直接提供,只能用方言 SQL 或硬编码实现,比如:日期增减、求年中第几天、求季度数,以及替换、left 截取、求 ASCII 码等。

以日期增减为例,假设数据库是 MySQL,则应当先创建自定义方言类,继承 org.hibernate.dialect.MySQLDialect 父类。在自定义方言类中注册 HQL 函数名(比如 udf_dateadd),并引入 MySQL 的日期增减函数 date_add,关键代码为:

registerFunction("udf_dateadd", new SQLFunctionTemplate( DateType.INSTANCE,"date_add(?1,INTERVAL ?2 DAY)") );

实际计算时,再编写 HQL

select udf_dateadd (orderDate,3) from OrdersEntity

上述代码不仅编写麻烦,而且无法移植,一旦更换数据库,就要重写自定义方言类。

SPL 内置丰富的日期和字符串函数,包括日期增减。同样的功能代码:

Orders.new(elapse(ORDERDATE,3))

上述 SPL 代码与数据库无关,不必修改就可以在不同数据库中迁移。
类似地,SPL 也直接支持取年中第几天,求季度数,以及字符串替换、left 截取、求 ASCII 码等函数,移植时同样不必修改。

SPL 函数选项和层次参数

值得一提的是,为了进一步提高开发效率,SPL 还提供了独特的函数语法。

有大量功能类似的函数时,大部分程序语言只能用不同的名字或者参数进行区分,使用不太方便。而 SPL 提供了非常独特的函数选项,使功能相似的函数可以共用一个函数名,只用函数选项区分差别。比如,select 函数的基本功能是过滤,如果只过滤出符合条件的第 1 条记录,可使用选项 @1:

T.select@1(Amount>1000)

对有序数据用二分法进行快速过滤,使用 @b:

T.select@b(Amount>1000)

函数选项还可以组合搭配,比如:

Orders.select@1b(Amount>1000)

有些函数的参数很复杂,可能会分成多层。常规程序语言对此并没有特别的语法方案,只能生成多层结构数据对象再传入,非常麻烦。SQL 使用了关键字把参数分隔成多个组,更直观简单,但这会动用很多关键字,使语句结构不统一。而 SPL 创造性地发明了层次参数简化了复杂参数的表达,通过分号、逗号、冒号自高而低将参数分为三层:

join(Orders:o,SellerId ; Employees:e,EId)

SPL 完整例子

SPL 以专业的结构化数据对象为基础,配合专业的结构化数据对象和专业的流程处理功能,可大幅提高数据业务逻辑的开发效率。比如:根据规则计算出奖金,向数据库插入包含奖金字段的记录,Hibernate 需要多个文件和大段代码才能实现,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


/返回入库结果

应用结构

Java 集成

Hibernate 代码本身就是 Java,可被其他 Java 代码直接调用。

SPL 是基于 JVM 的数据计算语言,提供了易用的 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()");

热部署

Hibernate(Java)是编译型语言,不支持热部署,修改代码后需要重新编译并重启整个应用,加大了维护难度,降低了系统稳定性。

SPL 是解释型语言,代码以脚本文件的形式外置于 JAVA,支持热部署,修改后不必编译,也不必重启应用。由于 SPL 代码不依赖 JAVA,业务逻辑和前端代码物理分离,耦合性也更低。

代码移植

代码移植是 Hibernate 的设计目标之一,即业务逻辑可在不同的数据库之间无缝切换,但由于结构化数据计算能力不足,Hibernate 在实践中普遍使用方言 SQL,深度绑定数据库,基本失去了移植能力,除非业务逻辑足够简单。

SPL 计算能力更强,不必借用 SQL,只用丰富的内置函数库就能实现复杂的结构化数据计算,这样的计算代码可在数据库间无缝移植。在数据库取数代码中,SPL 通常用 query 函数执行方言 SQL 生成序表,虽然取数 SQL 比较简单,手工移植不难,但仍有一定工作量,为了使取数代码便于移植,SPL 专门提供了不依赖特定数据库的通用 SQL,可在主流数据库间无缝移植。


通过多方面的比较可知:Hibernate 在针对数据库的业务逻辑开发方面专业性严重不足,开发效率低下。SPL 语法简练、表达效率高、代码移植方便,结构化数据对象更专业,函数更丰富且计算能力更强,流程处理更方便,开发效率远高于 Hibernate。