前言
本文提出的方案不一定是最优的方案,并且还有一些问题没有解决,如果您有更好的解决方案或是建议,请话一分钟的时间提出来,我会作出改正。
我的项目地址,欢迎start或fork
读场景简述
在使用了DDD设计原则之后,对于持久层操作,一个聚合是最小粒度,这就带来一个问题(我们假设项目使用关系型数据库):
- 如果我要查询某个聚合中的某个对象或是某个属性,我就必须要查询整个聚合。
为什么会有这个问题?举个例子,我们现在有一个订单聚合
订单聚合{
订单信息{下单时间,付款总额},
购买人信息{购买人姓名},
订单详情{包含的商品}
}
我现在需要查询某笔订单中包含的商品信息,一般来说步骤是:
- 1.查询出整个订单聚合
- 2.根据订单聚合来得到订单详情
我们再回过头来看:
- 其实我们真正需要的仅仅是聚合中的某一个成员属性,映射到数据库中可能是某个或某些表。
- 但是我们不得不查询整个聚合,映射到数据库可能需要关联更多的表。
那么流程就是这样的:
-
1.查询对应的PO
-
2.将PO转换成DO
-
3.将DO转换成DTO
真正有意义的数据其实就是该笔订单包含的商品信息,但是我们却读了很多不需要的数据,比如下单时间,付款总额,购买人姓名等等。。。。。。
那么有没有办法优化呢?
- 我采取的方案的是懒加载 + 延迟传递
解决方案:懒加载+延迟传递
懒加载
碰到这个场景,我首先想到的就是能不能将持久层的查询延迟到真正使用到数据的时候再去查询(这种延迟机制太常见了,比如hashmap真正put的时候再去初始化容量),后来我看到mybatis确实是有懒加载的机制,使用cglib(所以我们的PO不能是final的)
返回一个代理后的对象,并且拦截它的获取属性方法,在这时再去数据库查询。
简单看一下mybatis懒加载用法:
使用@Result并且为其指定对应的懒加载的select方法就可以实现懒加载了,在真正使用到GoodsColumnPO的column的属性时mybatis会去调用select的方法执行加载。
- 这里有个小技巧:你可以在@Select中这么写:
这样就连Select中的语句也不会真正执行mysql的引擎查询,也可以得到方法的参数作为延迟加载的参数了。
如果是三层架构,这里已经解决问题了,但是在DDD中,这还不够。
- 因为查询到PO之后需要将其转换成DO,而这个转换过程会触发mybatis的加载。
那么现在问题又变成了:我需要将PO转换成DO,并且不触发mybatis的延迟加载,而将这个延迟加载的特性传递出去,这就用到了下面的方案:延迟传递。
延迟传递
说到延迟传递,我第一个想到的就是使用代理,生成一个代理类,去拦截这个代理类的所有属性的getter方法,在这时再去真正调用原始对象获取属性(当然了,在这个场景下原始对象是mybatis生成的代理对象),即这样:
-
1.调用mybatis查询
在我们调用mybatis延迟查询的时候,mybatis会给我们一个代理对象,并且我们去尝试获取这个代理对象的时候去数据库查询。 -
2.生成我们的代理对象
我们需要创建一个新的代理对象,并拦截它的getter方法,在这时候再去从mybatis给我们的代理对象中获取我们需要的属性。
实现思路
方案总是简单的,但是真正实现就会有很多问题。
关于延迟加载
,我们只需要遵循mybatis的mapper编写规范就可以了。
主要是如何实现延迟传递:
- 1.我们的代理类中需要存放一份mybatis代理对象的引用
- 2.我们的代理类需要对getter进行增强
- 3.哪些getter需要被拦截,我们需要获取这个列表
- 4.拦截getter后我们需要返回什么样的值
因此,最初的设计是这样的:
- 1.创建一个mybatis代理对象的代理,并在其中保存一份原始对象的引用
- 2.对该对象的getter方法进行拦截
- 3.遍历这个PO,将其中含有的共有域field作为需要拦截的属性列表
- 4.根据getter的不同调用source中对应的method来获取
使用cglib实现1和2是很简单的,但是新的问题又有了:
PO对象和DO对象可能类结构不同,怎样将mybatis给我们的PO对象转换成我们需要的DO对象?
为此,我们需要遍历PO对象中所有可获得的field,并将其的method调用链维护在我们的代理对象中(你可能会问为什么不直接维护对应的field,因为一旦尝试获取它的field,mybatis将会从数据库加载,这将功亏一篑
),在对应的getter被调用的时候再去原始对象中执行method调用链获取。
那么整体的实现就是这样的:
- TargetEncher是我们的代理对象容器
我们需要在容器中维护一个原始对象(proxySource)和PO中所有可获得的属性map(lazyProperties),他需要解析PO中所有可获得的属性map并将其配置到一个getter拦截器(configInterceptor GetterInvokeInterceptor)中去。 - GetterInvokeInterceptor是我们的getter拦截器,继承自Cglib的MethodInterceptor
我们需要它来对我们的代理类的getter进行拦截(intercept),并且选择对应的property去返回(loadProperty)。 - LazyPropertyHolder是我们的属性持有懒加载类
最后,我们需要一个类来持有一个属性,而这个属性需要是延迟加载的。
可能的问题
即时对象过多
对于每次查询,每个PO我们都会遍历它的可获得属性来维护一个lazyProperties集合,这集合中每个fieldName我们都会创建一个cglib延迟加载holder对象。
- 如果查询一个大聚合,并且是列表查询,可能会突然创建很多的对象。
如何解决?
- 我打算对LazyPropertyHolder做点简单的优化,具体方案还没有想好。
多次隐式查询
本方案是建立在mybatis的懒加载机制上的,即每次获取属性的时候可能会尝试从数据库加载(如果之前没有加载过的话),这就带来一个问题:
- 如果我使用了这个方案,但是我在实际使用时却get了几乎大部分(甚至所有的属性),这就导致了mybatis多次建立数据库连接去查询需要的属性,这样多次查询肯定是比一次全部查询效率要低的。
如何解决?
- 方案
对于每一次查询(从PO到DO到DTO),我需要维护一个查询耗时时间列表(可以是平均数,中位数等等。。),每次查询时根据不同的策略(可以参考查询耗时来进行权重选择)采取不同的方案(是否选择懒加载)。 - 问题
- 时间列表如何维护?
主要问题
这个方案需要统计从PO到DO到DTO,怎样设计才能符合向内依赖解耦原则?
写场景简述
// TODO