logo
企业版

技术分享

NgBatis丨开发常见问题

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-boot3.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 或者对象的属性名。否则默认参数名按所处方法的参数位置,取 p0p1

  • 为了代码更具可读性,可在参数名中,对参数追加注解:@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,如: