重拾后端之Spring Boot(二):MongoDB的無縫集成

重拾后端之Spring Boot(一):REST API的搭建可以這樣簡單
重拾后端之Spring Boot(二):MongoDb的無縫集成
重拾后端之Spring Boot(三):找回熟悉的Controller并思,Service
重拾后端之Spring Boot(四):使用 JWT 和 Spring Security 保護 REST API
重拾后端之Spring Boot(五):跨域、自定義查詢及分頁
重拾后端之Spring Boot(六):熱加載帖鸦、容器和多項目

上一節(jié)术唬,我們做的那個例子有點太簡單了坐求,通常的后臺都會涉及一些數(shù)據(jù)庫的操作讼载,然后在暴露的API中提供處理后的數(shù)據(jù)給客戶端使用。那么這一節(jié)我們要做的是集成MongoDB ( https://www.mongodb.com )儒溉。

MongoDB是什么?

MongoDB是一個NoSQL數(shù)據(jù)庫发钝,是NoSQL中的一個分支:文檔數(shù)據(jù)庫顿涣。和傳統(tǒng)的關(guān)系型數(shù)據(jù)庫比如Oracle、SQLServer和MySQL等有很大的不同酝豪。傳統(tǒng)的關(guān)系型數(shù)據(jù)庫(RDBMS)已經(jīng)成為數(shù)據(jù)庫的代名詞超過20多年了涛碑。對于大多數(shù)開發(fā)者來說,關(guān)系型數(shù)據(jù)庫是比較好理解的孵淘,表這種結(jié)構(gòu)和SQL這種標(biāo)準化查詢語言畢竟是很大一部分開發(fā)者已有的技能蒲障。那么為什么又搞出來了這個什么勞什子NoSQL,而且看上去NoSQL數(shù)據(jù)庫正在飛快的占領(lǐng)市場。

NoSQL的應(yīng)用場景是什么揉阎?

假設(shè)說我們現(xiàn)在要構(gòu)建一個論壇庄撮,用戶可以發(fā)布帖子(帖子內(nèi)容包括文本、視頻毙籽、音頻和圖片等)洞斯。那么我們可以畫出一個下圖的表關(guān)系結(jié)構(gòu)。

論壇的簡略ER圖
論壇的簡略ER圖

這種情況下我們想一下這樣一個帖子的結(jié)構(gòu)怎么在頁面中顯示坑赡,如果我們希望顯示帖子的文字烙如,以及關(guān)聯(lián)的圖片、音頻毅否、視頻亚铁、用戶評論、贊和用戶的信息的話螟加,我們需要關(guān)聯(lián)八個表取得自己想要的數(shù)據(jù)徘溢。如果我們有這樣的帖子列表,而且是隨著用戶的滾動動態(tài)加載仰迁,同時需要監(jiān)聽是否有新內(nèi)容的產(chǎn)生甸昏。這樣一個任務(wù)我們需要太多這種復(fù)雜的查詢了。

NoSQL解決這類問題的思路是徐许,干脆拋棄傳統(tǒng)的表結(jié)構(gòu)施蜜,你不是帖子有一個結(jié)構(gòu)關(guān)系嗎,那我就直接存儲和傳輸一個這樣的數(shù)據(jù)給你雌隅,像下面那樣翻默。

{
    "id":"5894a12f-dae1-5ab0-5761-1371ba4f703e",
    "title":"2017年的Spring發(fā)展方向",
    "date":"2017-01-21",
    "body":"這篇文章主要探討如何利用Spring Boot集成NoSQL",
    "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一般情況下是沒有Schema這個概念的,這也給開發(fā)帶來較大的自由度恰起。因為在關(guān)系型數(shù)據(jù)庫中修械,一旦Schema確定,以后更改Schema检盼,維護Schema是很麻煩的一件事肯污。但反過來說Schema對于維護數(shù)據(jù)的完整性是非常必要的。

一般來說吨枉,如果你在做一個Web蹦渣、物聯(lián)網(wǎng)等類型的項目,你應(yīng)該考慮使用NoSQL貌亭。如果你要面對的是一個對數(shù)據(jù)的完整性柬唯、事務(wù)處理等有嚴格要求的環(huán)境(比如財務(wù)系統(tǒng)),你應(yīng)該考慮關(guān)系型數(shù)據(jù)庫圃庭。

在Spring中集成MongoDB

在我們剛剛的項目中集成MongoDB簡單到令人發(fā)指锄奢,只有三個步驟:

  1. build.gradle 中更改 compile('org.springframework.boot:spring-boot-starter-web')compile("org.springframework.boot:spring-boot-starter-data-rest")
  2. Todo.java 中給 private String id; 之前加一個元數(shù)據(jù)修飾 @Id 以便讓Spring知道這個Id就是數(shù)據(jù)庫中的Id
  3. 新建一個如下的 TodoRepository.java
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>{
}

此時我們甚至不需要Controller了失晴,所以暫時注釋掉 TodoController.java 中的代碼。然后我們 ./gradlew bootRun 啟動應(yīng)用拘央。訪問 http://localhost:8080/todo 我們會得到下面的的結(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
    }
}

