技术分享
NgBatis丨开发常见问题
各位朋友好久不见,这个系列自从上次的《ngbatis系列丨操纵字节码实现xml变代理类》,至今久未更新了。今天的话题是:日常开发中容易碰到的问题。
项目集成类
Q1:与存在 MyBatis 的项目集成
在与使用了 MyBatis 的项目集成时,最重要的是实现四部分代码的两两分离,例如以下配置方式:
- | Java 接口 | xml |
---|---|---|
MyBatis | MapperScan | mybatis.mapper-locations=classpath:mapper/*.xml |
NgBatis | SpringBootApplication.scanBasePackages | cql.parser.mapper-locations=ng-mapper/*/.xml |
1.Java层面,MapperScan 与 SpringBootApplication 中的 ScanBasePackages 二者所指向的包不存在交叉,即:让 MyBatis 发现不了NgBatis 的 Java 接口,同理,让 NgBatis 发现不了 MyBatis 的 Java 接口,避免重复发生会导致异常的动态代理行为。导致异常的原因有:
- 当 MyBatis 扫描到 NgBatis 的 Java 接口,会使用访问关系型数据库的逻辑来代理访问图数据库的接口,与期望相悖。
- 另外 NgBatis 再代理一次,一个接口下产生两个 Bean,会导致通过接口的单例注入报错。
- NgBatis 实现动态代理机制的入口是 xml 中的 namespace,如果添加了类似 @Component 或者 @Mapper 的注解,同样会导致报错。
2.xml 层面,同样需要使 mybatis.mapper-locations 与 cql.parser.mapper-location 所指向的路径不存在交叉。设计之初有不当之处,默认值状态下,二者路径重叠。两个框架的解析逻辑与约束不同,如果 NgBatis 通过 cql.parser.mapper-locations 指定的路径,扫描到 MyBatis的 xml,可能导致解析出错,反之亦然。另外,cql.parser.mapper-locations 所指向的路径至少需要包含一个 xml 文件,这一点会考虑在后续的版本优化。
Q2:与 spring-boot
3.x 集成
在主分支中,保持的版本是与 spring-boot
2.x 的集成,如果需要与 spring-boot
3.x 集成,可以使用带有 -jdk17
后缀的版本。
Q3:与 SpringCloud 集成
扫描接口所在包的注解可以使用 @ComponentScan(...)
,同样需要注意 Q1 中所提的包路径范围。
Q4:与存在 org.apache.shardingsphere 的项目集成
NgBatis 中所使用的 Beetl 模板引擎依赖了 antlr4,shardingsphere 同样依赖了 antlr4,这其中就有可能会产生 jar 包版本的冲突。以 shardingsphere 5.2.0 (antlr-4.9.2) 与 NgBatis 1.2.2 (antlr-4.11.1) 为例,两个版本冲突的解决方式如下:
<dependency>
<groupId>org.nebula-contrib</groupId>
<artifactId>ngbatis</artifactId>
<version>1.2.2</version>
<exclusions>
<exclusion>
<groupId>com.ibeetl</groupId>
<artifactId>beetl-antlr4.11-support</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>com.ibeetl</groupId>
<artifactId>beetl-default-antlr4.9-support</artifactId>
<version>3.15.10.RELEASE</version>
</dependency>
在项目中引入 NgBatis 的同时,排除掉高版本的 antlr,然后尝试引用相对低版本的 antlr,如不是例子中所提的版本,可以在 maven 中查找 beetl antlr support 这个系列的包,并引入相近版本。
自定义 nGQL 用法问题
Q1:参数占位符问题
参数的使用有两个时机可以选择,假如现在有一个参数 param:
执行到数据库时,由数据库读取参数,参数格式为:
$param
,前提是数据库支持该参数位置的参数化,正常用在查询子句的 == 后;执行到数据库前,由 NgBatis 组装到 xml 的 nGQL 模板中,参数格式为:
$param
;如果参数位置是 Schema,可以使用
${ng.valueFmt(param)}
,等价于${param}
;当参数不能确定具体类型时,可以使用
${ng.valueFmt(param)}
进行占位,NgBatis 会根据类型处理成符合语法结构的形式,如 String 类型会在前后追加引号,时间类型会转换成调用 date、datetime 等时间函数的字符串形式,进而完成 nGQL 组装。在模板中,参数还存在两种类型,模板参数类型与 Java 对象类型。如果访问参数方法,可使用
@param.methodName
的方式,如 param 是一个 Map 类型,可以使用@param.get('keyName')
。(1.18 调用 Java 方法与属性 · Beetl3 官方文档 · 看云:https://www.kancloud.cn/xiandafu/beetl3_guide/2138960)
Q2:参数名称问题
当 Java 接口只有一个参数,且为 Map 或对象类型时,可以直接使用 key 或者对象的属性名。否则默认参数名按所处方法的参数位置,取
p0
、p1
…为了代码更具可读性,可在参数名中,对参数追加注解:
@Param("xxx")
,需要注意的是,该注解的包名为:org.springframework.data.repository.query,如误使用了包含 ibatis 的包名,并不会加以识别。
Q3:如何在在多个集合元素使用“,
”分隔
可以使用 ${ng.join(list)}
,List 为传入的变量名。更多 NgBatis 的模板函数用法详见:(https://graph-cn.github.io/ngbatis-docs/dev-example/built-in-function.html)
Q4:如何判断参数是否为空
可以使用 isEmpty(param)
进行判断。更多模板函数详见:(https://www.kancloud.cn/xiandafu/beetl3_guide/2138956)
小结
在 xml 中自定义 nGQL 的用法中,基类 NebulaDaoBasic 实现的方式遵循着相同的模板方式,意味着源码中的 NebulaDaoBasic.xml 中有更多详尽的用例可以提供参考,当然基类为了满足更具通用性,整体用法会显得比常规模板写法要复杂一些。
NgBatis 结构下进行拓展
以下方式如果想对既有类型进行覆盖,需要建立在项目软件包在 org.nebula.contrib 包之后,如:
@SpringBootApplication(scanBasePackages={"org.nebula.contrib", "com.example.xxx"})
Q1:复杂对象作为返回值,如何处理
可以使用继承抽象类的方式对不兼容的类型进行处理,如:
@Component
public class MyTypeHandler extends AbstractResultHandler<MyType, MyType> {
@Override
public MyType handle(MyType newResult, ResultSet result, Class resultType) {
// 在当前位置,从 result 读取数据,填入 newResult 中
return null;
}
}
如果复杂类型需要支持集合的方式,还需要拓展一个集合的接口:
@Component
public class CollectionMyTypeHandler extends AbstractResultHandler<Collection, MyType> {
@Autowired private MyTypeHandler itemHandler;
@Override
public MyType handle(Collection newResult, ResultSet result, Class resultType) {
// 在当前位置,从 result 读取数据,填入 newResult 中
// 取对应元素值,调用 itemHandler 然后调用 newResult.add 完成添加
return newResult;
}
}
Q2:还有哪些接口可以拓展:
1.主键生成接口:PkGenerator,可根据入参获得 Vid 的类型,然后选择使用雪花算法或者 UUID 等方式,完成主键生成策略的指定;
2.模板引擎接口:TextResolver,当模板引擎的实现方式发生替换,需要将 NebulaDaoBasic.xml 迁移到项目的 resources 中,并用新模板引擎的方式进行重写;
3.传入参数的转换接口:ArgsResolver,如现有传入模板参数的转换方案不能满足需求,可对其进行替换,自定义实现类,对 ArgsResolver 进行实现,并注册成组件;
4….
配置相关问题
Q1:如何在控制台中输出 nGQL
logging:
level:
org.nebula.contrib: DEBUG
Q2:如何指定 mapper xml 的放置路径:
cql:
parser:
# 更换开发者自定义的 xml 所在位置
mapper-locations: ng-mapper/**/*.xml # 默认为 mapper/**/*.xml
报错类问题
StackOverflowError
场景1
- 报错时机:模板引擎生成语法树时产生报错
- 解决方式:指定虚拟机启动参数 -Xss2m
场景2
报错时机:参数传入数据库时,会经历一个类型转换的过程,将 Java 对象转换成 Nebula-Java 的可接受的 Value 类型,当 Java 对象的值存在循环依赖时,即 a 对象是 b 对象的属性值之一,同时 b 对象也是 a 对象的属性值之一,会导致递归过深问题。当前最新版本 v1.2.2 还未在类型转换时,做空对象缓存来规避循环问题。
解决方式 1:改变参数类的结构;
解决方式 2:对不需要写入数据库的属性添加 @Transient 注解。
优化类问题
第一种情况,查询语句组装耗时
在 NgBatis 对模板引擎的使用中,有一个比较重的资源使用了懒加载的方式。日志输出对应的是:nGQL make up costs 370ms,如果想把这部分时间挪到服务启动时, 可以在项目中使用以下方式,提前完成资源加载:
@Bean
public TextResolver textResolver(TextResolver resolver) {
resolver.resolve("", Collections.emptyMap());
return resolver;
}
第二种情况,执行查询的耗时
另外还有可能耗时的环节发生在数据库的查询上,日志输出对应的是:query costs 1091ms 如果最小连接数为 0,那么也会在第一次查询时创建连接,也会比单纯的查询本身额外消耗一些时间:可以让最小连接数大于 0,从而在服务启动时,完成连接创建,减少初次查询耗时。
nebula:
pool-config:
min-conns-size: 1
关于多个图空间的使用问题
当项目需要跨越多个图空间进行数据读写时,有以下几种方式可以实现跨空间: 1.在实体类中追加 @Space("myspace") 指定 tag 所属空间
2.在 xml 中的
3.在接口方法的标签中指定 space,如:
以上三种方式可以按需选择。其中第三种方式还支持使用参数的方式,如:
<select id="spaceFromParam" space="${paramMySpace}" spaceFromParam="true">
RETURN true;
</select>
paramMySpace
通过接口的参数传入。据我所知,有开发者在此用法的基础上实现了多租户场景的适配。
总结
目前将 NgBatis 集成到项目中出现得比较多的问题大体上都列在上面了,可能有一些问题的表象会跟列举的内容不太相同,但可以通过类比跟对应问题点附带的参考文档来完成。如果你在开发中碰到一些比较典型或者比较反直觉的问题,也可以留言讨论。
原文作者:大叶 作者公众号:大叶的技术栈 校对&审核:kristain