博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
Mybatis去xml化:我再也不想写xml了
阅读量:5832 次
发布时间:2019-06-18

本文共 14503 字,大约阅读时间需要 48 分钟。

某一天当我因为某个功能需要又一次创建一个很简单的数据库表,然后再为它写增删改查的操作时,我终于忍受不了了。对于写代码这件事,我一贯的原则是少写代码,少写重复代码,而这些大同小异的增删改查的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
{ 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.Insert     -> {              // 如果id类型为自增,则将自增的id回填到插入的对象中              val keyGenerator = when {                tableInfo.idType == IdType.AUTO -> Jdbc3KeyGenerator.INSTANCE                else                            -> NoKeyGenerator.INSTANCE              }              addInsertMappedStatement(                  mapperClass, modelClass, function.name, sqlSource,                  keyGenerator, tableInfo.keyProperty, tableInfo.keyColumn              )            }            QueryType.Update     -> {              addUpdateMappedStatement(mapperClass, modelClass, function.name, sqlSource)            }            else                 -> {            }          }        } catch (ex: Exception) {          LOG.error("""出错了 >>>>>>>>              可能存在下列情形之一:              ${possibleErrors.joinToString { String.format("\n\t\t-\t%s\n", it) }}              """.trimIndent(), ex)        }      }    }    resolvedQueries.log()    // 其实这里的return是没有必要的,mybatis plus也没有对这个返回值做任何的处理,    // 所里这里随便返回了一个sql声明    return addSelectMappedStatement(mapperClass,        "unknown",        languageDriver.createSqlSource(configuration, "select 1", modelClass),        modelClass, tableInfo    )  }复制代码

具体对于方法名称的解析,代码比较多,这里也无法一一放上来给大家讲解,所以只讲一下思路,方法名称并不能包含所有构建sql所需的信息,所有仍需要一些额外的信息辅助,这些信息基本上都来自于注解。

提供元信息的注解说明

@Handler 注解在持久化类的属性上,表明该属性需要进行类型转换,注解的value值是mybatis的typeHandler类

@InsertIgnore 注解在持久化类的属性上,表明该属性不参与数据库插入操作

@UpdateIgnore 注解在持久化类的属性上,表明该属性不参与数据库更新操作

@SelectIgnore 注解在持久化类的属性上,表明该属性不参与数据库查询操作

@JoinObject 表明该属性是一个关联的复杂对象,该对象的内容来自于关联的另一张数据库表

@JoinProperty 表明该属性是一个关联属性,属性内容来自于某个关联表的字段

@ModifyIgnore 注解在持久化类的属性上,表明该属性不参与数据库更新和查询操作

@ResolvedName 注解在Mapper接口的方法上,表示sql推断使用注解指定的名称而不是方法名称,这样可以不用为了sql推断而更改方法名,使方法名更具逻辑化

@SelectedProperties 注解在Mapper接口的方法上,表明sql查询、插入、或更新所使用的持久化对象的属性集合

@ValueAssign 用于在@ResolvedName指定某个条件使用特定值

有了以上注解的信息,结合方法名称的推断,可以完成百分之八十以上的数据库操作的自动推断,在简单的应用场景下,可以一个xml文件都不写就能完成数据库操作,而且后面要加入xml配置也完全不受影响。

使用方法

第一步: 添加maven仓库

nexus
http://nexus.aegis-info.com/repository/maven-releases/
复制代码

第二步:在pom中引用依赖

com.aegis
aegis-starter-mybatis
${mybatis-starter.version}
复制代码

配置说明

本项目的引入使用无需任何配置(当然mybatis的配置是必要的)即可使用

@Mapper注解的DAO接口是否需要sql推断是__可选__的,且mapper的xml文件的配置是具有更高优先级的,如果一个方法在xml中存在配置,则sql推断自动失效

本插件的使用可以是渐进式的,一开始在项目中使用本插件对原项目没有任何影响,可以先尝试删除一些方法的xml配置,让其使用sql推断,如果能够正常工作,则可继续去除xml,直到xml达到最简化

启用sql推断

让@Mapper注解的DAO接口继承 XmlLessMapper 接口即可实现DAO的sql推断

XmlLessMapper接口接收一个泛型参数,即该DAO要操作的对象,所有的sql推断都是基于该对象的

XmlLessMapper接口没有任何默认的方法,不会影响原有代码

原来使用mybatis-plus的方法注入需要继承BaseMapper接口,但BaseMapper接口有很多方法,可能大部分方法都是不需要的,所以我改写了这个逻辑,一个默认的方法也不添加,让开发自行添加DAO所需要的方法,

功能增强说明

表名称支持jpa注解__@Table__,原mybatis-plus的@TableName注解仍然有效,但@Table注解的优先级更高

主键属性支持jpa注解__@Id__

sql推断说明

select查询推断

  • 从方法名称中推断的字段名称均为mapper关联数据对象的属性名称,而非数据库中的表字段名称

例1 findById

解析为

SELECT * FROM table WHERE id = #{
id}复制代码

例2 findByName

解析为

SELECT * FROM table WHERE name = #{
name}复制代码