我勒個去,不光是有數(shù)據(jù)集的返回結(jié)果 todo: [ ] 堪滨,還附贈了一個links對象和page對象胯陋。如果你了解 Hypermedia 的概念,就會發(fā)現(xiàn)這是個符合 Hypermedia REST API返回的數(shù)據(jù)袱箱。

說兩句關(guān)于 MongoRepository<Todo, String> 這個接口遏乔,前一個參數(shù)類型是領(lǐng)域?qū)ο箢愋停笠粋€指定該領(lǐng)域?qū)ο蟮腎d類型发笔。

Hypermedia REST

簡單說兩句Hypermedia是什么盟萨。簡單來說它是可以讓客戶端清晰的知道自己可以做什么,而無需依賴服務(wù)器端指示你做什么了讨。原理呢捻激,也很簡單,通過返回的結(jié)果中包括不僅是數(shù)據(jù)本身前计,也包括指向相關(guān)資源的鏈接胞谭。拿上面的例子來說(雖然這種默認狀態(tài)生成的東西不是很有代表性):links中有一個profiles,我們看看這個profile的鏈接 http://localhost:8080/profile/todo 執(zhí)行的結(jié)果是什么:

{
  "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"
        } 
    ]
  }
}

這個對象雖然我們暫時不是完全的理解男杈,但大致可以猜出來丈屹,這個是todo這個REST API的元數(shù)據(jù)描述,告訴我們這個API中定義了哪些操作和接受哪些參數(shù)等等伶棒。我們可以看到todo這個API有增刪改查等對應(yīng)功能旺垒。

其實呢,Spring是使用了一個叫 ALPShttp://alps.io/spec/index.html) 的專門描述應(yīng)用語義的數(shù)據(jù)格式肤无。摘出下面這一小段來分析一下先蒋,這個描述了一個get方法,類型是 SAFE 表明這個操作不會對系統(tǒng)狀態(tài)產(chǎn)生影響(因為只是查詢)宛渐,而且這個操作返回的結(jié)果格式定義在 todo-representation 中了竞漾。 todo-representation

{
  "id" : "get-todo",
  "name" : "todo",
  "type" : "SAFE",
  "rt" : "#todo-representation"
} 

還是不太理解?沒關(guān)系窥翩,我們再來做一個實驗畴蹭,啟動 PostMan (不知道的同學(xué),可以去Chrome應(yīng)用商店中搜索下載)鳍烁。我們用Postman構(gòu)建一個POST請求:

用Postman構(gòu)建一個POST請求添加一個Todo
用Postman構(gòu)建一個POST請求添加一個Todo

執(zhí)行后的結(jié)果如下,我們可以看到返回的links中包括了剛剛新增的Todo的link http://localhost:8080/todo/588a01abc5d0e23873d4c1b8588a01abc5d0e23873d4c1b8 就是數(shù)據(jù)庫自動為這個Todo生成的Id)繁扎,這樣客戶端可以方便的知道指向剛剛生成的Todo的API鏈接幔荒。

執(zhí)行添加Todo后的返回Json數(shù)據(jù)
執(zhí)行添加Todo后的返回Json數(shù)據(jù)

再舉一個現(xiàn)實一些的例子糊闽,我們在開發(fā)一個“我的”頁面時,一般情況下除了取得我的某些信息之外爹梁,因為在這個頁面還會有一些可以鏈接到更具體信息的頁面鏈接右犹。如果客戶端在取得比較概要信息的同時就得到這些詳情的鏈接,那么客戶端的開發(fā)就比較簡單了姚垃,而且也更靈活了念链。

其實這個描述中還告訴我們一些分頁的信息,比如每頁20條記錄(size: 20)积糯、總共幾頁(totalPages:1)掂墓、總共多少個元素(totalElements: 1)、當(dāng)前第幾頁(number: 0)看成。當(dāng)然你也可以在發(fā)送API請求時君编,指定page、size或sort參數(shù)川慌。比如 http://localhost:8080/todos?page=0&size=10 就是指定每頁10條吃嘿,當(dāng)前頁是第一頁(從0開始)。

魔法的背后

這么簡單就生成一個有數(shù)據(jù)庫支持的REST API梦重,這件事看起來比較魔幻兑燥,但一般這么魔幻的事情總感覺不太托底,除非我們知道背后的原理是什么琴拧。首先再來回顧一下 TodoRepository 的代碼:

@RepositoryRestResource(collectionResourceRel = "todo", path = "todo")
public interface TodoRepository extends MongoRepository<Todo, String>{
}

Spring是最早的幾個IoC(控制反轉(zhuǎn)或者叫DI)框架之一降瞳,所以最擅長的就是依賴的注入了。這里我們寫了一個Interface艾蓝,可以猜到Spring一定是有一個這個接口的實現(xiàn)在運行時注入了進去力崇。如果我們?nèi)?spring-data-mongodb 的源碼中看一下就知道是怎么回事了,這里只舉一個小例子赢织,大家可以去看一下 SimpleMongoRepository.java源碼鏈接 )亮靴,由于源碼太長,我只截取一部分:

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);
    }
    ...
}

也就是說其實在運行時Spring將這個類或者其他具體接口的實現(xiàn)類注入了應(yīng)用于置。這個類中有支持各種數(shù)據(jù)庫的操作茧吊。我了解到這步就覺得ok了,有興趣的同學(xué)可以繼續(xù)深入研究八毯。

雖然不想在具體類上繼續(xù)研究搓侄,但我們還是應(yīng)該多了解一些關(guān)于 MongoRepository 的東西。這個接口繼承了 PagingAndSortingRepository (定義了排序和分頁) 和 QueryByExampleExecutor话速。而 PagingAndSortingRepository 又繼承了 CrudRepository (定義了增刪改查等)讶踪。

第二個魔法就是 @RepositoryRestResource(collectionResourceRel = "todo", path = "todo") 這個元數(shù)據(jù)的修飾了,它直接對MongoDB中的集合(本例中的todo)映射到了一個REST URI(todo)泊交。因此我們連Controller都沒寫就把API搞出來了乳讥,而且還是個Hypermedia REST柱查。

其實呢,這個第二個魔法只在你需要變更映射路徑時需要云石。本例中如果我們不加 @RepositoryRestResource 這個修飾符的話唉工,同樣也可以生成API,只不過其路徑按照默認的方式變成了 todoes 汹忠,大家可以試試把這個元數(shù)據(jù)修飾去掉淋硝,然后重啟服務(wù),訪問 http://localhost:8080/todoes 看看宽菜。

說到這里谣膳,順便說一下REST的一些約定俗成的規(guī)矩。一般來說如果我們定義了一個領(lǐng)域?qū)ο?(比如我們這里的Todo)赋焕,那么這個對象的集合(比如Todo的列表)可以使用這個對象的命名的復(fù)數(shù)方式定義其資源URL参歹,也就是剛剛我們訪問的 http://localhost:8080/todoes,對于新增一個對象的操作也是這個URL隆判,但Request的方法是POST犬庇。

