某一天當我由於某個功能須要又一次建立一個很簡單的數據庫表,而後再爲它寫增刪改查的操做時,我終於忍受不了了。對於寫代碼這件事,我一向的原則是少寫代碼,少寫重複代碼,而這些大同小異的增刪改查的xml配置,對我來講就是無腦重複的體力活。這是我沒法接受的。java
想一想當初使用Spring Data JPA 的時候, 只須要聲明一個接口, 增刪改查的方法立馬就有了,並且對於一些簡單的查詢,經過特定格式的方法名字,聲明一個接口方法就能完成。可是JPA是基於hibernate,效率低並且很不靈活,因此大部分企業的ORM框架選擇的是MyBatis,因此JPA老早也被我拋棄了。git
那麼我能不能在MyBatis之上構建一個相似Spring Data JPA的項目來完成像JPA同樣的功能呢?既可以擁有JPA式的簡單,又能保持Mybatis的靈活高效。一開始的想法是基於Spring Data JPA的源碼修改的,可是看了JPA源碼以後我放棄了這個想法,代碼太多了。後來偶然接觸到Mybatis Plus這個項目,讀了它的文檔以後,忽然有了思路,決定開始動手,基於Mybatis Plus來實現。github
一切從這裏開始:spring
override fun getMethodList(): List<AbstractMethod> {
return listOf(
UnknownMethods()
)
}
複製代碼
這裏只注入了一個Method,按照Mybatis Plus的設計思路,一個method只負責一個特定名稱方法的sql注入,可是經過閱讀AbstractMethod的代碼瞭解到,實際是在一個Method中能夠注入任意多的sql聲明,見以下代碼: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的注入方法,這裏本項目全部功能的入口。這個類的代碼也很少,我直接放上來bash
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所需的信息,全部仍須要一些額外的信息輔助,這些信息基本上都來自於註解。mybatis
@Handler 註解在持久化類的屬性上,代表該屬性須要進行類型轉換,註解的value值是mybatis的typeHandler類app
@InsertIgnore 註解在持久化類的屬性上,代表該屬性不參與數據庫插入操做框架
@UpdateIgnore 註解在持久化類的屬性上,代表該屬性不參與數據庫更新操做
@SelectIgnore 註解在持久化類的屬性上,代表該屬性不參與數據庫查詢操做
@JoinObject 代表該屬性是一個關聯的複雜對象,該對象的內容來自於關聯的另外一張數據庫表
@JoinProperty 代表該屬性是一個關聯屬性,屬性內容來自於某個關聯表的字段
@ModifyIgnore 註解在持久化類的屬性上,代表該屬性不參與數據庫更新和查詢操做
@ResolvedName 註解在Mapper接口的方法上,表示sql推斷使用註解指定的名稱而不是方法名稱,這樣能夠不用爲了sql推斷而更改方法名,使方法名更具邏輯化
@SelectedProperties 註解在Mapper接口的方法上,代表sql查詢、插入、或更新所使用的持久化對象的屬性集合
@ValueAssign 用於在@ResolvedName指定某個條件使用特定值
有了以上註解的信息,結合方法名稱的推斷,能夠完成百分之八十以上的數據庫操做的自動推斷,在簡單的應用場景下,能夠一個xml文件都不寫就能完成數據庫操做,並且後面要加入xml配置也徹底不受影響。
<distributionManagement>
<repository>
<id>nexus</id>
<url>http://nexus.aegis-info.com/repository/maven-releases/</url>
</repository>
</distributionManagement>
複製代碼
<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達到最簡化
讓@Mapper註解的DAO接口繼承 XmlLessMapper 接口便可實現DAO的sql推斷
XmlLessMapper接口接收一個泛型參數,即該DAO要操做的對象,全部的sql推斷都是基於該對象的
XmlLessMapper接口沒有任何默認的方法,不會影響原有代碼
原來使用mybatis-plus的方法注入須要繼承BaseMapper接口,但BaseMapper接口有不少方法,可能大部分方法都是不須要的,因此我改寫了這個邏輯,一個默認的方法也不添加,讓開發自行添加DAO所須要的方法,
表名稱支持jpa註解__@Table__,原mybatis-plus的@TableName註解仍然有效,但@Table註解的優先級更高
主鍵屬性支持jpa註解__@Id__
解析爲
SELECT * FROM table WHERE id = #{id}
複製代碼
解析爲
SELECT * FROM table WHERE name = #{name}
複製代碼
解析爲
SELECT * FROM table WHERE name LIKE CONCAT('%',#{name}, '%')
複製代碼
解析爲
SELECT * FROM table WHERE name LIKE CONCAT('%',#{keyword}, '%')
複製代碼
解析爲
SELECT * FROM table WHERE name = #{name} AND id = #{id}
複製代碼
解析爲
SELECT id, name FROM table WHERE age = #{age}
複製代碼
在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"]) 來代替
支持 deleteAll deleteById deleteByName的寫法
支持 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}
複製代碼
支持批量插入
在持久化對象中能夠關聯另一個對象,這個對象對應數據庫中的另一張表,那麼在查詢的時候若是須要級聯查詢能夠這樣配置:
在關聯的對象(支持單個對象或對象集合,即一對一或一對多的關係均可以支持)屬性上添加註解:
@JoinObject(
targetTable = "t_score",
targetColumn = "student_id",
joinProperty = "id",
associationPrefix = "score_",
selectColumns = ["score", "subject_id"]
)
複製代碼
註解中的屬性做用以下: targetTable 須要join的表 targetColumn join的表中用於關聯的列名稱 joinProperty 當前對象中用於關聯的屬性名稱(注意是對象屬性名稱而不是列名稱) associationPrefix 爲防止列名稱衝突,給關聯表的屬性別名添加固定前綴 selectColumns 關聯表中須要查詢的列集合
項目提供了對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
}
複製代碼
@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…
歡迎你們使用並提出問題和建議