让通用Mapper支持Join
关于关系型数据库的Join
关于关系型数据库的Join一直是争论不休的。笔者应该是在十年前就不太赞成Join的方式,主要是使用两个思路:
1、 冗余字段,后台保证数据的一致性,前端只需要基于ORM的单表查询即可。
2、 使用多个基于主键的查询来替代Join查询,利用数据库和自定义两级缓存保证效率。
然而,在许多的业务中,恐怕正常情况也都不会轻易去修改ER模型,毕竟可能带来的伤害太大了,并且上述两种方式对于n:n的关系上是几乎无能为力的。
同样现在的公司业务也是充斥着大量的Join,前段时间向主程了解了下,大约70%的业务都需要连表查询,因此使得Mapper或MybatisGenerator形同虚设。这也产生了我去写通用Mapper支持Join的念头(JPA是支持连表的,但是换的代价也同样很大)。
我们先看下简单的演示调用,调用的方式几乎和Example一模一样。如图一所示(仅仅是为了测试,实际环境别以这种恶习去编码):
图一 JoinMapper的调用
然后,我们看实体和Mapper的定义,这也和通用Mapper差不多,只是增加了一些注解和新定义了JoinMapper,如图二所示。
图二 实体与Mapper的定义
其中,这里的ElementTable表示需要Join的Table,JoinOns表示外键的连接,均支持多个,JoinOn为其中一组的连接条件。
一、 基本原理的剖析与遇到的坑
2.1 略过需要了解的知识
(1)MyBatis的原理
(2)TkMybatis的原理
2.2 Provider的提供
我们知道,在TkMybatis的核心是Provider的编写,那我们也着重说一下JoinBaseSelectProvider和JoinSelectExampleProvider,我们看下两个函数,分别为按照主键进行查找和按照条件进行查找,我们先看源代码,如图三,图四所示。
图三 按照主键进行查找的Provider方法
图四 按照条件进行查找Provider方法
熟悉TkMybatis源码的朋友肯定发现几乎和源码是一样的,仅仅SqlHelper换成了JoinTableSqlHelper,那么我们继续看JoinTableSqlHelper和SqlHelper的差别在哪里?
首先,我们也要TkMybatis中Table的改造,如图五所示。
图五 JoinEntityTable的定义
在这里,我们增加了连接表List的定义。
而在EntityHelper中,有setResolve(EntityResolve resolve)这样一个方法,并且能够通过配置的方式完成,配置方式如下所示:
mapper:
resolve-class: com.ai.teach.joinmybatis.mapperhelper.resolve.JoinEntityResolve
在这个Resolve中,我们做了如下的处理:
图六 重载Entityresolve
这里如果类是JoinTable的注解,则按照我们的方式进行解析EntityTable,否则按照默认方式。
2.3 查询表名部分的替换
TkMybatis查询的是单表,因此方法为
翻译成SQL即为
FROM 表名
那么在连表的情况下则为
FROM 表名1 [LEFT|RIGHT] JOIN 表名2 ON {表达式} [LEFT|RIGHT] JOIN ……
因此新增方法:
在这里,我们暂未不处理动态表名的情况,多张表都是动态表名会很麻烦。
2.4 遇到的一个坑
本以为到这里,就完全告一段落了,形成的SQL语句也是正确,可查询的:
SELECT goods.id,name,goods_extern.shortName From goods Left Join goods_extern on ((goods.id=goods_extern.id)) WHERE ( ( goods_extern.shortName like ? ) )
然而发现返回的Json为:[{“name":“测试”}]。
为什么,另外两个字段未被复制呢,我们跟踪到MyBatis的代码,发现是这样处理返回结果赋值给实体的:
接着我们继续看红色箭头部分
我们进入ResultSetWrapper的构造函数
从这里看,并未处理MetaData中的表名,因此无法和ResultMap中的column=”goods.id”进行对应了,于是只能采用下面的修改:
goods.id as 别名
那么在生成的SQL与设置ResultMap中也需要做响应的调整即可。
这样支持Join的通用Mapper就完成了,还有什么需要可以留言。下一章介绍类似JPA的OneToMany定义Mybatis的实体,然后获得Join结果。