而這個某個指定的對象(比如指定了某個ID的Todo)可以使用 todoes/:id 來訪問,比如本例中 http://localhost:8080/todoes/588a01abc5d0e23873d4c1b8侨嘀。對于這個對象的修改和刪除使用的也是這個URL臭挽,只不過HTTP Request的方法變成了PUT(或者PATCH)和DELETE。

這個里面默認采用的這個命名 todoes 是根據(jù)英語的語法來的咬腕,一般來說復(fù)數(shù)是加s即可欢峰,但這個todo,是輔音+o結(jié)尾涨共,所以采用的加es方式纽帖。 todo 其實并不是一個真正意義上的單詞,所以我認為更合理的命名方式應(yīng)該是 todos举反。所以我們還是改成 @RepositoryRestResource(collectionResourceRel = "todos", path = "todos")

無招勝有招

剛才我們提到的都是開箱即用的一些方法懊直,你可能會想,這些東西看上去很炫火鼻,但沒有毛用室囊,實際開發(fā)過程中,我們要使用的肯定不是這么簡單的增刪改查啊魁索。說的有道理融撞,我們來試試看非默認方法。那么我們就來增加一個需求粗蔚,我們可以通過查詢Todo的描述中的關(guān)鍵字來搜索符合的項目尝偎。

顯然這個查詢不是默認的操作,那么這個需求在Spring Boot中怎么實現(xiàn)呢鹏控?非常簡單冬念,只需在 TodoRepository 中添加一個方法:

...
public interface TodoRepository extends MongoRepository<Todo, String>{
    List<Todo> findByDescLike(@Param("desc") String desc);
}

太不可思議了趁窃,這樣就行?不信可以啟動服務(wù)后急前,在瀏覽器中輸入 http://localhost:8080/todos/search/findByDescLike?desc=swim 去看看結(jié)果。是的瀑构,我們甚至都沒有寫這個方法的實現(xiàn)就已經(jīng)完成了該需求(題外話裆针,其實 http://localhost:8080/todos?desc=swim 這個URL也起作用)。

你說這里肯定有鬼寺晌,我同意世吨。那么我們試試把這個方法改個名字 findDescLike ,果然不好用了呻征。為什么呢耘婚?這套神奇的療法的背后還是那個我們在第一篇時提到的 Convention over configuration,要神奇的療效就得遵循Spring的配方陆赋。這個配方就是方法的命名是有講究的:Spring提供了一套可以通過命名規(guī)則進行查詢構(gòu)建的機制沐祷。這套機制會把方法名首先過濾一些關(guān)鍵字,比如 find…By, read…By, query…By, count…Byget…By 攒岛。系統(tǒng)會根據(jù)關(guān)鍵字將命名解析成2個子語句赖临,第一個 By 是區(qū)分這兩個子語句的關(guān)鍵詞。這個 By 之前的子語句是查詢子語句(指明返回要查詢的對象)灾锯,后面的部分是條件子語句兢榨。如果直接就是 findBy… 返回的就是定義Respository時指定的領(lǐng)域?qū)ο蠹希ū纠械腡odo組成的集合)。

一般到這里顺饮,有的同學(xué)可能會問 find…By, read…By, query…By, get…By 到底有什么區(qū)別俺炒稀?答案是兼雄。吟逝。。木有區(qū)別君旦,就是別名澎办,從下面的定義可以看到這幾個東東其實生成的查詢是一樣的,這種讓你不用查文檔都可以寫對的方式也比較貼近目前流行的自然語言描述風(fēng)格(類似各種DSL)金砍。

private static final String QUERY_PATTERN = "find|read|get|query|stream";

剛剛我們實驗了模糊查詢局蚀,那如果要是精確查找怎么做呢,比如我們要篩選出已完成或未完成的Todo恕稠,也很簡單:

  List<Todo> findByCompleted(@Param("completed") boolean completed);

