上一節(jié),我們做的那個(gè)例子有點(diǎn)太簡(jiǎn)單了,通常的后臺(tái)都會(huì)涉及一些數(shù)據(jù)庫(kù)的操作,然后在暴露的API中提供處理后的數(shù)據(jù)給客戶端使用。那么這一節(jié)我們要做的是集成MongoDB ( https://www.mongodb.com )。
MongoDB是什么?
MongoDB是一個(gè)NoSQL數(shù)據(jù)庫(kù),是NoSQL中的一個(gè)分支:文檔數(shù)據(jù)庫(kù)。和傳統(tǒng)的關(guān)系型數(shù)據(jù)庫(kù)比如Oracle、SQLServer和MySQL等有很大的不同。傳統(tǒng)的關(guān)系型數(shù)據(jù)庫(kù)(RDBMS)已經(jīng)成為數(shù)據(jù)庫(kù)的代名詞超過(guò)20多年了。對(duì)于大多數(shù)開(kāi)發(fā)者來(lái)說(shuō),關(guān)系型數(shù)據(jù)庫(kù)是比較好理解的,表這種結(jié)構(gòu)和SQL這種標(biāo)準(zhǔn)化查詢語(yǔ)言畢竟是很大一部分開(kāi)發(fā)者已有的技能。那么為什么又搞出來(lái)了這個(gè)什么勞什子NoSQL,而且看上去NoSQL數(shù)據(jù)庫(kù)正在飛快的占領(lǐng)市場(chǎng)。
NoSQL的應(yīng)用場(chǎng)景是什么?
假設(shè)說(shuō)我們現(xiàn)在要構(gòu)建一個(gè)論壇,用戶可以發(fā)布帖子(帖子內(nèi)容包括文本、視頻、音頻和圖片等)。那么我們可以畫(huà)出一個(gè)下圖的表關(guān)系結(jié)構(gòu)。
論壇的簡(jiǎn)略ER圖
這種情況下我們想一下這樣一個(gè)帖子的結(jié)構(gòu)怎么在頁(yè)面中顯示,如果我們希望顯示帖子的文字,以及關(guān)聯(lián)的圖片、音頻、視頻、用戶評(píng)論、贊和用戶的信息的話,我們需要關(guān)聯(lián)八個(gè)表取得自己想要的數(shù)據(jù)。如果我們有這樣的帖子列表,而且是隨著用戶的滾動(dòng)動(dòng)態(tài)加載,同時(shí)需要監(jiān)聽(tīng)是否有新內(nèi)容的產(chǎn)生。這樣一個(gè)任務(wù)我們需要太多這種復(fù)雜的查詢了。
NoSQL解決這類(lèi)問(wèn)題的思路是,干脆拋棄傳統(tǒng)的表結(jié)構(gòu),你不是帖子有一個(gè)結(jié)構(gòu)關(guān)系嗎,那我就直接存儲(chǔ)和傳輸一個(gè)這樣的數(shù)據(jù)給你,像下面那樣。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
{ "id" : "5894a12f-dae1-5ab0-5761-1371ba4f703e" , "title" : "2017年的Spring發(fā)展方向" , "date" : "2017-01-21" , "createdBy" :User, "images" :[ "http://dev.local/myfirstimage.png" , "http://dev.local/mysecondimage.png" ], "videos" :[ { "url" : "http://dev.local/myfirstvideo.mp4" , "title" : "The first video" }, { "url" : "http://dev.local/mysecondvideo.mp4" , "title" : "The second video" } ], "audios" :[ { "url" : "http://dev.local/myfirstaudio.mp3" , "title" : "The first audio" }, { "url" : "http://dev.local/mysecondaudio.mp3" , "title" : "The second audio" } ] } |
NoSQL一般情況下是沒(méi)有Schema這個(gè)概念的,這也給開(kāi)發(fā)帶來(lái)較大的自由度。因?yàn)樵陉P(guān)系型數(shù)據(jù)庫(kù)中,一旦Schema確定,以后更改Schema,維護(hù)Schema是很麻煩的一件事。但反過(guò)來(lái)說(shuō)Schema對(duì)于維護(hù)數(shù)據(jù)的完整性是非常必要的。
一般來(lái)說(shuō),如果你在做一個(gè)Web、物聯(lián)網(wǎng)等類(lèi)型的項(xiàng)目,你應(yīng)該考慮使用NoSQL。如果你要面對(duì)的是一個(gè)對(duì)數(shù)據(jù)的完整性、事務(wù)處理等有嚴(yán)格要求的環(huán)境(比如財(cái)務(wù)系統(tǒng)),你應(yīng)該考慮關(guān)系型數(shù)據(jù)庫(kù)。
在Spring中集成MongoDB
在我們剛剛的項(xiàng)目中集成MongoDB簡(jiǎn)單到令人發(fā)指,只有三個(gè)步驟:
在 build.gradle 中更改 compile('org.springframework.boot:spring-boot-starter-web') 為 compile("org.springframework.boot:spring-boot-starter-data-rest")
在 Todo.java 中給 private String id; 之前加一個(gè)元數(shù)據(jù)修飾 @Id 以便讓Spring知道這個(gè)Id就是數(shù)據(jù)庫(kù)中的Id
新建一個(gè)如下的 TodoRepository.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
|
package dev.local.todo; import org.springframework.data.mongodb.repository.MongoRepository; import org.springframework.data.rest.core.annotation.RepositoryRestResource; @RepositoryRestResource (collectionResourceRel = "todo" , path = "todo" ) public interface TodoRepository extends MongoRepository<Todo, String>{ } 此時(shí)我們甚至不需要Controller了,所以暫時(shí)注釋掉 TodoController.java 中的代碼。然后我們 ./gradlew bootRun 啟動(dòng)應(yīng)用。訪問(wèn) http: //localhost:8080/todo 我們會(huì)得到下面的的結(jié)果。 { _embedded: { todo: [ ] }, _links: { self: { href: "http://localhost:8080/todo" }, profile: { href: "http://localhost:8080/profile/todo" } }, page: { size: 20 , totalElements: 0 , totalPages: 0 , number: 0 } } |
我勒個(gè)去,不光是有數(shù)據(jù)集的返回結(jié)果 todo: [ ] ,還附贈(zèng)了一個(gè)links對(duì)象和page對(duì)象。如果你了解 Hypermedia 的概念,就會(huì)發(fā)現(xiàn)這是個(gè)符合 Hypermedia REST API返回的數(shù)據(jù)。
說(shuō)兩句關(guān)于 MongoRepository<Todo, String>
這個(gè)接口,前一個(gè)參數(shù)類(lèi)型是領(lǐng)域?qū)ο箢?lèi)型,后一個(gè)指定該領(lǐng)域?qū)ο蟮腎d類(lèi)型。
Hypermedia REST
簡(jiǎn)單說(shuō)兩句Hypermedia是什么。簡(jiǎn)單來(lái)說(shuō)它是可以讓客戶端清晰的知道自己可以做什么,而無(wú)需依賴服務(wù)器端指示你做什么。原理呢,也很簡(jiǎn)單,通過(guò)返回的結(jié)果中包括不僅是數(shù)據(jù)本身,也包括指向相關(guān)資源的鏈接。拿上面的例子來(lái)說(shuō)(雖然這種默認(rèn)狀態(tài)生成的東西不是很有代表性):links中有一個(gè)profiles,我們看看這個(gè)profile的鏈接 http://localhost:8080/profile/todo 執(zhí)行的結(jié)果是什么:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
|
{ "alps" : { "version" : "1.0" , "descriptors" : [ { "id" : "todo-representation" , "href" : "http://localhost:8080/profile/todo" , "descriptors" : [ { "name" : "desc" , "type" : "SEMANTIC" }, { "name" : "completed" , "type" : "SEMANTIC" } ] }, { "id" : "create-todo" , "name" : "todo" , "type" : "UNSAFE" , "rt" : "#todo-representation" }, { "id" : "get-todo" , "name" : "todo" , "type" : "SAFE" , "rt" : "#todo-representation" , "descriptors" : [ { "name" : "page" , "doc" : { "value" : "The page to return." , "format" : "TEXT" }, "type" : "SEMANTIC" }, { "name" : "size" , "doc" : { "value" : "The size of the page to return." , "format" : "TEXT" }, "type" : "SEMANTIC" }, { "name" : "sort" , "doc" : { "value" : "The sorting criteria to use to calculate the content of the page." , "format" : "TEXT" }, "type" : "SEMANTIC" } ] }, { "id" : "patch-todo" , "name" : "todo" , "type" : "UNSAFE" , "rt" : "#todo-representation" }, { "id" : "update-todo" , "name" : "todo" , "type" : "IDEMPOTENT" , "rt" : "#todo-representation" }, { "id" : "delete-todo" , "name" : "todo" , "type" : "IDEMPOTENT" , "rt" : "#todo-representation" }, { "id" : "get-todo" , "name" : "todo" , "type" : "SAFE" , "rt" : "#todo-representation" } ] } } |
這個(gè)對(duì)象雖然我們暫時(shí)不是完全的理解,但大致可以猜出來(lái),這個(gè)是todo這個(gè)REST API的元數(shù)據(jù)描述,告訴我們這個(gè)API中定義了哪些操作和接受哪些參數(shù)等等。我們可以看到todo這個(gè)API有增刪改查等對(duì)應(yīng)功能。
其實(shí)呢,Spring是使用了一個(gè)叫 ALPS (http://alps.io/spec/index.html) 的專(zhuān)門(mén)描述應(yīng)用語(yǔ)義的數(shù)據(jù)格式。摘出下面這一小段來(lái)分析一下,這個(gè)描述了一個(gè)get方法,類(lèi)型是 SAFE 表明這個(gè)操作不會(huì)對(duì)系統(tǒng)狀態(tài)產(chǎn)生影響(因?yàn)橹皇遣樵儯疫@個(gè)操作返回的結(jié)果格式定義在 todo-representation 中了。 todo-representation
1
2
3
4
5
6
|
{ "id" : "get-todo" , "name" : "todo" , "type" : "SAFE" , "rt" : "#todo-representation" } |
還是不太理解?沒(méi)關(guān)系,我們?cè)賮?lái)做一個(gè)實(shí)驗(yàn),啟動(dòng) PostMan (不知道的同學(xué),可以去Chrome應(yīng)用商店中搜索下載)。我們用Postman構(gòu)建一個(gè)POST請(qǐng)求:
用Postman構(gòu)建一個(gè)POST請(qǐng)求添加一個(gè)Todo
執(zhí)行后的結(jié)果如下,我們可以看到返回的links中包括了剛剛新增的Todo的link http://localhost:8080/todo/588a01abc5d0e23873d4c1b8 ( 588a01abc5d0e23873d4c1b8 就是數(shù)據(jù)庫(kù)自動(dòng)為這個(gè)Todo生成的Id),這樣客戶端可以方便的知道指向剛剛生成的Todo的API鏈接。
執(zhí)行添加Todo后的返回Json數(shù)據(jù)
再舉一個(gè)現(xiàn)實(shí)一些的例子,我們?cè)陂_(kāi)發(fā)一個(gè)“我的”頁(yè)面時(shí),一般情況下除了取得我的某些信息之外,因?yàn)樵谶@個(gè)頁(yè)面還會(huì)有一些可以鏈接到更具體信息的頁(yè)面鏈接。如果客戶端在取得比較概要信息的同時(shí)就得到這些詳情的鏈接,那么客戶端的開(kāi)發(fā)就比較簡(jiǎn)單了,而且也更靈活了。
其實(shí)這個(gè)描述中還告訴我們一些分頁(yè)的信息,比如每頁(yè)20條記錄(size: 20)、總共幾頁(yè)(totalPages:1)、總共多少個(gè)元素(totalElements: 1)、當(dāng)前第幾頁(yè)(number: 0)。當(dāng)然你也可以在發(fā)送API請(qǐng)求時(shí),指定page、size或sort參數(shù)。比如 http://localhost:8080/todos?page=0&size=10 就是指定每頁(yè)10條,當(dāng)前頁(yè)是第一頁(yè)(從0開(kāi)始)。
魔法的背后
這么簡(jiǎn)單就生成一個(gè)有數(shù)據(jù)庫(kù)支持的REST API,這件事看起來(lái)比較魔幻,但一般這么魔幻的事情總感覺(jué)不太托底,除非我們知道背后的原理是什么。首先再來(lái)回顧一下 TodoRepository 的代碼:
1
2
3
|
@RepositoryRestResource (collectionResourceRel = "todo" , path = "todo" ) public interface TodoRepository extends MongoRepository<Todo, String>{ } |
Spring是最早的幾個(gè)IoC(控制反轉(zhuǎn)或者叫DI)框架之一,所以最擅長(zhǎng)的就是依賴的注入了。這里我們寫(xiě)了一個(gè)Interface,可以猜到Spring一定是有一個(gè)這個(gè)接口的實(shí)現(xiàn)在運(yùn)行時(shí)注入了進(jìn)去。如果我們?nèi)?spring-data-mongodb 的源碼中看一下就知道是怎么回事了,這里只舉一個(gè)小例子,大家可以去看一下 SimpleMongoRepository.java ( 源碼鏈接 ),由于源碼太長(zhǎng),我只截取一部分:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
|
public class SimpleMongoRepository<T, ID extends Serializable> implements MongoRepository<T, ID> { private final MongoOperations mongoOperations; private final MongoEntityInformation<T, ID> entityInformation; /** * Creates a new {@link SimpleMongoRepository} for the given {@link MongoEntityInformation} and {@link MongoTemplate}. * * @param metadata must not be {@literal null}. * @param mongoOperations must not be {@literal null}. */ public SimpleMongoRepository(MongoEntityInformation<T, ID> metadata, MongoOperations mongoOperations) { Assert.notNull(mongoOperations); Assert.notNull(metadata); this .entityInformation = metadata; this .mongoOperations = mongoOperations; } /* * (non-Javadoc) * @see org.springframework.data.repository.CrudRepository#save(java.lang.Object) */ public <S extends T> S save(S entity) { Assert.notNull(entity, "Entity must not be null!" ); if (entityInformation.isNew(entity)) { mongoOperations.insert(entity, entityInformation.getCollectionName()); } else { mongoOperations.save(entity, entityInformation.getCollectionName()); } return entity; } ... public T findOne(ID id) { Assert.notNull(id, "The given id must not be null!" ); return mongoOperations.findById(id, entityInformation.getJavaType(), entityInformation.getCollectionName()); } private Query getIdQuery(Object id) { return new Query(getIdCriteria(id)); } private Criteria getIdCriteria(Object id) { return where(entityInformation.getIdAttribute()).is(id); } ... } |
也就是說(shuō)其實(shí)在運(yùn)行時(shí)Spring將這個(gè)類(lèi)或者其他具體接口的實(shí)現(xiàn)類(lèi)注入了應(yīng)用。這個(gè)類(lèi)中有支持各種數(shù)據(jù)庫(kù)的操作。我了解到這步就覺(jué)得ok了,有興趣的同學(xué)可以繼續(xù)深入研究。
雖然不想在具體類(lèi)上繼續(xù)研究,但我們還是應(yīng)該多了解一些關(guān)于 MongoRepository 的東西。這個(gè)接口繼承了 PagingAndSortingRepository (定義了排序和分頁(yè)) 和 QueryByExampleExecutor。而 PagingAndSortingRepository 又繼承了 CrudRepository (定義了增刪改查等)。
第二個(gè)魔法就是 @RepositoryRestResource(collectionResourceRel = "todo", path = "todo") 這個(gè)元數(shù)據(jù)的修飾了,它直接對(duì)MongoDB中的集合(本例中的todo)映射到了一個(gè)REST URI(todo)。因此我們連Controller都沒(méi)寫(xiě)就把API搞出來(lái)了,而且還是個(gè)Hypermedia REST。
其實(shí)呢,這個(gè)第二個(gè)魔法只在你需要變更映射路徑時(shí)需要。本例中如果我們不加 @RepositoryRestResource 這個(gè)修飾符的話,同樣也可以生成API,只不過(guò)其路徑按照默認(rèn)的方式變成了 todoes ,大家可以試試把這個(gè)元數(shù)據(jù)修飾去掉,然后重啟服務(wù),訪問(wèn) http://localhost:8080/todoes 看看。
說(shuō)到這里,順便說(shuō)一下REST的一些約定俗成的規(guī)矩。一般來(lái)說(shuō)如果我們定義了一個(gè)領(lǐng)域?qū)ο?(比如我們這里的Todo),那么這個(gè)對(duì)象的集合(比如Todo的列表)可以使用這個(gè)對(duì)象的命名的復(fù)數(shù)方式定義其資源URL,也就是剛剛我們?cè)L問(wèn)的 http://localhost:8080/todoes,對(duì)于新增一個(gè)對(duì)象的操作也是這個(gè)URL,但Request的方法是POST。
而這個(gè)某個(gè)指定的對(duì)象(比如指定了某個(gè)ID的Todo)可以使用 todoes/:id 來(lái)訪問(wèn),比如本例中 http://localhost:8080/todoes/588a01abc5d0e23873d4c1b8。對(duì)于這個(gè)對(duì)象的修改和刪除使用的也是這個(gè)URL,只不過(guò)HTTP Request的方法變成了PUT(或者PATCH)和DELETE。
這個(gè)里面默認(rèn)采用的這個(gè)命名 todoes 是根據(jù)英語(yǔ)的語(yǔ)法來(lái)的,一般來(lái)說(shuō)復(fù)數(shù)是加s即可,但這個(gè)todo,是輔音+o結(jié)尾,所以采用的加es方式。 todo 其實(shí)并不是一個(gè)真正意義上的單詞,所以我認(rèn)為更合理的命名方式應(yīng)該是 todos。所以我們還是改成 @RepositoryRestResource(collectionResourceRel = "todos", path = "todos")
無(wú)招勝有招
剛才我們提到的都是開(kāi)箱即用的一些方法,你可能會(huì)想,這些東西看上去很炫,但沒(méi)有毛用,實(shí)際開(kāi)發(fā)過(guò)程中,我們要使用的肯定不是這么簡(jiǎn)單的增刪改查啊。說(shuō)的有道理,我們來(lái)試試看非默認(rèn)方法。那么我們就來(lái)增加一個(gè)需求,我們可以通過(guò)查詢Todo的描述中的關(guān)鍵字來(lái)搜索符合的項(xiàng)目。
顯然這個(gè)查詢不是默認(rèn)的操作,那么這個(gè)需求在Spring Boot中怎么實(shí)現(xiàn)呢?非常簡(jiǎn)單,只需在 TodoRepository 中添加一個(gè)方法:
...
public interface TodoRepository extends MongoRepository<Todo, String>{
List<Todo> findByDescLike(@Param("desc") String desc);
}
太不可思議了,這樣就行?不信可以啟動(dòng)服務(wù)后,在瀏覽器中輸入 http://localhost:8080/todos/search/findByDescLike?desc=swim 去看看結(jié)果。是的,我們甚至都沒(méi)有寫(xiě)這個(gè)方法的實(shí)現(xiàn)就已經(jīng)完成了該需求(題外話,其實(shí) http://localhost:8080/todos?desc=swim 這個(gè)URL也起作用)。
你說(shuō)這里肯定有鬼,我同意。那么我們?cè)囋嚢堰@個(gè)方法改個(gè)名字 findDescLike ,果然不好用了。為什么呢?這套神奇的療法的背后還是那個(gè)我們?cè)诘谝黄獣r(shí)提到的 Convention over configuration,要神奇的療效就得遵循Spring的配方。這個(gè)配方就是方法的命名是有講究的:Spring提供了一套可以通過(guò)命名規(guī)則進(jìn)行查詢構(gòu)建的機(jī)制。這套機(jī)制會(huì)把方法名首先過(guò)濾一些關(guān)鍵字,比如 find…By, read…By, query…By, count…By 和 get…By 。系統(tǒng)會(huì)根據(jù)關(guān)鍵字將命名解析成2個(gè)子語(yǔ)句,第一個(gè) By 是區(qū)分這兩個(gè)子語(yǔ)句的關(guān)鍵詞。這個(gè) By 之前的子語(yǔ)句是查詢子語(yǔ)句(指明返回要查詢的對(duì)象),后面的部分是條件子語(yǔ)句。如果直接就是 findBy… 返回的就是定義Respository時(shí)指定的領(lǐng)域?qū)ο蠹希ū纠械腡odo組成的集合)。
一般到這里,有的同學(xué)可能會(huì)問(wèn) find…By, read…By, query…By, get…By 到底有什么區(qū)別啊?答案是。。。木有區(qū)別,就是別名,從下面的定義可以看到這幾個(gè)東東其實(shí)生成的查詢是一樣的,這種讓你不用查文檔都可以寫(xiě)對(duì)的方式也比較貼近目前流行的自然語(yǔ)言描述風(fēng)格(類(lèi)似各種DSL)。
1
|
private static final String QUERY_PATTERN = "find|read|get|query|stream" ; |
剛剛我們實(shí)驗(yàn)了模糊查詢,那如果要是精確查找怎么做呢,比如我們要篩選出已完成或未完成的Todo,也很簡(jiǎn)單:
1
|
List<Todo> findByCompleted( @Param ( "completed" ) boolean completed); |
嵌套對(duì)象的查詢?cè)趺锤悖?/strong>
看到這里你會(huì)問(wèn),這都是簡(jiǎn)單類(lèi)型,如果復(fù)雜類(lèi)型怎么辦?嗯,好的,我們還是增加一個(gè)需求看一下:現(xiàn)在需求是要這個(gè)API是多用戶的,每個(gè)用戶看到的Todo都是他們自己創(chuàng)建的項(xiàng)目。我們新建一個(gè)User領(lǐng)域?qū)ο螅?/p>
1
2
3
4
5
6
7
8
|
package dev.local.user; import org.springframework.data.annotation.Id; public class User { @Id private String id; private String username; private String email; //此處為節(jié)省篇幅省略屬性的getter和setter } |
為了可以添加User數(shù)據(jù),我們需要一個(gè)User的REST API,所以添加一個(gè) UserRepository
1
2
3
4
|
package dev.local.user; import org.springframework.data.mongodb.repository.MongoRepository; public interface UserRepository extends MongoRepository<User, String> { } |
然后給 Todo 領(lǐng)域?qū)ο筇砑右粋€(gè)User屬性。
1
2
3
4
5
6
7
8
9
10
11
12
|
package dev.local.todo; //省略import部分 public class Todo { //省略其他部分 private User user; public User getUser() { return user; } public void setUser(User user) { this .user = user; } } |
接下來(lái)就可以來(lái)把 TodoRepository 添加一個(gè)方法定義了,我們先實(shí)驗(yàn)一個(gè)簡(jiǎn)單點(diǎn)的,根據(jù)用戶的email來(lái)篩選出這個(gè)用戶的Todo列表:
1
2
3
|
public interface TodoRepository extends MongoRepository<Todo, String>{ List<Todo> findByUserEmail( @Param ( "userEmail" ) String userEmail); } |
現(xiàn)在需要構(gòu)造一些數(shù)據(jù)了,你可以通過(guò)剛剛我們建立的API使用Postman工具來(lái)構(gòu)造:我們這里創(chuàng)建了2個(gè)用戶,以及一些Todo項(xiàng)目,分別屬于這兩個(gè)用戶,而且有部分項(xiàng)目的描述是一樣的。接下來(lái)就可以實(shí)驗(yàn)一下了,我們?cè)跒g覽器中輸入 http://localhost:8080/todos/search/findByUserEmail?userEmail=peng@gmail.com ,我們會(huì)發(fā)現(xiàn)返回的結(jié)果中只有這個(gè)用戶的Todo項(xiàng)目。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
|
{ "_embedded" : { "todos" : [ { "desc" : "go swimming" , "completed" : false , "user" : { "username" : "peng" , "email" : "peng@gmail.com" }, "_links" : { "self" : { "href" : "http://localhost:8080/todos/58908a92c5d0e2524e24545a" }, "todo" : { "href" : "http://localhost:8080/todos/58908a92c5d0e2524e24545a" } } }, { "desc" : "go for walk" , "completed" : false , "user" : { "username" : "peng" , "email" : "peng@gmail.com" }, "_links" : { "self" : { "href" : "http://localhost:8080/todos/58908aa1c5d0e2524e24545b" }, "todo" : { "href" : "http://localhost:8080/todos/58908aa1c5d0e2524e24545b" } } }, { "desc" : "have lunch" , "completed" : false , "user" : { "username" : "peng" , "email" : "peng@gmail.com" }, "_links" : { "self" : { "href" : "http://localhost:8080/todos/58908ab6c5d0e2524e24545c" }, "todo" : { "href" : "http://localhost:8080/todos/58908ab6c5d0e2524e24545c" } } }, { "desc" : "have dinner" , "completed" : false , "user" : { "username" : "peng" , "email" : "peng@gmail.com" }, "_links" : { "self" : { "href" : "http://localhost:8080/todos/58908abdc5d0e2524e24545d" }, "todo" : { "href" : "http://localhost:8080/todos/58908abdc5d0e2524e24545d" } } } ] }, "_links" : { "self" : { "href" : "http://localhost:8080/todos/search/findByUserEmail?userEmail=peng@gmail.com" } } } |
看到結(jié)果后我們來(lái)分析這個(gè) findByUserEmail 是如何解析的:首先在 By 之后,解析器會(huì)按照 camel (每個(gè)單詞首字母大寫(xiě))的規(guī)則來(lái)分詞。那么第一個(gè)詞是 User,這個(gè)屬性在 Todo 中有沒(méi)有呢?有的,但是這個(gè)屬性是另一個(gè)對(duì)象類(lèi)型,所以緊跟著這個(gè)詞的 Email 就要在 User 類(lèi)中去查找是否有 Email 這個(gè)屬性。聰明如你,肯定會(huì)想到,那如果在 Todo 類(lèi)中如果還有一個(gè)屬性叫 userEmail 怎么辦?是的,這種情況下 userEmail 會(huì)被優(yōu)先匹配,此時(shí)請(qǐng)使用 _ 來(lái)顯性分詞處理這種混淆。也就是說(shuō)如果我們的 Todo 類(lèi)中同時(shí)有 user 和 userEmail 兩個(gè)屬性的情況下,我們?nèi)绻胍付ǖ氖?user 的 email ,那么需要寫(xiě)成 findByUser_Email。
還有一個(gè)問(wèn)題,我估計(jì)很多同學(xué)現(xiàn)在已經(jīng)在想了,那就是我們的這個(gè)例子中并沒(méi)有使用 user 的 id,這不科學(xué)啊。是的,之所以沒(méi)有在上面使用 findByUserId 是因?yàn)橐鲆粋€(gè)易錯(cuò)的地方,下面我們來(lái)試試看,將 TodoRepository 的方法改成
1
2
3
|
public interface TodoRepository extends MongoRepository<Todo, String>{ List<Todo> findByUserId( @Param ( "userId" ) String userId); } |
你如果打開(kāi)瀏覽器輸入 http://localhost:8080/todos/search/findByUserId?userId=589089c3c5d0e2524e245458 (這里的Id請(qǐng)改成你自己mongodb中的user的id),你會(huì)發(fā)現(xiàn)返回的結(jié)果是個(gè)空數(shù)組。原因是雖然我們?cè)陬?lèi)中標(biāo)識(shí) id 為 String 類(lèi)型,但對(duì)于這種數(shù)據(jù)庫(kù)自己生成維護(hù)的字段,它在MongoDB中的類(lèi)型是ObjectId,所以在我們的接口定義的查詢函數(shù)中應(yīng)該標(biāo)識(shí)這個(gè)參數(shù)是 ObjectId。那么我們只需要改動(dòng) userId 的類(lèi)型為 org.bson.types.ObjectId 即可。
1
2
3
4
5
6
7
8
9
10
|
package dev.local.todo; import org.bson.types.ObjectId; import org.springframework.data.mongodb.repository.MongoRepository; import org.springframework.data.repository.query.Param; import org.springframework.data.rest.core.annotation.RepositoryRestResource; import java.util.List; @RepositoryRestResource (collectionResourceRel = "todos" , path = "todos" ) public interface TodoRepository extends MongoRepository<Todo, String>{ List<Todo> findByUserId( @Param ( "userId" ) ObjectId userId); } |
再?gòu)?fù)雜一些行不行?
好吧,到現(xiàn)在我估計(jì)還有一大波攻城獅表示不服,實(shí)際開(kāi)發(fā)中需要的查詢比上面的要復(fù)雜的多,再?gòu)?fù)雜一些怎么辦?還是用例子來(lái)說(shuō)話吧,那么現(xiàn)在我們想要模糊搜索指定用戶的Todo中描述的關(guān)鍵字,返回匹配的集合。這個(gè)需求我們只需改動(dòng)一行,這個(gè)以命名規(guī)則為基礎(chǔ)的查詢條件是可以加 And、Or 這種關(guān)聯(lián)多個(gè)條件的關(guān)鍵字的。
1
|
List<Todo> findByUserIdAndDescLike( @Param ( "userId" ) ObjectId userId, @Param ( "desc" ) String desc); |
當(dāng)然,還有其他操作符:Between (值在兩者之間), LessThan (小于), GreaterThan (大于), Like (包含), IgnoreCase (b忽略大小寫(xiě)), AllIgnoreCase (對(duì)于多個(gè)參數(shù)全部忽略大小寫(xiě)), OrderBy (引導(dǎo)排序子語(yǔ)句), Asc (升序,僅在 OrderBy 后有效) 和 Desc (降序,僅在 OrderBy 后有效)。
剛剛我們談到的都是對(duì)于查詢條件子語(yǔ)句的構(gòu)建,其實(shí)在 By 之前,對(duì)于要查詢的對(duì)象也可以有限定的修飾詞 Distinct (去重,如有重復(fù)取一個(gè)值)。比如有可能返回的結(jié)果有重復(fù)的記錄,可以使用 findDistinctTodoByUserIdAndDescLike。
我可以直接寫(xiě)查詢語(yǔ)句嗎?幾乎所有碼農(nóng)都會(huì)問(wèn)的問(wèn)題。當(dāng)然可以咯,也是同樣簡(jiǎn)單,就是給方法加上一個(gè)元數(shù)據(jù)修飾符 @Query
1
2
3
4
|
public interface TodoRepository extends MongoRepository<Todo, String>{ @Query ( "{ 'user._id': ?0, 'desc': { '$regex': ?1} }" ) List<Todo> searchTodos( @Param ( "userId" ) ObjectId userId, @Param ( "desc" ) String desc); } |
采用這種方式我們就不用按照命名規(guī)則起方法名了,可以直接使用MongoDB的查詢進(jìn)行。上面的例子中有幾個(gè)地方需要說(shuō)明一下
?0 和 ?1 是參數(shù)的占位符,?0 表示第一個(gè)參數(shù),也就是 userId;?1 表示第二個(gè)參數(shù)也就是 desc。
使用user._id 而不是 user.id 是因?yàn)樗斜?@Id 修飾的屬性在Spring Data中都會(huì)被轉(zhuǎn)換成 _id
MongoDB中沒(méi)有關(guān)系型數(shù)據(jù)庫(kù)的Like關(guān)鍵字,需要以正則表達(dá)式的方式達(dá)成類(lèi)似的功能。
其實(shí),這種支持的力度已經(jīng)可以讓我們寫(xiě)出相對(duì)較復(fù)雜的查詢了。但肯定還是不夠的,對(duì)于開(kāi)發(fā)人員來(lái)講,如果不給可以自定義的方式基本沒(méi)人會(huì)用的,因?yàn)榭傆羞@樣那樣的原因會(huì)導(dǎo)致我們希望能完全掌控我們的查詢或存儲(chǔ)過(guò)程。但這個(gè)話題展開(kāi)感覺(jué)就內(nèi)容更多了,后面再講吧。
本章代碼:https://github.com/wpcfan/spring-boot-tut/tree/chap02
原文鏈接:https://juejin.im/post/589282280ce4630056e76b12