例3 findByNameLike

解析为

SELECT * FROM table WHERE name LIKE CONCAT('%',#{
name}, '%')复制代码

例4 findByNameLikeKeyword

解析为

SELECT * FROM table WHERE name LIKE CONCAT('%',#{keyword}, '%')复制代码

例5 findByNameEqAndId

解析为

SELECT * FROM table WHERE name = #{
name} AND id = #{
id}复制代码

例6 findIdAndNameByAge

解析为

SELECT id, name FROM table WHERE age = #{age}复制代码

sql推断名称与方法名称隔离

在mapper方法上使用@ResolvedName注解,该注解的必选参数name将会代替方法名称作为推断sql的名称,这样可以让方法名称更具语义化

例如

@ResolvedName("findIdAndNameAndAge")fun findSimpleInfoList(): List
复制代码

将使用 findIdAndNameAndAge 推断sql,推断的结果为:

SELECT id,name,age FROM user复制代码

指定方法获取的属性集合

使用 @SelectedProperties注解

例如

@SelectedProperties(properties=["id", "name", "age"])fun findSimpleInfoList(): List
复制代码

上一个示例中的 @ResolvedName("findIdAndNameAndAge") 便可以用 @SelectedProperties(properties=["id", "name", "age"]) 来代替

  • 注:使用@SelectedProperties注解之后,从方法名中推断的查询属性将被忽略

delete操作推断

支持 deleteAll deleteById deleteByName的写法

update操作推断

支持 update 一个对象或 update某个字段

为了防止出现数据更新错误,update操作必须指定对象的主键属性

例1:

fun update(user: User): Int复制代码

最终解析为:

UPDATE   user SET     user.name = #{
name}, user.password = #{
password}, user.email = #{email}WHERE id = #{
id}复制代码

例2:

fun updateNameById(name:String,id:Int): Int复制代码
UPDATE   user SET     user.name = #{
name} WHERE id = #{
id}复制代码

支持 Insert 操作

支持批量插入

join的支持

join 一个对象

在持久化对象中可以关联另外一个对象,这个对象对应数据库中的另外一张表,那么在查询的时候如果需要级联查询可以这样配置:

在关联的对象(支持单个对象或对象集合,即一对一或一对多的关系都可以支持)属性上添加注解:

@JoinObject(      targetTable = "t_score",      targetColumn = "student_id",      joinProperty = "id",      associationPrefix = "score_",      selectColumns = ["score", "subject_id"]  )复制代码

注解中的属性作用如下: targetTable 需要join的表 targetColumn join的表中用于关联的列名称 joinProperty 当前对象中用于关联的属性名称(注意是对象属性名称而不是列名称) associationPrefix 为防止列名称冲突,给关联表的属性别名添加固定前缀 selectColumns 关联表中需要查询的列集合

  • 注:如果关联的是对象集合,在kotlin中必须声明为可变的集合

Spring Data的支持

项目提供了对Spring Data的一些支持,兼容spring data的Pageable对象作为参数进行分页和排序,并支持Page对象作为返回接受分页的数据和数据总数。

测试

建立数据表

CREATE TABLE t_student(  id           VARCHAR(20) NOT NULL,  name         VARCHAR(20) NOT NULL,  phone_number VARCHAR(20) NOT NULL,  sex          INT         NOT NULL,  CONSTRAINT t_student_id_uindex    UNIQUE (id));ALTER TABLE t_student  ADD PRIMARY KEY (id);CREATE TABLE t_score(  id         INT AUTO_INCREMENT    PRIMARY KEY,  score      INT         NOT NULL,  student_id VARCHAR(20) NOT NULL,  subject_id INT         NOT NULL);CREATE TABLE t_subject(  id   INT AUTO_INCREMENT    PRIMARY KEY,  name VARCHAR(20) NOT NULL,  CONSTRAINT t_subject_name_uindex    UNIQUE (name));复制代码

创建数据对象

/** * * @author 吴昊 * @since 0.0.4 */class Student() {  @TableField("sex")  var gender: Int = 1  @Id  var id: String = ""  var name: String = ""  var phoneNumber: String = ""  @JoinObject(      targetTable = "t_score",      targetColumn = "student_id",      joinProperty = "id",      associationPrefix = "score_",      selectColumns = ["score", "subject_id"]  )  @ModifyIgnore  var scores: MutableList
? = null constructor(id: String, name: String, phoneNumber: String, gender: Int) : this() { this.id = id this.name = name this.phoneNumber = phoneNumber this.gender = gender }}class Score { var score: Int = 0 var studentId: String = "" var subjectId: Int = 0}复制代码

创建DAO

@Mapperinterface UserDAO : XmlLessMapper
{ fun deleteById(id: Int) @SelectedProperties(["name"]) fun findAllNames(): List
fun findById(id: Int): User? @ResolvedName("findById") fun findSimpleUserById(id: Int): UserSimple fun save(user: User) fun saveAll(user: List
) fun update(user: User) fun count(): Int}复制代码

编写测试类

