Mybatis去xml化:我再也不想写xml了

某一天当我因为某个功能需要又一次创建一个很简单的数据库表,然后再为它写增删改查的操作时,我终于忍受不了了。对于写代码这件事,我一贯的原则是少写代码,少写重复代码,而这些大同小异的增删改查的xml配置,对我来说就是无脑重复的体力活。这是我无法接受的。

想想当初使用Spring Data JPA 的时候, 只需要声明一个接口, 增删改查的方法立马就有了,而且对于一些简单的查询,通过特定格式的方法名字,声明一个接口方法就能完成。但是JPA是基于hibernate,效率低而且很不灵活,所以大部分企业的ORM框架选择的是MyBatis,所以JPA老早也被我抛弃了。

那么我能不能在MyBatis之上构建一个类似Spring Data JPA的项目来完成像JPA一样的功能呢?既能够拥有JPA式的简单,又能保持Mybatis的灵活高效。一开始的想法是基于Spring Data JPA的源码修改的,但是看了JPA源码之后我放弃了这个想法,代码太多了。后来偶然接触到Mybatis Plus这个项目,读了它的文档之后,突然有了思路,决定开始动手,基于Mybatis Plus来实现。

项目的功能特点:

  • 支持根据DAO的方法名称自动推断添加、查询、修改、删除、统计、是否存在等数据库操作
  • 支持多种形式的表达,如findById,queryById,selectById是等价的,deleteById与removeById是等价的
  • 支持根据对象结构自动解析resultMap(支持级联的对象),不再需要在xml文件中配置resultMap
  • 支持join的推断,复杂的sql也能自动推断
  • 支持分页操作,支持spring data的Pageable对象分页和排序
  • 支持spring data的Pageable和Page对象,基本可以和jpa做到无缝切换
  • 支持部分jpa注解:@Table、@Transient、@Id、@GeneratedValue,作用于持久化对象
  • 支持自增主键回填,需要在主键属性上添加jpa注解@GeneratedValue

设计思路

使用MyBatis Plus的Sql注入器

一切从这里开始:

override fun getMethodList(): List<AbstractMethod> {
    return listOf(
        UnknownMethods()
    )
}
复制代码

这里只注入了一个Method,按照Mybatis Plus的设计思路,一个method只负责一个特定名称方法的sql注入,但是通过阅读AbstractMethod的代码了解到,实际是在一个Method中可以注入任意多的sql声明,见如下代码:

/**
 * 添加 MappedStatement 到 Mybatis 容器
 */
protected MappedStatement addMappedStatement(Class<?> mapperClass,      String id, SqlSource sqlSource,
    SqlCommandType sqlCommandType, Class<?> parameterClass,
    String resultMap, Class<?> resultType, 
    KeyGenerator keyGenerator, String keyProperty, String keyColumn) {
    ...
}
复制代码

有了这个方法,你可以注入任意的sql声明。

再回头看上面,我只注入了一个UnknownMethods的注入方法,这里本项目所有功能的入口。这个类的代码也不多,我直接放上来

override fun injectMappedStatement(mapperClass: Class<*>, modelClass: Class<*>, tableInfo: TableInfo): MappedStatement {
    // 修正表信息,主要是针对一些JPA注解的支持以及本项目中自定义的一些注解的支持,
    MappingResolver.fixTableInfo(modelClass, tableInfo)
    // 判断Mapper方法是否已经定义了sql声明,如果没有定义才进行注入,这样如果存在Mapper方法在xml文件中有定义则会优先使用,如果没有定义才会进行推断
    val statementNames = this.configuration.mappedStatementNames
    val unmappedFunctions = mapperClass.kotlin.declaredFunctions.filter {
      (mapperClass.name + DOT + it.name) !in statementNames
    }
    // 解析未定义的方法,进行sql推断
    val resolvedQueries = ResolvedQueries(mapperClass, unmappedFunctions)
    unmappedFunctions.forEach { function ->
      val resolvedQuery: ResolvedQuery = QueryResolver.resolve(function, tableInfo, modelClass, mapperClass)
      resolvedQueries.add(resolvedQuery)
      // query为null则表明推断失败,resolvedQuery中将包含推断失败的原因,会在后面进行统一输出,方便开发人员了解sql推断的具体结果和失败的具体原因
      if (resolvedQuery.query != null && resolvedQuery.sql != null) {
        val sql = resolvedQuery.sql
        try {
          val sqlSource = languageDriver.createSqlSource(configuration, sql, modelClass)
          when (resolvedQuery.type()) {
            in listOf(QueryType.Select,
                QueryType.Exists,
                QueryType.Count) -> {
              val returnType = resolvedQuery.returnType
              var resultMap = resolvedQuery.resultMap
              if (resultMap == null && resolvedQuery.type() == QueryType.Select) {
                // 如果没有指定resultMap,则自动生成resultMap
                val resultMapId = mapperClass.name + StringPool.DOT + function.name
                resultMap = resolvedQuery.resolveResultMap(resultMapId, this.builderAssistant,
                    modelClass, resolvedQuery.query.mappings)
              }
              // addSelectMappedStatement这个方法中会使用默认的resultMap,该resultMap映射的类型和modelClass一致,所以如果当前方法的返回值和modelClass
              // 不一致时,不能使用该方法,否则会产生类型转换错误
              if (returnType == modelClass && resultMap == null) {
                addSelectMappedStatement(mapperClass, function.name, sqlSource, returnType, tableInfo)
              } else {
                addMappedStatement(mapperClass, function.name,
                    sqlSource, SqlCommandType.SELECT, null, resultMap, returnType,
                    NoKeyGenerator(), null, null)
              }
              // 为select查询自动生成count的statement,用于分页时查询总数
              if (resolvedQuery.type() == QueryType.Select) {
                addSelectMappedStatement(mapperClass, function.name + COUNT_STATEMENT_SUFFIX,
                    languageDriver.createSqlSource(configuration, resolvedQuery.countSql(), modelClass),
                    Long::class.java, tableInfo
                )
              }
            }
            QueryType.Delete     -> {
              addDeleteMappedStatement(mapperClass, function.name, sqlSource)
            }
            QueryType
  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值