sagacity-sqltoy 缓存翻译功能整理,源码拆解
1.ORM 框架之 sqltoy
今天介绍一个让我觉得很特别、用起来特别舒服的 ORM 框架:sagacity-sqltoy,简称:sqltoy,这个框架完全国产,框架作者也是中国人。
作者也一直在推广该框架,让更多人了解 sqltoy,sqltoy 的文档完善,对开发者友好,上手特别简单,我写一些自己玩的项目时,就用的 sqltoy 框架作为持久层框架,也是我选择 ORM 框架的首选。
这个框架我还和同事吐槽过,说框架太智能了,用了这个框架之后,我都不会使用 Mybatis 了,可想而知,这个框架有多优秀,推荐所有人可以学习一下,哪怕你不使用,学习一下这个框架的优点和特别的地方,也能学到很多。
2.sqltoy 是个什么框架
Github 开源地址:https://github.com/chenrenfei/sagacity-sqltoy
Gitee 开源地址:https://gitee.com/sagacity/sagacity-sqltoy
在线文档:https://chenrenfei.github.io/sqltoy/#/
关于 sqltoy 的介绍我从开源仓库上截取了一部分
2.1 sqltoy-orm是什么
sqltoy-orm是比hibernate+myBatis更加贴合项目的orm框架,具有hibernate增删改和对象加载的便捷性同时也具有比myBatis更加灵活优雅的自定义sql查询功能。
支持以下数据库:
- oracle 从oracle11g到19c
- db2 9.5+,建议从10.5 开始
- mysql 支持5.6、5.7、8.0 版本
- postgresql 支持9.5 以及以上版本
- sqlserver 支持2008到2019版本,建议使用2012或以上版本
- sqlite
- DM达梦数据库
- elasticsearch 只支持查询,版本支持5.7+版本,建议使用7.3以上版本
- clickhouse
- mongodb (只支持查询)
- sybase_iq 支持15.4以上版本,建议使用16版本
2.2 是否重复造轮子,我只想首先说五个特性:
2.2.1 根本上杜绝了sql注入问题,sql支持写注释、sql文件动态更新检测,开发时sql变更会自动重载
2.2.2 最直观的sql编写模式,当查询条件稍微复杂一点的时候就会体现价值,后期变更维护的时候尤为凸显
2.2.3 极为强大的缓存翻译查询:巧妙的结合缓存减少查询语句表关联,极大简化sql和提升性能。
2.2.3 最强大的分页查询:很多人第一次了解到何为快速分页、分页优化这种极为巧妙的处理,还有在count语句上的极度优化。
2.2.3 跨数据库函数方言替换,如:isnull/ifnull/nvl、substr/substring 等不同数据库
当然这只是sqltoy其中的五个特点,还有行列转换(俗称数据旋转)、多级分组汇总、统一树结构表(如机构)查询、分库分表sharding、取随机记录、取top记录、修改并返回记录、慢sql提醒等这些贴合项目应用的功能, 当你真正了解上述特点带来的巨大优势之后,您就会对中国人创造的sqltoy-orm有了信心!
sqltoy-orm 来源于个人亲身经历的无数个项目的总结和思考,尤其是性能优化上不断的挖掘,至于是不是重复的轮子并不重要,希望能够帮到大家
这里的介绍我只摘取了一部分,更多的特性介绍可以前往 Github 上查看,下面我们进入本文的主题:缓存翻译
3.缓存翻译功能
缓存翻译,这是一个什么功能?
- 通过缓存翻译: 将 code (编码)转化为名称,无需关联查询,极大简化sql并提升查询效率。
- 通过缓存名称模糊匹配: 获取精准的编码作为条件,避免关联like 模糊查询。
例如 MyBatis:
SELECT
i.staff_id,
i.staff_name,
i.sex_type,
d1.DICT_NAME AS sex_type_name,
i.post,
d2.DICT_NAME AS post_name,
i.create_by,
d3.STAFF_NAME
FROM
sqltoy_staff_info i
LEFT JOIN ( SELECT d.DICT_KEY, d.DICT_NAME FROM sqltoy_dict_detail d WHERE d.DICT_TYPE = "SEX_TYPE" ) d1 ON i.SEX_TYPE = d1.DICT_KEY
LEFT JOIN ( SELECT d.DICT_KEY, d.DICT_NAME FROM sqltoy_dict_detail d WHERE d.DICT_TYPE = "POST_TYPE" ) d2 ON i.post = d2.DICT_KEY
LEFT JOIN ( SELECT info.STAFF_ID, info.STAFF_NAME FROM sqltoy_staff_info info ) d3 ON i.create_by = d3.STAFF_ID
WHERE
i.staff_id = "S0003"
解释下这个 SQL,我要获取 S0003 的个人信息,sex_type、post、create_by 是 code 编码,需要转化为名称,方便前端展示。那我就需要去关联字典表和员工表,造成 SQL 需要进行三次关联。
而 sqltoy:
只需要在 xml 中的 sql 语句上配置 translate 缓存翻译功能就行了,code 编码则会自动转化为名称。
是不是简化了 sql,至于效率也不用担心,首先在 sql 层面省去了多表关联,code 翻译的结果值是从缓存中获取,效率更高。总的来看,提高了效率、简化了 sql 的复杂性,十分方便。
看完了案例,我们就来仔细看看关于缓存翻译的配置使用,以及作者没在文档中详细说的另外两种方式(service、rest)的缓存翻译。
3.1.缓存翻译初始化
缓存翻译的功能实现其实并不复杂,通过阅读源码能了解到缓存翻译的加载和使用。
例如:需要获取员工信息,顺便把员工的性别编码进行翻译,sqltoy 的流程如下:
- 根据业务 sql 去查询员工信息。
- jdbc 查询的结果值进行 aop 过滤,判断是否需要翻译。
- 需要翻译进入翻译逻辑,不需要进行结果值封装,返回结果。
- 进入翻译逻辑,按照翻译 sql 去查询字典值。
- 业务数据和翻译数据结果集,进行业务数据的 code 翻译,替换。
- 封装翻译之后的结果,返回 service 层。
总结一下,整个翻译就是用 Spring AOP 在底层对查询结果进行统一替换处理。
下面我们一起来看一下缓存翻译的加载流程,方便我们后面理解缓存翻译使用,这里建议大家去 Github 上把源码 clone 下来,打断点调两遍,会理解的更加透彻。
3.1.1 加载入口
org.sagacity.sqltoy.SqlToyContext 文件就是 sqltoy 框架加载主入口,这里加载的配置有:sqlToy 配置解析插件、实体对象管理器、翻译器插件、缓存管理器、统一公共字段赋值处理、延时检测时长、数据库方言参数、es的地址配置等等。然后翻译器插件就是缓存翻译。
在 SqlToyContext.initialize 初始化方法中找到翻译器的初始化方法,点进去。
这个方法的主要目的就是:配置缓存翻译、缓存路径、载入具体的缓存翻译配置。
TranslateConfigParse.parseTranslateConfig
方法才是真正的缓存翻译文件的内容解析。
由于方法内容过长,不好截图,我简单概述下方法所做的事,具体内容大家可以通过源码进行了解。
parseTranslateConfig
主要功能有:设置缓存的存储地址、内存大小、过期时间、sql 语句加载、sql 语句参数加载、数据源以及增量缓存的刷新时间等。用大白话来说就是,把缓存 xml 文件中的内容和配置进行解析,加载到 SqlToyConfig 实例里,方便后续的使用。
到这里,缓存翻译的初始化加载流程就是介绍完了,介绍起来几句话就讲完了,但实际的加载流程,建议大家去看看源码。
3.2 三种缓存翻译方式
下面我们就来看看缓存翻译的具体使用方法,先说明,本篇文章的缓存翻译,只是 cache-translates 部分,没有 cache-update-checkers 部分。
- cache-translates 负责将数据加载到缓存
- cache-update-checkers 则负责检查数据是否发生变化清理缓存,当下次使用缓存时会自动重新获取数据放入缓存,从而实现缓存的刷新
3.2.1 sql 缓存翻译
sql 类型的缓存翻译,是最基本的使用,也是作者在文档中公开的使用方式,使用的方式也最简单、广泛,适用于绝大部分翻译场景。
sql 类型缓存翻译案例走起。
第一步,先在缓存翻译的 xml 中,书写好缓存值的 sql 语句。例如我要缓存员工的姓名,员工ID作为缓存 key,姓名作为 value,sql 如下:
<!-- 员工ID和姓名的缓存 -->
<sql-translate cache="staffIdName" datasource="dataSource">
<sql>
<![CDATA[
SELECT
STAFF_ID,
STAFF_NAME
FROM
sqltoy_staff_info
]]>
</sql>
</sql-translate>
cache 是缓存名称,名称必须要唯一(必填),datasource 是当前数据库数据源(非必填)。
第二步,在需要翻译的业务 sql 语句上,配置缓存翻译功能,并指定使用的缓存名称、需要翻译的 key 值。例如:
<sql id="getStaffInfoByStaffId">
<translate cache="staffIdName" columns="create_by" />
<value>
<![CDATA[
SELECT
i.staff_id,
i.staff_name,
i.create_by
FROM
sqltoy_staff_info i
WHERE i.staff_id = :staffId
]]>
</value>
</sql>
translate 可配置的属性列表:
- cache:具体的缓存定义的名称
- cache-type:一般针对数据字典,提供一个分类条件过滤
- columns:sql中的查询字段名称,可以逗号分隔对多个字段进行翻译
- cache-indexs:缓存数据名称对应的列,不填则默认为第二列(从0开始,1则表示第二列),例如缓存的数据结构是:key、name、fullName,则第三列表示全称
然后我们测试一下结果:
service 层,调用上面的业务 sql(getStaffInfoByStaffId):
/**
* 根据 ID 获取 vo
*
* @param staffId
* @return
*/
@Override
public SqltoyStaffInfoVO getStaffInfoByStaffId(String staffId) {
return this.sqlToyLazyDao.loadBySql("getStaffInfoByStaffId", new String[]{"staffId"}, new String[]{staffId}, SqltoyStaffInfoVO.class);
}
test 层:
@Test
void testFive() {
SqltoyStaffInfoVO vo = this.passwordService.getStaffInfoByStaffId("S0001");
Assert.assertEquals("测试失败-性别",vo.getSexType(),"男");
Assert.assertEquals("测试失败-职位类别",vo.getPost(),"管理岗");
Assert.assertEquals("测试失败-职位等级",vo.getPostGrade(),"L10");
System.out.printf("性别:%s,职位:%s,职位等级:%s",vo.getSexType(),vo.getPost(),vo.getPostGrade());
}
测试结果:
可以看到,断言测试通过,并且打印的日志显示,翻译的结果成功,成功把性别、职位类别、职位等级等编码翻译为中文。
3.2.2 service 缓存翻译
service 类型的缓存翻译,作者只是在 sqltoy-starter-showcase 模块项目中的 sqltoy-translate.xml 文件中提到过,具体的案例,在项目中我没有找到,以为功能没实现,作者说实现了,就自己研究了一下,测试了一遍翻译功能。
service 类型的缓存翻译我一开始认为有点多余,我的想法是,都有 sql 类型的缓存翻译了,干嘛多此一举弄一个 service,并且 service 的缓存翻译最终还是用 sql 获取数据。
仔细思考过后,我发现自己错了,存在就是合理的,我认为没有、多余,只是我没有使用场景,而作者开发 service 类型,肯定就是有使用场景的。
思考一番过后,说下我认为 service 类型的使用场景,在微服务系统中,A、B 两个系统是分开的,分别使用各自的库 A 库和 B 库,这个时候,我在 A 系统中查询一些业务数据,其中某个字段的编码所对应的 value 值并不在 A 库中,而是 B 库,这种场景下,数据是跨库的,无法关联查询,只能在业务代码中进行 B 库数据查询,进行编码转换。
而 service 类型的缓存翻译,则可以很顺利的解决这个问题,在 A 系统中通过 Fegin 调用 B 系统的 API,形成一个 service 方法,然后 A 系统在业务 sql 使用 service 类型的缓存翻译,就可以翻译 sql 中的编码了。
说起来有点绕,我们案例整起:
我这里没有开两个服务,而是在 service 里新写一个方法,然后在业务 sql 中调用这个方法,模拟跨服务的场景。
第一步,先把缓存方法书写好。依然是把员工ID翻译为员工姓名。
/**
* 获取所有员工信息记录
*
* @return
*/
@Override
public List<Object[]> queryStaffInof() {
List<SqltoyStaffInfoVO> findStaffInof = this.sqlToyLazyDao.findBySql("findStaffInof", new SqltoyStaffInfoVO());
List<Object[]> list = new ArrayList<>(findStaffInof.size());
findStaffInof.stream().forEach(e -> {
Object[] arr = new Object[]{e.getStaffId(),e.getStaffName()};
list.add(arr);
});
return list;
}
这个方法可以看作是 A 系统中通过 Fegin 调用 B 系统的 API 返回的值。
方法返回类型是:List<Object[]>,这个是缓存翻译底层的限制,返回类型可以是:List>、List<Object[]>。
找到对应的源码可以看到限制:
private static HashMap<String, Object[]> wrapCacheResult(Object target, TranslateConfigModel cacheModel) {
if (target == null) {
return null;
} else if (target instanceof HashMap && ((HashMap)target).isEmpty()) {
return null;
} else if (target instanceof HashMap && ((HashMap)target).values().iterator().next().getClass().isArray()) {
return (HashMap)target;
} else {
LinkedHashMap<String, Object[]> result = new LinkedHashMap();
Object[] row;
if (target instanceof HashMap) {
if (!((HashMap)target).isEmpty()) {
Iterator iter;
Entry entry;
if (((HashMap)target).values().iterator().next() instanceof List) {
iter = ((HashMap)target).entrySet().iterator();
while(iter.hasNext()) {
entry = (Entry)iter.next();
row = new Object[((List)entry.getValue()).size()];
((List)entry.getValue()).toArray(row);
result.put(entry.getKey(), row);
}
} else {
iter = ((HashMap)target).entrySet().iterator();
while(iter.hasNext()) {
entry = (Entry)iter.next();
result.put(entry.getKey(), new Object[]{entry.getKey(), entry.getValue()});
}
}
}
} else if (target instanceof List) {
List tempList = (List)target;
if (!tempList.isEmpty()) {
int cacheIndex = cacheModel.getKeyIndex();
int i;
int n;
List dataSet;
if (tempList.get(0) instanceof List) {
i = 0;
for(n = tempList.size(); i < n; ++i) {
dataSet = (List)tempList.get(i);
Object[] rowAry = new Object[dataSet.size()];
dataSet.toArray(rowAry);
result.put(rowAry[cacheIndex].toString(), rowAry);
}
} else if (tempList.get(0) instanceof Object[]) {
i = 0;
for(n = tempList.size(); i < n; ++i) {
row = (Object[])((Object[])tempList.get(i));
result.put(row[cacheIndex].toString(), row);
}
} else if (cacheModel.getProperties() != null && cacheModel.getProperties().length > 1) {
dataSet = BeanUtil.reflectBeansToInnerAry(tempList, cacheModel.getProperties(), (Object[])null, (ReflectPropertyHandler)null, false, 0);
Iterator var12 = dataSet.iterator();
while(var12.hasNext()) {
Object[] row = (Object[])var12.next();
result.put(row[cacheIndex].toString(), row);
}
}
}
}
return result;
}
}
wrapCacheResult 方法的参数:
- target 就是 queryStaffInof() 方法(可以理解为 B 系统的方法)的返回值。
- TranslateConfigModel 是缓存翻译的模型 Bean,Bean 里面有缓存翻译的相关基础属性,例如:
- 缓存类型(sql,service,rest),
- 数据源,
- 缓存名称,
- 自定义的 ServiceBean,
- 自定义的 ServiceMethod,
- rest 类型的 url 等等。
第二步,在 A 系统中的缓存翻译文件中,配置 service 类型的缓存,方法有参,无参,在缓存的 xml 中没去区别。。
<!-- service 缓存翻译 -->
<service-translate service="com.boyguhui.manage.service.PasswordService" method="queryStaffInof" cache="staffInfoServiceCache" />
- service service 类的路径
- method 具体调用方法
- cache 缓存名称
第三步,A 系统的业务 sql 上配置 service 缓存。这里和 sql 类型的使用方式是一模一样的,service 方法如果有参数,也是通过 cache-type 属性传入。唯一不一样的地方就是 cache 改为 servcie 的缓存名称。
<sql id="getStaffInfoByStaffId">
<translate cache="staffInfoServiceCache" columns="create_by" />
<value>
<![CDATA[
SELECT
i.staff_id,
i.staff_name,
i.create_by
FROM
sqltoy_staff_info i
]]>
</value>
</sql>
然后我们测试一下结果,调用的 service 层方法不变,只是把 service 对应的业务 sql 上的缓存由 sql 类型换为 servcie 类型。
test 层
@Test
void testEight() {
SqltoyStaffInfoVO vo = this.passwordService.getStaffInfoByStaffId("S0001");
Assert.assertEquals("测试失败-创建人姓名",vo.getCreateBy(),"张三");
System.out.printf("创建人姓名:%s",vo.getCreateBy());
}
测试结果:
断言测试通过,并且打印的日志显示,翻译的结果成功,成功把创建人ID翻译为中文名称。
我补充一下,service 缓存是如何通过你配置的 service 和 method 就获取到缓存数据的?其实是通过配置的 service 反射调用 method 来获取数据的,这点可以在源码中找到。
这是缓存翻译的三种类型判断,根据类型调用不同的缓存数据获取方法,我们进入 service 类型看看,看下底层是不是反射。
TranslateFactory.getServiceCacheData() -> SqlToyContext.getServiceData() -> BeanUtil.invokeMethod() -> Method.invoke()。
从 servcie 类型调用 getServiceCacheData() 一直往下,会走到 Method.invoke(),可以证明 servcie 类型缓存翻译方法调用方式通过反射来进行的。
3.2.3 rest 缓存翻译
rest 缓存翻译,它和 servcie、sql 类型都不一样,rest 缓存翻译是通过 url 地址向第三方服务发起请求,获取所需要的缓存值或字典数据。
前面 service 缓存翻译可以跨服务,从 A 服务调用 B 服务的数据,而 rest 则可以跨系统,从 A 系统调用 B 系统的数据(当然, service 也可以做到,在本地 servcie 层通过 HttpClient 调用第三方服务和通过 Fegin 调用其他服务都是一样的)。
说下我认为 rest 缓存翻译的使用场景,假设我公司有两个单体应用 A 和 B,A 和 B 各自有各自的服务器、数据库、nginx,如果 A 需要调用 B 的数据字典,来翻译自己的数据里的某个字段。
这个场景用 service 也能做到,不过需要自己去写 HttpClient 部分的代码,而 rest 则在底层帮用户做好了,只需要提供调用 url 就行。
如果调用的 B 系统接口还有用户身份验证,也可以配置一个账号,进行请求认证。
说了这么多,人都整懵了,我们案例走起。
第一步,在缓存文件中,配置 rest 缓存。
<rest-translate url="http://localhost:8082/password/findDictKeyNameByType" cache="findDictKeyNameByTypeRestCache" username="user" password="123" />
- url 就是你请求的第三方系统地址 (必填)
- cache 缓存名称 (必填)
- username 身份认证的用户名 (非必填)
- password 身份认证的密码 (非必填)
用户名和密码两个属性,看请求的接口,接口需要进行身份认证,就需要加上,不需要认证,则可以没有。
其实到这里,rest 的缓存就配置好了,很简单的一个配置,使用就直接在业务 sql 上配置缓存,指定使用缓存名称为第一步的缓存名称就行了,和使用 sql 类型、service 类型的缓存翻译方式没有区别。
下面通过案例和 rest 类型的源码,来帮助大家更好的理解 rest 类型的原理和身份认证这部分,以及如果是带参数请求,第三方的接口如何接收参数。
这本地写了一个案例,翻译员工的岗位,用的缓存翻译,就是第一步中配置的 rest 翻译,带了一个字典编码作为参数。
然后我们看下 sqltoy 底层是如何调用第三方接口的的。
在 TranslateFactory 类下有 getCacheData 方法
getCacheData 方法是根据不同的缓存类型调用对应的方法获取缓存数据,然后返回上层,进行翻译值的替换。
我们进入 rest 类型的方法看看。
重点看下 332 行,这一行,拿到了请求的 url、username、password、请求参数 Key、请求参数 Value 等信息,通过封装的 HttpClient 进行发起了接口请求。
332 行之后的代码就是对接口响应的数据进行封装处理,把 String 字符串转为 JSON 格式,再转为 List<Object[]> 返回给上层调用方法。
我们继续进入封装的 doPost 方法看看。
重点看两个地方,75 -79 行,这里是设置身份认证的地方,另一个就是 84 - 92 行,这部分是设置请求参数,请求参数的封装用的是:UrlEncodedFormEntity,body 参数格式会转为“KEY1=VALUE1&KEY2=VALUE2&…”这种形式,服务端接收以后也要依据这种协议形式做处理。
好了,rest 类型的底层说完了,一起来看下服务端的代码,以及接收参数的处理。
在方法的第一行,是参数转换处理方法,第二行才是服务端的业务逻辑代码。
由于参数是通过 UrlEncodedFormEntity 方式传递的,我通过 HttpServletRequest 读取字符流,然后转为字符串,根据 = 号切割,获取 value 值,这个 value 值就是 rest 请求带过来字典参数。
整个 rest 配置流程和底层源码都讲完了,我们测试一遍,看看翻译功能对不对。我这里的测试,是把测试的服务,打成 jar 包,用 8082 端口启动,模拟 A、B 两个单体应用,下面是我的测试结果。
可以看到,(8080 A 系统)本地调用 servcie 方法获取员工信息,断言的结果是正确通过的,IDEA 控制台打印的日志显示员工岗位翻译成功。
然后在 (8082 B 系统)的日志上,能看到获取的参数 Value 和查询 sql 日志。
4.缓存翻译底层 Ehcache
这一节,主要说下缓存翻译的底层缓存和一些源码上的内容。
4.1 Ehcache
缓存翻译的缓存,底层实现是:Ehcache,这点在框架的源码能找到,源码位置在:org.sagacity.sqltoy.translate.cache 包下面。
红色部分是缓存翻译相关的文件,包括:解析缓存翻译的配置、定时检测缓存是否更新程序、缓存刷新检测、缓存翻译器、缓存相关 model、缓存实现等。
绿色部分就是缓存的实现,TranslateCacheManager 是抽象类,提供缓存接口规范。
TranslateEhcacheManager 是具体缓存实现类,继承抽象类,实现缓存具体功能。
从实现类中我们可以看到,实现类用的缓存是 CacheManager,而 CacheManager 就是 Ehcache 的缓存管理器。
在实现类重写的 init 方法可以看到,如果 CacheManager 是 null,就为 CacheManager 创建一个实例,提供给其他方法(put、getCache)使用。
4.2 缓存翻译的 AOP 原理
在 3.1.缓存翻译初始化 节我讲了,缓存翻译的工作流程,但只是进行文字描述,没用过这个框架的朋友可能会有点懵,这一小节,我通过源码来仔细梳理缓存翻译的工作流程。
缓存翻译是如何获取值的?又是如何把 code 编码替换为中文名称的?又是如何在翻译之后把结果集返回给我们的 service 层?这一小节具体解决这三个问题。
4.2.1 AOP 处理
我们从业务逻辑代码 servcie 层调用的 SqlToyLazyDao.loadBySql 方法进去,一直往下找:SqlToyDaoSupport.loadBySql -> SqlToyDaoSupport.loadByQuery。
第一行的 SqlToyConfig 是一个 sql 解析对象,里面有 sql 语句属性、参数 key、参数 value、数据库方言、翻译器、脱敏配置等,其实就是 sql.xml 解析出来的对象,里面包含当前 sql 的各种配置。
第二行就是拿到 sqltoyCofing 实例,去查询数据以及根据实例里的配置,去解析 sql 的转换、解析、翻译、脱敏、行转列的操作。进入 findByQuery 方法,看里面的具体操作。
方法的 750 - 755 行,操作的是把 sql 中的参数 key 替换为占位符号,并且把参数 values 按照顺序整理到 queryParam 实例中。
queryParam 的 sql 就是一个完整的 sql,例如:sql select id,name,age from users u where u.name = ? and u.age ?
而 paramsValue 就是 ?的数组 value :[“java”,21]。
然后我们看具体的调用 findBySql 方法,我本地用的 Mysql 数据库,所以我进入的是 Mysql 的实现方法。
一直往下找:MySqlDialect.findBySql -> DialectUtils.findBySql。
现在我们到底层了,相信读者对我标记出来的代码不陌生,JDBC 代码而已,根据 sql 查询数据,然后看 222 行,这里很重要,框架在这里把 JDBC 查询出来的实例 rs 进行了结果值封装、替换,然后把封装、替换之后的 rs 结果集返回给 service 层。
对这个功能有没有很熟悉?像不像 Spring AOP 切面处理?其实就是 AOP 原理,本来我没意识到是 AOP,后面在 sqltoy 的 QQ 群里,作者提了一句,我才意识到。
到这里就能知道我们的结果集为什么会进行 code 编码翻译,翻译为中文名称,因为框架底层对 rs(ResultSet) 集合进行了替换。
然后我们继续看缓存翻译是如何获取值的?又是如何把 code 编码进行替换的?
4.2.2 缓存翻译是如何获取值的?又是如何把 code 编码进行替换的?
点击 222 行的 ResultUtils.processResultSet() 方法。
再进入 104 行的 getResultSet() 方法。
看到 288 行,这里通过 sqlToyConfig.getTranslateMap()
方法获取当前运行的 sql 是否有解析到缓存翻译器。
如果你在业务 sql 上配置了<translate cache="dictKeyName" columns="post" cache-type="POST_TYPE" />
就有缓存翻译器,没配置就代表不需要翻译,就没有缓存翻译器。
然后看到 289 行,如果有缓存翻译器,就获取翻译器,并在 292 根据翻译器获取缓存数据,我们进入到 sqlToyContext.getTranslateManager().getTranslates()
方法看看。
方法 139 行在循环翻译器,然后挨个处理每个翻译器,141 行,判断加载的缓存有没有翻译器里的缓存名称,有就把当前翻译器取出来,通过 143 行的 getCacheData 方法取获取缓存数据。
175 行,先是根据缓存名称取缓存数据,如果获取到的数据为 null 或为空,则通过 TranslateFactory.getCacheData()
方法去数据库获取数据,查到数据之后,放入缓存( 181 行)。
这就是 TranslateFactory.getCacheData()
,根据翻译器的类型,进入不同的缓存数据加载方法。
这里我们看 sql 类型方法。
283 行获取缓存上数据源,如果没获取到就获取当前 sql 解析的数据源,然后 288 行判断缓存有没有传入参数,也就是 <translate cache="dictKeyName" columns="post" cache-type="POST_TYPE" />
cache-type 属性的值,有参数则构造一个带条件 QueryExecutor,调用 DialectFactory.getInstance().findByQuery()
方法就又进入了 AOP 小节的讲的 findByQuery() 方法。
这个方法是不是有点眼熟?其实就是 service 层调用的底层方法,缓存器调用 findByQuery 方法,通过 JDBC 把缓存器解析的 sql 执行,获取到翻译数据。
缓存数据取到了,我们回到 getResultSet(),看到 293 行,缓存数据为 null 或为空,则把 288 行的缓存翻译器标记赋值为:false。
假设缓存数据不为空,缓存标记为 true,方法一直往下运行,找到 370 行(我标记的位置),先是判断缓存标记是否为 true,为真则进入 processResultRowWithTranslate 方法,我们进入该方法。
注意看方法上的注释:@todo 存在缓存翻译的结果处理。这就是我们要找的翻译方法,把 code 编码替换为中文名称。
这里我在本地断点,把方法的运行情况截图下来,方便大家理解各个参数的意思和值以及是如何替换的。
- labelNames 是业务 sql 上的所有字段,例如业务 sql 为:select id,name from users,那 labelNames = [id,name]
- fieldValue 是根据当前 labelNames 获取的 value值,例如当前 label 为 name,fieldValue 则为:张三,也就是业务 sql 查询得到的数据。
- keyIndex 是方法参数 size( labelNames 的长度 ) 的循环当前值。
然后我们看下 translateKey() 方法,还是断点调试的截图。
方法第一行获取了需要翻译的 code 编码,然后判断是不是单值翻译,如果是多个值翻译,就进入 else 进行切割,然后循环翻译,我这里是单值。
cacheValues 是根据翻译的 code 编码获取到对应的缓存对象,如果没有获取到,则代表没查到这个 code 编码的中文信息,打印错误日志。对象存在,就根据翻译器的下标获取中文名称,赋值给 fieldValue 并返回。
这里说一下 translate.getIndex() 下标问题,下标默认是 1,可以在 更改 cache-indexs 属性,这个下标为 1 是什么意思呢?它代表的就是把 code 编码替换为缓存 sql 的哪一个字段。
translateKey() 方法把 code 替换为中文后,返回到 processResultRowWithTranslate() 方法,添加到 rowData(List 集合)实例中,然后逐步返回到 DialectUtils.findBySql(),最终返回到 service 层,被我们获取。
这就是完整的缓存翻译流程,从一开始的业务 sql 查询,到返回 rs AOP 处理,到判断是否有翻译器,到获取缓存数据,到翻译 code,再到翻译完成,返回到 rs AOP 处理位置,返回到 servcie,整个流程,就讲完了。
5.结尾
缓存翻译这个功能,是 sqltoy 框架最吸引我的地方,它的便捷性、性能、sql 简化和设计,都让我着迷。
这里我建议大家把 sqltoy 的源码 clone 下来,仔细看看,会看到很多我们用起来没注意的小细节,能帮助我们更好的理解 sqltoy。
最后,如果我的文章有帮助到大家或者认为写的不错,请分享给更多人,谢谢。
如文章有错误地方,欢迎大家留言或私信(boyguhui@qq.com),告知我,感激不尽。