嵌套對象的查詢怎么搞琅绅?

看到這里你會問,這都是簡單類型鹅巍,如果復(fù)雜類型怎么辦千扶?嗯料祠,好的,我們還是增加一個需求看一下:現(xiàn)在需求是要這個API是多用戶的澎羞,每個用戶看到的Todo都是他們自己創(chuàng)建的項目髓绽。我們新建一個User領(lǐng)域?qū)ο螅?/p>

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ù),我們需要一個User的REST API妆绞,所以添加一個 UserRepository

package dev.local.user;

import org.springframework.data.mongodb.repository.MongoRepository;

public interface UserRepository extends MongoRepository<User, String> {
}

然后給 Todo 領(lǐng)域?qū)ο筇砑右粋€User屬性顺呕。

package dev.local.todo;
//省略import部分
public class Todo {
    //省略其他部分
    private User user;

    public User getUser() {
        return user;
    }

    public void setUser(User user) {
        this.user = user;
    }
}

接下來就可以來把 TodoRepository 添加一個方法定義了,我們先實驗一個簡單點的括饶,根據(jù)用戶的email來篩選出這個用戶的Todo列表:

public interface TodoRepository extends MongoRepository<Todo, String>{
    List<Todo> findByUserEmail(@Param("userEmail") String userEmail);
}

現(xiàn)在需要構(gòu)造一些數(shù)據(jù)了株茶,你可以通過剛剛我們建立的API使用Postman工具來構(gòu)造:我們這里創(chuàng)建了2個用戶,以及一些Todo項目图焰,分別屬于這兩個用戶启盛,而且有部分項目的描述是一樣的。接下來就可以實驗一下了技羔,我們在瀏覽器中輸入 http://localhost:8080/todos/search/findByUserEmail?userEmail=peng@gmail.com 僵闯,我們會發(fā)現(xiàn)返回的結(jié)果中只有這個用戶的Todo項目。

{
  "_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é)果后我們來分析這個 findByUserEmail 是如何解析的:首先在 By 之后堕阔,解析器會按照 camel (每個單詞首字母大寫)的規(guī)則來分詞棍厂。那么第一個詞是 User,這個屬性在 Todo 中有沒有呢超陆?有的牺弹,但是這個屬性是另一個對象類型,所以緊跟著這個詞的 Email 就要在 User 類中去查找是否有 Email 這個屬性时呀。聰明如你张漂,肯定會想到,那如果在 Todo 類中如果還有一個屬性叫 userEmail 怎么辦谨娜?是的航攒,這種情況下 userEmail 會被優(yōu)先匹配,此時請使用 _ 來顯性分詞處理這種混淆趴梢。也就是說如果我們的 Todo 類中同時有 useruserEmail 兩個屬性的情況下漠畜,我們?nèi)绻胍付ǖ氖?useremail ,那么需要寫成 findByUser_Email坞靶。

還有一個問題憔狞,我估計很多同學(xué)現(xiàn)在已經(jīng)在想了,那就是我們的這個例子中并沒有使用 userid彰阴,這不科學(xué)啊瘾敢。是的,之所以沒有在上面使用 findByUserId 是因為要引出一個易錯的地方,下面我們來試試看簇抵,將 TodoRepository 的方法改成

public interface TodoRepository extends MongoRepository<Todo, String>{
    List<Todo> findByUserId(@Param("userId") String userId);
}

你如果打開瀏覽器輸入 http://localhost:8080/todos/search/findByUserId?userId=589089c3c5d0e2524e245458 (這里的Id請改成你自己mongodb中的user的id)庆杜,你會發(fā)現(xiàn)返回的結(jié)果是個空數(shù)組。原因是雖然我們在類中標(biāo)識 idString 類型碟摆,但對于這種數(shù)據(jù)庫自己生成維護的字段虑省,它在MongoDB中的類型是ObjectId带饱,所以在我們的接口定義的查詢函數(shù)中應(yīng)該標(biāo)識這個參數(shù)是 ObjectId畏陕。那么我們只需要改動 userId 的類型為 org.bson.types.ObjectId 即可视卢。

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);
}

