最近我在做一個新項目,由于我們項目組一直使用的是 MongoDB 數(shù)據(jù)庫,所以新項目我就打算上 Spring Data MongoDB 嘗試一下,雖然我早就用過了 Spring Data JPA,對 Spring Data 的相關(guān) CRUD 和 動態(tài)查詢的封裝也比較熟悉,但是自帶的封裝顯然不能很好的滿足我們的需求,本篇帶大家講述我所遇到的問題以及解決方案。
注: MongoRepository / JPARepository 都繼承自
PagingAndSortingRepository,除了對應(yīng)的數(shù)據(jù)庫不同之外,功能都基本相同,所以本文的二次封裝也可以用于 JPARepository 上。
1. 我遇到的問題
問題一
在 Spring Data 中可以通過繼承 MongoRepository / JPARepository 接口的方式獲得 CRUD 和 分頁的能力,但是這種能力也僅僅滿足基礎(chǔ)的 CRUD 操作和 分頁,對于極其常用的兩個操作比如:針對數(shù)據(jù)庫某個字段進行更新 和 多條件查詢,這個接口并沒有提供。
準(zhǔn)確的來說,多條件查詢的能力是提供了,但是非常不宜用,它必須使用你的類做為查詢條件,這個類的變量名還必須和數(shù)據(jù)庫表中的字段名保持一致,這可以非常簡單的讓我們想到使用 PO 類當(dāng)作這個查詢條件。
但是在有些規(guī)范中,PO 類應(yīng)該是一個擁有全參構(gòu)造器的不可變類,這使得先創(chuàng)建這個類然后對應(yīng)的查詢字段進行賦值的操作變得不可行,這里我舉一個簡單的例子,我擁有一個數(shù)據(jù)表的映射對象:User,這就是俗稱的 PO。
@Document("user")
class User (
@Id
val id : String,
?
val account : String,
?
val pwd : String,
?
val name : String,
)
復(fù)制代碼
然后我如果想要單獨更新 name 這個字段時,我需要擁有整個 User 對象中的所有屬性,因為 Repository 接口所提供的能力是把新增操作和更新操作放在一起的 (save 方法),每次更新都是所有字段的更新,這是我不愿意看到的,也是極其麻煩的。
接著就是多條件查詢的問題,我們先來看下如果我想要使用多條件查詢,它的參數(shù)是什么:
可以明顯看到是一個叫 Example 的對象,如果我想使用,它應(yīng)該是這樣的:
fun test() {
val user = cssUser()
user.name = "我要查詢的參數(shù)具體值"
?
userRepository.findAll(Example.of(user))
}
復(fù)制代碼
這里我定義了一個 CssUser 去當(dāng)它的查詢條件的類,而且這個類和 User 類的內(nèi)容幾乎一樣,因為我的 User 類是一個全參構(gòu)造器沒辦法直接創(chuàng)建一個空對象進行賦值,所以我不得不創(chuàng)建一個 CssUser 去當(dāng)查詢條件的類,對于程序員來講,這很煩。
我想要的效果是什么樣的呢?是這樣的:
fun test() {
?
userRepository.listAll(Criteria
.where("account").`is`("admin")
.and("name").`is`("你的名字")
)
?
}
復(fù)制代碼
通過 lambda 的方式直接獲取到某個屬性的名字,然后作為查詢變量,然后跟著鏈?zhǔn)秸{(diào)用可以隨便在里面加上各樣的查詢條件,例子中的 Criteria 類是 Spring 已經(jīng)為我們做好的,但是 Repository 接口并沒有提供它,所以我們需要一層封裝。
問題二
從上面的例子中我們可以看到在組裝查詢條件時,需要硬編碼進去字段名,這對于程序員來說,是很煩的。
所以我們應(yīng)該使用 lambda 的特性,幫助我們?nèi)カ@取某一個類的字段名,通常是 PO,因為它和數(shù)據(jù)庫屬性是一一對應(yīng)的,整體要達到的有點像 MyBatis-PLus 的效果,大概是這樣:
fun test() {
?
userRepository.listAll(Criteria
.where(CssUser::account.mongoFiled()).`is`("admin")
.and(CssUser::name.mongoFiled()).`is`("你的名字")
)
?
}
復(fù)制代碼
當(dāng)然我的這個效果還沒有 Mybatis-PLus 的效果好,它可以直接省略 .mongoFiled() 這個操作,這是因為我只加了三四行代碼就能達到這個效果,對我而言夠用了,而 Mybatis-PLus 則是有一套相關(guān)支持。
雖然我這是 Kotlin 示例,但隨后也會給出 JAVA 語法中的相關(guān)思路。
2. Repository 接口封裝
先來談?wù)剬?CRUD 的增強,正常情況下,我們只需要使用一個接口繼承 MongoRepository 接口,然后 Spring Data 就會幫我們生成一個動態(tài)代理類,并聲明為 Bean,直接注入就可以使用了,就像這樣(代碼中的 :語法是繼承的意思):
interface UserMongoRepository : MongoRepository<User, String> {
?
}
復(fù)制代碼
現(xiàn)在既然我們要對 Repository 進行增強,就需要再抽象出一個類,作為我們新的基類,之后的自己的業(yè)務(wù)類需要繼承這個接口,而非原來的 MongoRepository 接口,當(dāng)然,我們這個新的基類接口還會去繼承 MongoRepository 接口,然后在接口中定義我們需要的新操作即可:
@NoRepositoryBean
interface BaseMongoRepository<T, ID> : MongoRepository<T, ID> {
?
fun listAll(condition: Criteria, pageable: Pageable): Page<T>
?
fun updateById(id: ID, update: Update): Long
}
復(fù)制代碼
我創(chuàng)建了一個新的接口:BaseMongoRepository,用它來繼承 MongoRepository,接著定義我們需要的擴展的一些方法,這里我擴展類了兩個方法:新的多條件分頁方法和新的更新接口。
其中 listAll 方法的第一個參數(shù) Criteria 是 Spring Data 已經(jīng)給我們提供好的類,它廣泛運用于 MongoTemplate 里面,畢竟這層 CRUD 的封裝底層其實還是 MongoTemplate 來操作。
除了繼承接口外,我們還需要對這兩個方法進行實現(xiàn),再創(chuàng)建一個 BaseMongoRepository 的實現(xiàn)類去繼承 MongoRepository 的實現(xiàn)類——SimpleMongoRepository:
class BaseMongoRepositoryClass<T, ID>(
private val metadata: MongoEntityInformation<T, ID>,
private val mongoOperations: MongoOperations
) :
SimpleMongoRepository<T, ID>(metadata, mongoOperations), BaseMongoRepository<T, ID> {
?
private val clazz: Class<T> = metadata.javaType
?
override fun listAll(condition: Criteria, pageable: Pageable): Page<T> {
val list = mongoOperations.find(Query(condition).with(pageable), this.clazz, metadata.collectionName)
?
return PageableExecutionUtils.getPage(list, pageable) {
mongoOperations
.count(
Query(condition).limit(-1).skip(-1),
clazz,
metadata.collectionName
)
}
}
?
override fun updateById(id: ID, update: Update): Long {
if (update.updateObject.isEmpty()) return 0
return mongoOperations.updateFirst(
Query().addCriteria(Criteria.where("_id").`is`(id)),
update,
metadata.collectionName
).modifiedCount
}
?
?
}
復(fù)制代碼
其中 BaseMongoRepositoryClass 需要兩個參數(shù),這兩個參數(shù)直接從 SimpleMongoRepository 里面拷貝過來然后通過構(gòu)造再傳遞給 SimpleMongoRepository 即可,反正都是從自動注入里面來。
兩個變量簡單講解一下都是什么意思:
- MongoEntityInformation:這個是 MongoEntity 的元信息,就是最上面用 @Document 注解標(biāo)記的 PO 類的元信息,我們可以通過它拿到 PO 類的類型和數(shù)據(jù)表的名字。
- MongoOperations:MongoTemplate 的實現(xiàn)類,這個我想不用多談。
接著就是方法實現(xiàn),方法實現(xiàn)就是就是通過 MongoTemplate 操作了這個這個方法要做什么事,代碼都比較簡單因為不包含什么邏輯,熟悉 MongoTemplate 的一眼就可看懂。
接下來就是最重要的一步,沒有這一步一切都是白費,還會造成項目啟動失敗,那就是把這個新的基類告訴 Spring,這是新的基類,你可以在項目的入口中加上這一句注解:
@EnableMongoRepositories(basePackages = ["com.xxx.*"], repositoryBaseClass = BaseMongoRepositoryClass::class)
class AdminApplication
?
fun main(args: Array<String>) {
runApplication<AdminApplication>(*args)
}
復(fù)制代碼
指定一下 repositoryBaseClass,這樣生成動態(tài)代理的時候會以這個類為基類,我們動態(tài)代理類也就具有了我們定義的兩個方法的能力了,使用中和原來的一樣,只不過繼承的接口不同罷了:
interface UserRepository : BaseMongoRepository<User, String> {
?
}
復(fù)制代碼
到這一步,我們可以完成這個效果:
fun test() {
?
userRepository.listAll(Criteria
.where("account").`is`("admin")
.and("name").`is`("你的名字")
)
?
}
復(fù)制代碼
3. 實體類變量進行 lambda 封裝
接下來是對實體變量進行 lambda 封裝,這個東西我覺得可以分為 Kotlin 和 Java 兩個版本來說,兩者各有千秋。
先來說說Kotlin,因為 Kotlin 自身的語言特性的關(guān)系,實現(xiàn)起來比較簡單,但也會拖一個尾巴,Kotlin 具有一個擴展函數(shù)的能力,簡單點說就是直接給某個類加上一些自定義方法,比如 String 我們可以在不繼承的情況下直接給 String 類加上一個新的方法,然后它就會出現(xiàn)在 String 對象可調(diào)用的函數(shù)列表中。
所以我們?nèi)绻胍?User::account.mongoFiled() 這種效果,就得先知道 User::account 返回值是什么,在 Kotlin 中,它的返回值是一個 KProperty 類對象,那么我們直接給這個類加上擴展如下:
fun KProperty<*>.mongoFiled(): String {
if (this.hasAnnotation<Id>()) return "_id"
return this.findAnnotation<Field>()?.run {
this.name.ifEmpty { this@mongoFiled.name }
} ?: this.name
}
復(fù)制代碼
這樣在 lambda 調(diào)用下就可以再調(diào)用這個方法了,接著來看看方法內(nèi)容。
- 首先判斷了是否存在 ID 注解,這個 ID 注解是用來標(biāo)識 Mongo 的主鍵屬性的注解,這種注解標(biāo)識的變量在數(shù)據(jù)庫中統(tǒng)一叫做 "_id",所以這里我也返回這個名字。
- 接著判斷是否存在 Field 注解,它是用來標(biāo)識數(shù)據(jù)庫字段和類變量不一樣的情況,如果出現(xiàn)這種情況,我們使用注解所標(biāo)識的字段名。
- 最后,以上兩種情況排除后,我們直接使用這個字段的名字。
這樣就可以達到如下效果了:
fun test() {
?
userRepository.listAll(Criteria
.where(CssUser::account.mongoFiled()).`is`("admin")
.and(CssUser::name.mongoFiled()).`is`("你的名字")
)
?
}
復(fù)制代碼
接著我們可以來說說 Java 的做法,首先也需要一個方法通過 lambda 拿到字段名,這個方法網(wǎng)上有很多我不再贅述,但是拿到之后該怎么辦呢?
你當(dāng)然可以直接通過工具類的靜態(tài)方法去拿,就像這樣:
fun test() {
?
userRepository.listAll(Criteria
.where(Util.getName(CssUser::account).`is`("admin")
.and(Util.getName(CssUser::name).`is`("你的名字")
)
?
}
復(fù)制代碼
可能到這一步看起來還是略微不雅,追求極致的小伙伴這個時候就可以再度發(fā)揮封裝的本色,將 Criteria 類封裝出一個新的查詢條件類,比如叫 Condition,然后將 Criteria 裝在里面再封裝一下查詢時的相關(guān)常用方法,就像這樣(注意此處的 Funtion 入?yún)⒅皇且粋€例子,實際應(yīng)該是泛型):
public class Condition {
private Criteria criteria = new Criteria();
?
public Condition where(Function<String, String> function, String value) {
criteria.andOperator(Criteria.where(Util.getName(function)).is(value));
return this;
}
}
復(fù)制代碼
除了 where 方法你還可以繼續(xù)封裝 gt、lt、or 等常用方法,并且它們還能形成鏈?zhǔn)秸{(diào)用,最終的效果是這樣的:
public static void main(String[] args) {
Criteria criteria = new Condition()
.where(CssUser::getName, "你的名字")
.where(CssUser::getAccount, "admin");
}
復(fù)制代碼
是不是更優(yōu)雅了呢?
4. 最后
今天是滿滿的技術(shù)干貨,希望 Get 到新技能的小伙伴可以積極的點贊,有什么問題都可以再評論區(qū)留言,我會積極對線的,下篇見。
作者:和耳朵
鏈接:
https://juejin.cn/post/7168133740093243423