内容简介:某一天当我因为某个功能需要又一次创建一个很简单的数据库表,然后再为它写增删改查的操作时,我终于忍受不了了。对于写代码这件事,我一贯的原则是少写代码,少写重复代码,而这些大同小异的增删改查的xml配置,对我来说就是无脑重复的体力活。这是我无法接受的。想想当初使用Spring Data JPA 的时候, 只需要声明一个接口, 增删改查的方法立马就有了,而且对于一些简单的查询,通过特定格式的方法名字,声明一个接口方法就能完成。但是JPA是基于hibernate,效率低而且很不灵活,所以大部分企业的ORM框架选择
某一天当我因为某个功能需要又一次创建一个很简单的数据库表,然后再为它写增删改查的操作时,我终于忍受不了了。对于写代码这件事,我一贯的原则是少写代码,少写重复代码,而这些大同小异的增删改查的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.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仓库
<distributionManagement>
<repository>
<id>nexus</id>
<url>http://nexus.aegis-info.com/repository/maven-releases/</url>
</repository>
</distributionManagement>
复制代码
第二步:在pom中引用依赖
<dependency>
<groupId>com.aegis</groupId>
<artifactId>aegis-starter-mybatis</artifactId>
<version>${mybatis-starter.version}</version>
</dependency>
复制代码
配置说明
本项目的引入使用无需任何配置(当然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<User>
复制代码
将使用 findIdAndNameAndAge 推断sql,推断的结果为:
SELECT id,name,age FROM user 复制代码
指定方法获取的属性集合
使用 @SelectedProperties注解
例如
@SelectedProperties(properties=["id", "name", "age"]) fun findSimpleInfoList(): List<User> 复制代码
上一个示例中的 @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<Score>? = 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
@Mapper
interface UserDAO : XmlLessMapper<User> {
fun deleteById(id: Int)
@SelectedProperties(["name"])
fun findAllNames(): List<String>
fun findById(id: Int): User?
@ResolvedName("findById")
fun findSimpleUserById(id: Int): UserSimple
fun save(user: User)
fun saveAll(user: List<User>)
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地址: github.com/wuhao000/my…
欢迎大家使用并提出问题和建议
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
Hit Refresh
Satya Nadella、Greg Shaw / HarperBusiness / 2017-9-26 / USD 20.37
Hit Refresh is about individual change, about the transformation happening inside of Microsoft and the technology that will soon impact all of our lives—the arrival of the most exciting and disruptive......一起来看看 《Hit Refresh》 这本书的介绍吧!