再復(fù)雜一些行不行?

好吧嘉裤,到現(xiàn)在我估計還有一大波攻城獅表示不服,實際開發(fā)中需要的查詢比上面的要復(fù)雜的多栖博,再復(fù)雜一些怎么辦屑宠?還是用例子來說話吧,那么現(xiàn)在我們想要模糊搜索指定用戶的Todo中描述的關(guān)鍵字仇让,返回匹配的集合典奉。這個需求我們只需改動一行,這個以命名規(guī)則為基礎(chǔ)的查詢條件是可以加 And丧叽、Or 這種關(guān)聯(lián)多個條件的關(guān)鍵字的卫玖。

List<Todo> findByUserIdAndDescLike(@Param("userId") ObjectId userId, @Param("desc") String desc);

當(dāng)然,還有其他操作符:Between (值在兩者之間), LessThan (小于), GreaterThan (大于), Like (包含), IgnoreCase (b忽略大小寫), AllIgnoreCase (對于多個參數(shù)全部忽略大小寫), OrderBy (引導(dǎo)排序子語句), Asc (升序踊淳,僅在 OrderBy 后有效) 和 Desc (降序假瞬,僅在 OrderBy 后有效)。

剛剛我們談到的都是對于查詢條件子語句的構(gòu)建迂尝,其實在 By 之前脱茉,對于要查詢的對象也可以有限定的修飾詞 Distinct (去重,如有重復(fù)取一個值)垄开。比如有可能返回的結(jié)果有重復(fù)的記錄琴许,可以使用 findDistinctTodoByUserIdAndDescLike

我可以直接寫查詢語句嗎溉躲?幾乎所有碼農(nóng)都會問的問題榜田。當(dāng)然可以咯,也是同樣簡單锻梳,就是給方法加上一個元數(shù)據(jù)修飾符 @Query

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的查詢進行。上面的例子中有幾個地方需要說明一下

  1. ?0?1 是參數(shù)的占位符唱蒸,?0 表示第一個參數(shù)邦鲫,也就是 userId?1 表示第二個參數(shù)也就是 desc
  2. 使用user._id 而不是 user.id 是因為所有被 @Id 修飾的屬性在Spring Data中都會被轉(zhuǎn)換成 _id
  3. MongoDB中沒有關(guān)系型數(shù)據(jù)庫的Like關(guān)鍵字庆捺,需要以正則表達式的方式達成類似的功能古今。

其實,這種支持的力度已經(jīng)可以讓我們寫出相對較復(fù)雜的查詢了滔以。但肯定還是不夠的捉腥,對于開發(fā)人員來講,如果不給可以自定義的方式基本沒人會用的你画,因為總有這樣那樣的原因會導(dǎo)致我們希望能完全掌控我們的查詢或存儲過程抵碟。但這個話題展開感覺就內(nèi)容更多了,后面再講吧坏匪。

本章代碼:https://github.com/wpcfan/spring-boot-tut/tree/chap02

重拾后端之Spring Boot(一):REST API的搭建可以這樣簡單
重拾后端之Spring Boot(二):MongoDb的無縫集成
重拾后端之Spring Boot(三):找回熟悉的Controller拟逮,Service
重拾后端之Spring Boot(四):使用 JWT 和 Spring Security 保護 REST API
重拾后端之Spring Boot(五):跨域、自定義查詢及分頁

有問題的童鞋可以加入我的小密圈討論: http://t.xiaomiquan.com/jayRnaQ (該鏈接7天內(nèi)(5月14日前)有效)

另外适滓,我的 《Angular 從零到一》出版了敦迄,下面是書籍的內(nèi)容簡介:

