项目集成类
问题1:与存在MyBatis的项目集成
在与使用了MyBatis的项目集成时,最重要的是实现四部分代码的两两分离,例如以下配置方式:
- | java接口 | xml |
---|---|---|
MyBatis | MapperScan | mybatis.mapper-locations=classpath:mapper/*.xml |
NgBatis | SpringBootApplication.scanBasePackages | cql.parser.mapper-locations=ng-mapper/**/*.xml |
- Java层面,MapperScan与SpringBootApplication中的scanBasePackages 二者所指向的包不存在交叉,即:让 MyBatis发现不了NgBatis的java接口,同理,让NgBatis发现不了MyBatis的java接口,避免重复发生会导致异常的动态代理行为。导致异常的原因有:
- 当MyBatis扫描到NgBatis的java接口,会使用访问关系型数据库的逻辑来代理访问图数据库的接口,与期望相悖。
- 另外NgBatis再代理一次,一个接口下产生两个Bean,会导致通过接口的单例注入报错。
- NgBatis实现动态代理机制的入口是xml中的namespace,如果添加了类似
@Component
或者@Mapper
的注解,同样会导致报错。
- xml层面,同样需要使得
mybatis.mapper-locations
与cql.parser.mapper-location
所指向的路径不存在交叉。设计之初有不当之处,默认值状态下,二者路径重叠。两个框架的解析逻辑与约束不同,如果NgBatis通过cql.parser.mapper-locations指定的路径,扫描到MyBatis的xml,可能导致解析出错,反之亦然。另外,cql.parser.mapper-locations所指向的路径至少需要包含一个 xml 文件,这一点会考虑在后续的版本优化。
问题2:与spring-boot
3.x 集成
在主分支中,保持的版本是与spring-boot
2.x 的集成,如果需要与spring-boot
3.x集成,可以使用带有 -jdk17
后缀的版本。
问题3:与 SpringCloud 集成
扫描接口所在包的注解可以使用@ComponentScan(...)
,同样需要注意问题1中所提的包路径范围。
问题4:与存在 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的用法问题
问题1:参数占位符问题
参数的使用有两个时机可以选择,假如现在有一个参数 param:
- 执行到数据库时,由数据库读取参数,参数格式为:
$param
,前提是数据库支持该参数位置的参数化,正常用在查询子句的==
后; - 执行到数据库前,由NgBatis组装到xml的nGQL模板中,参数格式为:
${param}
; - 如果参数位置是schema,可以使用
${ng.schemaFmt(param)}
,等价于 `${param}`; - 当参数不能确定具体类型时,可以使用
${ng.valueFmt(param)}
进行占位,NgBatis会根据类型处理成符合语法结构的形式,如 String 类型会在前后追加引号,时间类型会转换成调用 date、datetime 等时间函数的字符串形式,进而完成nGQL组装。 - 在模板中,参数还存在两种类型,模板参数类型与java对象类型。如果访问参数方法,可使用
@param.methodName
的方式,如 param 是一个 Map 类型,可以使用@param.get('keyName')
。(1.18 调用Java方法与属性 · Beetl3官方文档 · 看云)
问题2:参数名称问题
- 当java接口只有一个参数,且为 map 或对象类型时,可以直接使用 key 或者对象的属性名。否则默认参数名按所处方法的参数位置,取
p0
、p1
… - 为了代码更具可读性,可在参数名中,对参数追加注解:
@Param("xxx")
,需要注意的是,该注解的包名为:org.springframework.data.repository.query,如误使用了包含 ibatis 的包名,并不会加以识别。
问题3:如何在在多个集合间追加,
可以使用 ${ng.join(list)}
,list为传入的变量名。更多NgBatis的模板函数用法详见:(NgBatis 内置函数与变量 | NgBatis Docs)
问题3:如何判断参数是否为空
可以使用isEmpty(param)
进行判断。更多模板函数详见:(1.14 函数调用 · Beetl3官方文档 · 看云)
小结
在 xml 中自定义 nGQL 的用法中,基类 NebulaDaoBasic 实现的方式遵循着相同的模板方式,意味着源码中的 NebulaDaoBasic.xml 中有更多详尽的用例可以提供参考,当然基类为了满足更具通用性,整体用法会显得比常规模板写法要复杂一些。
在NgBatis的结构下进行拓展
以下方式如果想对既有类型进行覆盖,需要建立在项目软件包在 org.nebula.contrib 包之后,如:
@SpringBootApplication(scanBasePackages = {"org.nebula.contrib", "com.example.xxx"})
问题1:复杂对象作为返回值,如何处理
可以使用继承抽象类的方式对不兼容的类型进行处理,如:
@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;
}
}
问题2:还有哪些接口可以拓展:
- 主键生成接口:
PkGenerator
,可根据入参获得vid的类型,然后选择使用雪花算法或者UUID等方式,完成主键生成策略的指定; - 模板引擎接口:
TextResolver
,当模板引擎的实现方式发生替换,需要将 NebulaDaoBasic.xml 迁移到项目的 resources 中,并用新模板引擎的方式进行重写。 - 传入参数的转换接口:
ArgsResolver
,如现有传入模板参数的转换方案不能满足需求,可对其进行替换,自定义实现类,对ArgsResolver
进行实现,并注册成组件。
配置相关问题
问题1:如何在控制台中输出 nGQL
logging:
level:
org.nebula.contrib: DEBUG
问题2:如何指定 mapper xml 的放置路径:
cql:
parser:
# 更换开发者自定义的 xml 所在位置
mapper-locations: ng-mapper/**/*.xml # 默认为 mapper/**/*.xml
报错类
问题1:StackOverflowError
-
场景1
- 报错时机:模板引擎生成语法树时产生报错
- 解决方式:指定虚拟机启动参数 -Xss2m
-
场景2
- 报错时机:参数传入数据库时,会经历一个类型转换的过程,将Java对象转换成nebula-java的可接受的
Value
类型,当Java对象的值存在循环依赖时,即a对象是b对象的属性值之一,同时b对象也是a对象的属性值之一,会导致递归过深问题。当前最新版本v1.2.2还未在类型转换时,做空对象缓存来规避循环问题。 - 解决方式1:改变参数类的结构;
- 解决方式2:对不需要写入数据库的属性添加
@Transient
注解。
- 报错时机:参数传入数据库时,会经历一个类型转换的过程,将Java对象转换成nebula-java的可接受的
优化类问题
第一种情况,查询语句组装耗时
在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
关于多个图空间的使用问题
当项目需要跨越多个图空间进行数据读写时,有以下几种方式可以实现跨空间:
- 在实体类中追加
@Space("myspace")
指定 tag 所属空间 - 在xml中的
- 在接口方法的标签中指定 space,如:
以上三种方式可以按需选择。
其中第三种方式还支持使用参数的方式,如:<select id="spaceFromParam" space="${paramMySpace}" spaceFromParam="true"> RETURN true; </select>
paramMySpace
通过接口的参数传入。据我所知,有开发者在此用法的基础上实现了多租户场景的适配。
总结
目前将 NgBatis 集成到项目中出现得比较多的问题大体上都列在上面了,可能有一些问题的表象会跟列举的内容不太相同,但可以通过类比跟对应问题点附带的参考文档来完成。如果你在开发中碰到一些比较典型或者比较反直觉的问题,也可以留言讨论。