java开源平台的技术框架非常丰富,但是开源平台上的权限管理、会员管理之类的纯业务模块往往与某种技术框架耦合在一起,比如与mybatis,hibernate等持久层技术耦合。一旦你选定了某个现成的业务组件,就必须接受他对应的持久层框架。比如如果你选择用知名开源框架jeesite做自己的小型web应用系统,你在用它的权限、cms业务模块的时候,就必须用mybaits做持久化框架,而不能用hibernate或spring jpa之类的,这对于只考虑用hibernate而不打算用mybatis的团队来说,不得不得放弃这个框架。所以,我在想,如果能有一些通用的业务组件,可以同时支持mybatis,hibernate等不同的持久层技术,可能具有更好的通用性和可移植性。
在开源平台上,确实有这类似需求的解决方案,比如开源中国上的Uncode-DAL项目,就支持同时支持mybatis、spring jdbc、hibernate等ORM框架。但遗憾的是,它只支持单表操作,不支持多表关联之类的功能。实事上,在待久层框架中,处理多表关联确实是一个比较复杂的问题。
我经过一番摸索,找到了一种感觉还过得去的解决方案,并已经用到了自己的jad项目中。这里先给出实现的思路和原理,以后jad项目开源后,再将这个框架一起开源。
功能需求
既然称之为通用持久层,那么需要实现的功能至少有以下几点:
1、需要同时支持不同的待久层,至少需要同时支持mybatis、hibernate和spring jpa。
也就是说,通过这个框架实现的业务组件,可以在不修改任何业务代码的基础上,可灵活的在mybatis、hibernate和spring jpa之间任意切换。
2、支持多表关联,至少需要支持一对多。
3、能自动处理不同数据库之间的方言,比如分页。
大致实现思路
考虑到不同的持久层框架,对OR映射,数据访问等方式迥异。要做一个他们之间通用的框架,必须在它们之间找到一个共同的规范,让它们同时遵守这个规范,就基本上能达到通用的目地。
似乎所有对数据库的访问,都是通过sql语问来实现的,只不过hibernate等比较重量级的持久化框架还提供一些比如hql之类的语句来访问,然而,这种hql最终还是被框架解释成sql语句被传送到数据库。对于查询语句,在数据库返回数据后,大部分框架都会把数据库的数据转换成相应的实体对像(即使它是传统的关系型数据库),这个过程就是所调的OR映射。只不过不同的框架,对OR映射的实现机制有所不同。
对于mybatis来说,需要开发人员自己维护对像属性与表字段之间的关系,它并没有实现完整的OR映射,但是它有比较完善动态sql引擎,用来实现动态拼接sql访问数据库。而hibernate非常完美的实现了OR 映射并对JPA规范都有很好的支持,而且还提供了一个面象对象的hql查询语言,大大方便了开发人员。spring jpa本身并没有实现数据访问逻辑,它只不过在JPA规范的基础上进行了一些扩展,采用一些大家都能接受的编码规范来精简编码工作。程序员甚至只要写一些简单的操作数据的Dao接口而无需任何实现就可以通过spring强大的IOC机制自动生成Dao的代理实现类来访问数据库。而且,Spring jpa底层并没有实现访问数据库的具体逻辑,对于数据库操作的具体逻辑,它最终还是委托给了比如Hibernate之类的框架作为一个持久化提供者的角色来实现数据访问(Spring jpa默认的持久化提供者就是Hibernate,它支持通过配置,采用别的持久化提供者)。所以,spring jpa的本质只不过是对现有的持久化方案做扩展,以提高开发效率而已。
通过以上分析,可以整理出一个大致的实现通用的持久化思路,就是统一的使用sql访问数据库,并实现统一的OR映射,把这两点作为它们共同的规约。那么问题来了,首先,如何不依赖于特定持久化框架来生成访问数据库的sql语句?其次,对于查询,如何把关系型数转换为对象?如果遇到多表关联怎么办?难点汇总如下:
难点汇总与解决方案
1、如何不依赖于特定持久化框架来生成sql语句
对于普通的insert,update,delete语句,开发人员可以随意写sql语句,直接交给底层实现就行了。但是对于查询,考虑到需要将查询结果映射成对像,那么就不通随意写了,需要符合某些规范。
因为hibernate有比较完整的OR实现,通过面向对象的hql语言就可以操作数据库,程员无需写sql,同样spring jpa也提供了类似的jql语句来进行操作,但mybatis就需要开发人员自己写sql了。对于前两个框架,他们都实现了jpa规范,可以通过jpa注解来做到实体属性与表数据实段之间的映射。基于此,我在想,如果mybatis也能实现jpa规范,哪怕只是部分实现也好。要么,我们自己写实现这个的逻辑,参考jpa规范,我们自己用一些诸如Column之类的注解来标识实现字段,通过反射的方式来构建对应的查询sql。嗯,这确实是一个不错的主意。
2、生成sql语句的过程如何解决表间关联
在不依赖于特定持久化框架来生成sql语句的过程,难免会碰到多表关联的情况,特别在处理查询sql的查询结果时,表间关联的查询比较普遍。上述第一点问题的解决方案中,通过返射来处理这些关联时,稍微复杂一点。
实事上,对于所有持久层技术来说,表间关联都比较难处理。在一些比较小众的对常用持久层框架作改造或扩展的开发者们往往都会避开多表关联,只考虑对单表的持久化功能扩展。而遇到多表关联时,依旧采用原始依赖的持久化框架来实现。比如号称同时支持mybatis、spring jdbc、hibernate等ORM框架的Uncode-DAL框架,它也就只支持单表操作而已,再比如对mybatis扩展的比较好的mybatis-plus项目,它也就只扩展了单表操作,对于多表关联时,还是要自己用原始的mybatis api来做关联映射。
在JPA规范中,处理表间关联时,可以采用一些诸如OneToMany和ManyToOne之类的注解来处理这些关系。所以,在不依赖于特定持久化框架来生成sql语句时,我们同样可以自己写代码来处理ManyToOne之类的注解,把这些注解翻译成我们自己的查询sql,只不过有一点小复杂而已,但致少是一种思路。如果实在处理不了,可以只处理ManyToOne,把它翻译成left join语句。
2、如何处理查询语句的返回结果
对于数据库返回的查询结果,如何转换对像?转换过程中如何处理关联?这确实是一个比较难处理的问题。我们都可以参考mybatis的实现方式,通过在构建查询sql的过程中,把sql中查询字段用类属性名字作为别名,这样mybatis就会自己转换成对像。对于hibernate来说,也可以通过写sql的方式,然后参考hibernate中的基于别名的结果集转换器AliasedTupleSubsetResultTransformer的实现机制来做到转换结果。如果要处理关联,也可以参考mybatis的方式,在别名点用一些点号(".")来处理关联列。而用hibernate时,我们也可以自己写一个结果集处理器,从AliasedTupleSubsetResultTransformer类继承,再在自己的处理器按照别名中的点号来自己实现关联结果的转换。这个过程可能有点复杂,但肯定是行得通的,因为我已经实现了。对于spring jpa,因为没有比较好的结果集处理方案,但是考虑到spring jpa底层,本身也可以依赖于hibernate作为持久化提供者。在这种情况下,我们对查询结果的处理,可以不用spring jpa的api而是退回来,使用原始的hibernate,采用跟hibernate一样的处理方式来处理结果集映射。
如何集成
通过以上分析,我们完全可以不依赖于mybatis和hibernate等特定框架而只采用jpa规范中的注解配合返射,就可以生成自己的sql语句,而且在生成sql语句的过程中,借鉴了mybatis中的按照别名的方式自动映射的机制,生成了方便处理结果集的相对规范的查询语句。
有了上述基础,做一个通用的框架就不难了。基于不重复造轮子的原则,参考spring jpa的架构思路。我们可以采用适配器的设计模式,同spring jpa一样把操作数据库的具体逻辑交给mybatis或hibernate等具体的提供方来实现(实事上,我们做的事情,就是spring jpa做的事情,只不过我们把跟本不支持jpa规范的mybatis也考虑进来了,通用性更强)。
这样,在使用通过这个通用框架的做出来的业务组件时,如果项目用的是mybatis,就通过配置,把操作数据的具体逻辑委托给mybaits实现,在用hibernate的项目里,就把这些逻辑委托给hibernate来做(可以把操作数据库的动作分为两大类,一类是insert,update,delete执行语句,另一类是需要映射结果的select语句)。这样,就实现了无需修改业务组件的代码,而通用于mybatis,hibernate等任何持久化化框架的项目中。
值得一提的是,在集成mybatis的时候,如何自己处理sql中的问题参数占位符,如何分页等,这都需要自己写mybatis插件来实现。另外还有其它的种种细节问题,这里就不多说了。本人已经按照这个思路实现了一个同时支持mybatis,hibernate和spring jpa并且支持一对多映射的通用框架,准备测试好后开源出来,有兴趣的朋友们可以扫以下二维码关注我的原创公众已获取最新信息。