本書系統(tǒng)介紹Angular的基礎(chǔ)知識與開發(fā)技巧,可幫助前端開發(fā)者快速入門凭迹。共有9章罚屋,第1章介紹Angular的基本概念,第2~7章從零開始搭建一個待辦事項應(yīng)用嗅绸,然后逐步增加功能脾猛,如增加登錄驗證、將應(yīng)用模塊化鱼鸠、多用戶版本的實現(xiàn)猛拴、使用第三方樣式庫、動態(tài)效果制作等瞧柔。第8章介紹響應(yīng)式編程的概念和Rx在Angular中的應(yīng)用漆弄。第9章介紹在React中非常流行的Redux狀態(tài)管理機制,這種機制的引入可以讓代碼和邏輯隔離得更好造锅,在團隊工作中強烈建議采用這種方案撼唾。本書不僅講解Angular的基本概念和最佳實踐,而且分享了作者解決問題的過程和邏輯哥蔚,講解細膩倒谷,風(fēng)趣幽默,適合有面向?qū)ο缶幊袒A(chǔ)的讀者閱讀糙箍。

歡迎大家圍觀渤愁、訂購、提出寶貴意見深夯。

京東鏈接:https://item.m.jd.com/product/12059091.html?from=singlemessage&isappinstalled=0

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末抖格,一起剝皮案震驚了整個濱河市诺苹,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌雹拄,老刑警劉巖收奔,帶你破解...
    沈念sama閱讀 207,248評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異滓玖,居然都是意外死亡坪哄,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,681評論 2 381
  • 文/潘曉璐 我一進店門势篡,熙熙樓的掌柜王于貴愁眉苦臉地迎上來翩肌,“玉大人,你說我怎么就攤上這事禁悠∧罴溃” “怎么了?”我有些...
    開封第一講書人閱讀 153,443評論 0 344
  • 文/不壞的土叔 我叫張陵碍侦,是天一觀的道長棒卷。 經(jīng)常有香客問我,道長祝钢,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,475評論 1 279
  • 正文 為了忘掉前任若厚,我火速辦了婚禮拦英,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘测秸。我一直安慰自己疤估,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 64,458評論 5 374
  • 文/花漫 我一把揭開白布霎冯。 她就那樣靜靜地躺著铃拇,像睡著了一般。 火紅的嫁衣襯著肌膚如雪沈撞。 梳的紋絲不亂的頭發(fā)上慷荔,一...
    開封第一講書人閱讀 49,185評論 1 284
  • 那天,我揣著相機與錄音缠俺,去河邊找鬼显晶。 笑死,一個胖子當(dāng)著我的面吹牛壹士,可吹牛的內(nèi)容都是我干的磷雇。 我是一名探鬼主播,決...
    沈念sama閱讀 38,451評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼躏救,長吁一口氣:“原來是場噩夢啊……” “哼唯笙!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,112評論 0 261
  • 序言:老撾萬榮一對情侶失蹤崩掘,失蹤者是張志新(化名)和其女友劉穎七嫌,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體呢堰,經(jīng)...
    沈念sama閱讀 43,609評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡抄瑟,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,083評論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了枉疼。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片皮假。...
    茶點故事閱讀 38,163評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖骂维,靈堂內(nèi)的尸體忽然破棺而出惹资,到底是詐尸還是另有隱情,我是刑警寧澤航闺,帶...
    沈念sama閱讀 33,803評論 4 323
  • 正文 年R本政府宣布褪测,位于F島的核電站,受9級特大地震影響潦刃,放射性物質(zhì)發(fā)生泄漏侮措。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,357評論 3 307
  • 文/蒙蒙 一乖杠、第九天 我趴在偏房一處隱蔽的房頂上張望分扎。 院中可真熱鬧,春花似錦胧洒、人聲如沸畏吓。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,357評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽菲饼。三九已至,卻和暖如春列赎,著一層夾襖步出監(jiān)牢的瞬間宏悦,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,590評論 1 261
  • 我被黑心中介騙來泰國打工包吝, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留肛根,地道東北人。 一個月前我還...
    沈念sama閱讀 45,636評論 2 355
  • 正文 我出身青樓漏策,卻偏偏與公主長得像派哲,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子掺喻,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,925評論 2 344

推薦閱讀更多精彩內(nèi)容