NgBatis丨开发常见问题

项目集成类

问题1:与存在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-locationscql.parser.mapper-location所指向的路径不存在交叉。设计之初有不当之处,默认值状态下,二者路径重叠。两个框架的解析逻辑与约束不同,如果NgBatis通过cql.parser.mapper-locations指定的路径,扫描到MyBatis的xml,可能导致解析出错,反之亦然。另外,cql.parser.mapper-locations所指向的路径至少需要包含一个 xml 文件,这一点会考虑在后续的版本优化。

问题2:与spring-boot3.x 集成

在主分支中,保持的版本是与spring-boot2.x 的集成,如果需要与spring-boot3.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 或者对象的属性名。否则默认参数名按所处方法的参数位置,取 p0p1
  • 为了代码更具可读性,可在参数名中,对参数追加注解:@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:还有哪些接口可以拓展:

  1. 主键生成接口:PkGenerator,可根据入参获得vid的类型,然后选择使用雪花算法或者UUID等方式,完成主键生成策略的指定;
  2. 模板引擎接口:TextResolver,当模板引擎的实现方式发生替换,需要将 NebulaDaoBasic.xml 迁移到项目的 resources 中,并用新模板引擎的方式进行重写。
  3. 传入参数的转换接口: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注解。

优化类问题

第一种情况,查询语句组装耗时

在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 集成到项目中出现得比较多的问题大体上都列在上面了,可能有一些问题的表象会跟列举的内容不太相同,但可以通过类比跟对应问题点附带的参考文档来完成。如果你在开发中碰到一些比较典型或者比较反直觉的问题,也可以留言讨论。

2 个赞