class StudentDAOTest : BaseTest() {  val id = "061251170"  @Autowired  private lateinit var studentDAO: StudentDAO  @Test  fun count() {    assert(studentDAO.count() > 0)  }  @Test  fun delete() {    val id = "061251171"    studentDAO.save(Student(        id,        "wuhao",        "18005184916", 1    ))    assert(studentDAO.existsById(id))    studentDAO.deleteById(id)    assert(!studentDAO.existsById(id))  }  @Test  fun deleteByName() {    val id = "testDeleteByName"    val name = "nameOfTestDeleteByName"    studentDAO.save(        Student(            id,            name,            "18005184916", 1        )    )    assert(studentDAO.existsByName(name))    studentDAO.deleteByName(name)    assert(!studentDAO.existsByName(name))  }  @Test  fun existsByClientId() {    val id = "1234"    assert(!studentDAO.existsById(id))  }  @Test  fun findAll() {    val list = studentDAO.findAll()    val spec = list.first { it.id == id }    assert(spec.scores != null && spec.scores!!.isNotEmpty())    assert(list.isNotEmpty())  }  @Test  fun findById() {    val student = studentDAO.findById(id)    println(student?.scores)    assert(studentDAO.findById(id) != null)  }  @Test  fun findPage() {    studentDAO.findAllPageable(        PageRequest.of(0, 20)).apply {      this.content.map {        it.name + " / ${it.id}"      }.forEach { println(it) }      println(this.content.first().name.compareTo(this.content.last().name))    }    studentDAO.findAllPageable(        PageRequest.of(0, 20, Sort(Sort.Direction.DESC, "name"))).apply {      this.content.map {        it.name + " / ${it.id}"      }.forEach { println(it) }      println(this.content.first().name.compareTo(this.content.last().name))    }    studentDAO.findAllPageable(        PageRequest.of(0, 20, Sort.by("name"))).apply {      this.content.map {        it.name + " / ${it.id}"      }.forEach { println(it) }      println(this.content.first().name.compareTo(this.content.last().name))    }  }  @Test  fun save() {    studentDAO.deleteById(id)    assert(!studentDAO.existsById(id))    studentDAO.save(Student(        id,        "wuhao",        "18005184916", 1    ))    assert(studentDAO.existsById(id))  }  @Test  fun saveAll() {    val id1 = "saveAll1"    val id2 = "saveAll2"    studentDAO.saveAll(        listOf(            Student(id1,                "zs", "123", 1),            Student(id2,                "zs", "123", 1)        )    )    assert(studentDAO.existsById(id1))    assert(studentDAO.existsById(id2))    studentDAO.deleteByIds(listOf("saveAll1", "saveAll2"))    assert(!studentDAO.existsById(id1))    assert(!studentDAO.existsById(id2))  }  @Test  fun selectPage() {    val page = studentDAO.findAllPage(PageRequest.of(0, 20))    println(page.content.size)    println(page.totalElements)  }  @Test  fun update() {    assert(        studentDAO.update(            Student(                "061251170", "zhangsan",                "17712345678",                9            )        ) == 1    )  }  @Test  fun updateNameById() {    val id = "testUpdateNameById"    val oldName = "oldName"    val newName = "newName"    studentDAO.save(        Student(            id,            oldName,            "18005184916", 1        )    )    assert(studentDAO.findById(id)?.name == oldName)    assert(studentDAO.updateNameById(newName, id) == 1)    assert(studentDAO.findById(id)?.name == newName)    studentDAO.deleteById(id)  }}复制代码

测试结果

写在最后

项目写的比较仓促,大概花了一周的时间,代码质量会在后期进行一些优化和完善,但是目前我想要完成的功能基本上都已经完成了。

项目的Github地址

欢迎大家使用并提出问题和建议

转载地址:http://lcrdx.baihongyu.com/

你可能感兴趣的文章
linux命令:ls
查看>>
Using RequireJS in AngularJS Applications
查看>>
hdu 2444(二分图最大匹配)
查看>>
【SAP HANA】关于SAP HANA中带层次结构的计算视图Cacultation View创建、激活状况下在系统中生成对象的研究...
查看>>
DevOps 前世今生 | mPaaS 线上直播 CodeHub #1 回顾
查看>>
iOS 解决UITabelView刷新闪动
查看>>
CentOS 7 装vim遇到的问题和解决方法
查看>>
JavaScript基础教程1-20160612
查看>>
【ros】Create a ROS package:package dependencies报错
查看>>
通过容器编排和服务网格来改进Java微服务的可测性
查看>>
re:Invent解读:没想到你是这样的AWS
查看>>
PyTips 0x02 - Python 中的函数式编程
查看>>
使用《Deep Image Prior》来做图像复原
查看>>
Linux基础命令---rmdir
查看>>
Squid 反向代理服务器配置
查看>>
Java I/O操作
查看>>
Tomcat性能调优
查看>>
Android自学--一篇文章基本掌握所有的常用View组件
查看>>
灰度图像和彩色图像
查看>>
FreeMarker-Built-ins for strings
查看>>