SpringBoot實戰分析-MongoDB操做

前言

MongoDB做爲一個基於分佈式文件存儲的數據庫,在微服務領域中普遍使用.本篇文章將學習 Spring Boot 程序如何執行 MongoDB 操做以及底層實現方式的源碼分析,來更好地幫助咱們理解Spring程序操做 MongoDB 數據庫的行爲.如下兩點是源碼分析的收穫,讓咱們一塊兒來看下這些是怎麼發現的吧.java

  • Spring 框架操做MongoDB 數據的底層使用的 MongoDBTemplate, 而實際使用時經過JDK 動態代理和 AOP 攔截器方式層層調用.
  • 在本身的DAO對象中自定義查詢方法是要符合spring-boot-data-mongodb框架的方法命名規則,才能達到徹底自動處理的效果.

正文

本文使用 MongoDB 服務器版本爲4.0.0git

MongoDB 服務器的安裝能夠參考個人另外一篇博客:後端架構搭建系列之MonogDBgithub

下載示例工程

首先在SPRING INITIALIZR網站上下載示例工程,Spring Boot 版本爲1.5.17,僅依賴一個 MongoDB.spring

image-20181020201332247

用 IDE 導入工程後打開POM 文件,就能夠看到 MongoDB 依賴對應的Maven 座標和對應第三方庫爲 spring-boot-starter-data-mongodbmongodb

<dependency>
	<groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-mongodb</artifactId>
</dependency>
複製代碼

那之後咱們要在Spring Boot項目使用 MongoDB 時就能夠在主 POM文件中引入這個庫的座標就OK 了.數據庫

spring-boot-starter-data-mongodbspring-data的子項目, 其做用就是針對 MongoDB 的訪問提供豐富的操做和簡化.後端

配置 MongoDB鏈接

要操做 MongoDB 數據庫, 首先要讓程序鏈接到 MongoDB 服務器,因爲 Spring Boot 強大的簡化配置特性, 想要鏈接 MongoDB 服務器, 咱們只需在資源文件夾下的 application.properties文件裏新增一行配置便可.springboot

spring.data.mongodb.uri=mongodb://localhost:27017/test
複製代碼

若是鏈接有用戶驗證的 MongoDB 服務器,則uri 形式爲 mongodb://name:password@ip:port/dbName服務器

編寫代碼

配置後以後,接下來咱們先建立一個實體 Post, 包含屬性: id,title,content,createTimemarkdown

public class Post {
    @Id
    private Long id;
    private String title;
    private String content;
    private Date createTime;

    public Post() {
    }

    public Post(Long id, String title, String content) {
        this.id = id;
        this.title = title;
        this.content = content;
        this.createTime = new Date();
    }
    // 省略 setter,getter 方法
}
複製代碼

這裏用 註解@Id表示該實體屬性對應爲數據庫記錄的主鍵.

而後再提供對Post的數據訪問的存儲對象 PostRepository, 繼承 官方提供的MongoRepository接口

public interface PostRepository extends MongoRepository<Post,Long> {
    void findByTitle(String title);
}
複製代碼

到這裏 對 Post 實體的 CRUD 操做代碼就完成. What !!! 咱們還沒寫什麼代碼就結束了麼? 咱們如今就來寫個測試用例來看看吧.

@RunWith(SpringRunner.class)
@SpringBootTest
public class SpringbootMongodbApplicationTests {
	@Autowired
	private PostRepository postRepository;

	@Test
	public void testInsert() {
		Post post = new Post(1L,"sayhi", "hi,mongodb");
		postRepository.insert(post);
		List<Post> all = postRepository.findAll();
		System.out.println(all); 
        // [Post{id=1, title='sayhi', content='hi,mongodb',
        //createTime=Sat Oct 20 20:55:15 CST 2018}]
		Assert.assertEquals(all.size(),1); // true
	}
}
複製代碼

運行測試用例,運行結果以下, Post數據成功地存儲到了 MongoDB 數據庫中,而且可以查詢出來了.

image-20181020205843907

咱們也能夠在MongoDB 服務器裏查到這條記錄:

image-20181020210805972

從記錄中看到多了個 _ class字段 的值,實際上是由 MongoRepository自動幫咱們設置的,用來表示這條記錄對應的實體類型,但底層是何時操做的呢,期待在咱們後續分析的時候揭曉答案.

新增以後,咱們再嘗試下更新操做,這裏用的也是用繼承而來的 save 方法,除此以外咱們還使用了本身寫的接口方法 findByTitle來根據 title字段查詢出 Post 實體.

@Test
public void testUpdate() {
    Post post = new Post();
    post.setId(1L);
    post.setTitle("sayHi");
    post.setContent("hi,springboot");
    post.setCreateTime(new Date());
    postRepository.save(post); // 更新 post 對象
    Post updatedPost = postRepository.findByTitle("sayHi"); // 根據 title 查詢
    Assert.assertEquals(updatedPost.getId(),post.getId());
    Assert.assertEquals(updatedPost.getTitle(),post.getTitle());
    Assert.assertEquals(updatedPost.getContent(),"hi,springboot");
}
複製代碼

運行這個測試用例,結果也是經過.但這裏也有個疑問: 本身提供的方法,沒有寫如何實現,程序怎麼就能依照咱們所想要的:根據title字段的值去查詢到匹配到的記錄呢 ? 這樣也在下面實戰分析裏看個明白吧.

image-20181020212654792

到這裏咱們對數據的增,改,查都已經試過了,刪除其實也很簡單,只要調用 postRepositorydelete 方法便可,如今最主要仍是探究 PostRepository僅經過繼承MongoRepository如何實現數據增刪改查的呢?

實戰分析

postRepository的執行底層

實現了基本的數據操做以後,咱們如今就來看下這一切是怎麼作到的呢? 首先咱們對測試用例 testUpdate 中的postRepository#save 進行斷點調試,觀察程序的執行路徑.在單步進入 save 方法內部,代碼執行到了JdkDynamicAopProxy類型下, 此時代碼調用鏈以下圖所示

image-20181020215752763

很顯然這裏是用到 SpringJDK 動態代理,而invoke方法內這個 proxy對象十分引人注意, 方法執行時實際調用的 proxysave 方法,而這個 proxy 則是 org.springframework.data.mongodb.repository.support.SimpleMongoRepository@8deb645

, 是 SimpleMongoRepository 類的實例.那麼最後調用就會落到SimpleMongoRepository# save 方法中,咱們在這個方法裏再次進行斷點而後繼續運行.

image-20181020220728719

從這裏能夠看出,save 方法內部有兩個操做: 若是是傳入的實體是新紀錄則執行 insert,不然執行 save更新操做.顯然如今要執行的是後者.

而要完成操做跟兩個對象 entityInformationmongoOperations有着密切關係,他們又是幹什麼的呢,何時初始化的呢.

image-20181020221604857

首先咱們看下 mongoOperations這個對象,利用IDEA 調試工具能夠看到 mongoOperations 其實就是 MongoTemplate對象, 相似 JDBCTemplate,針對MongoDB 數據的增刪改查, Spring 也採用類似的名稱方式和 API.因此真正操做MongoDB數據庫底層就是這個MongoTemplate對象.

至於entityInformation對象所屬的類 MappingMongoEntityInformation,存儲着Mongo數據實體信息,如集合名稱,主鍵類型,一些所映射的實體元數據等.

再來看下他們的初始化時機,在SimpleMongoRepository類, 能夠找到他們都在的構造方法中初始化

public SimpleMongoRepository(MongoEntityInformation<T, ID> metadata, MongoOperations mongoOperations) {
	Assert.notNull(metadata, "MongoEntityInformation must not be null!");
	Assert.notNull(mongoOperations, "MongoOperations must not be null!");
    
	this.entityInformation = metadata;
	this.mongoOperations = mongoOperations;
}
複製代碼

以一樣的方式,在SimpleMongoRepository構造器中進行斷點,從新容許觀察初始化 SimpleMongoRepository對象時的調用鏈.發現整個鏈路以下,從運行測試用例到這裏很長的執行鏈路,這裏只標識出了咱們所須要關注的那些類和方法.

image-20181020224041366

從一層層源碼能夠跟蹤到 SimpleMongoRepository 類的建立和初始化是由 工廠類MongoRepositoryFactory完成,

public <T> T getRepository(Class<T> repositoryInterface, Object customImplementation) {

		RepositoryMetadata metadata = getRepositoryMetadata(repositoryInterface);
		Class<?> customImplementationClass = null == customImplementation ? null : customImplementation.getClass();
		RepositoryInformation information = getRepositoryInformation(metadata, customImplementationClass);

		validate(information, customImplementation);

		Object target = getTargetRepository(information); // 獲取初始化後的SimpleMongoRepository對象.

		// Create proxy
		ProxyFactory result = new ProxyFactory();
		result.setTarget(target);
		result.setInterfaces(new Class[] { repositoryInterface, Repository.class }); 
    	// 對 repositoryInterface接口類進行 AOP 代理

		result.addAdvice(SurroundingTransactionDetectorMethodInterceptor.INSTANCE);
		result.addAdvisor(ExposeInvocationInterceptor.ADVISOR);
    

		return (T) result.getProxy(classLoader);
}
複製代碼

下圖就是MongoRepositoryFactory的類圖,而MongoRepositoryFactory又是在MongoRepositoryFactoryBean類裏構造的.

image-20181020224811986

在調用鏈的下半截裏,咱們再看下發生着一切的來源在哪, 找到 org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory#createBean方法,內部建立Bean 實例的 doCreateBean調用參數爲postRepositoryMongoRepositoryFactoryBean實例,也就是在建立postRepository實例的時候完成的.

image-20181020230418830

而建立postRepository對應實體對象實際爲 MongoRepositoryFactoryBean這個工廠 Bean

image-20181020232644057

當須要使用 postRepository對象時,實際就是使用工廠對象的方法MongoRepositoryFactoryBean#getObject返回的 SimpleMongoRepository對象,詳見當類AbstractBeanFactorydoGetBean方法,當參數 namepostRepository時代碼調用鏈.

image-20181021000306776

好了,到這裏基本說完 postRepository是如何完成MongoDB數據庫操做的,還有個問題就是僅定義了接口方法 findByTitle,如何實現根據 title 字段查找的.

findByTitle的查找實現

斷點到執行 findByTitle 方法的地方,調試進去跟以前同樣在 JdkDynamicAopProxy 類中執行,而在獲取調用鏈時

,這個代理對象的所擁有的攔截器中一個攔截器類org.springframework.data.repository.core.support.RepositoryFactorySupport.QueryExecutorMethodInterceptor引發了個人注意.從命名上看是專門處理查詢方法的攔截器.我嘗試在這個攔截的invoke方法進行斷點,果真執行findByTitle時,程序執行到了這裏.

image-20181021085124017

而後在攔截器方法中判斷該方法是否爲查詢方法,若是是就會攜帶參數調用 PartTreeMongoQuery對象繼承而來的AbstractMongoQuery#execute方法.

// AbstractMongoQuery
public Object execute(Object[] parameters) {
    MongoParameterAccessor accessor = new MongoParametersParameterAccessor(method, parameters);
    // 構建查詢對象 Query: { "title" : "sayHi"}, Fields: null, Sort: null
    Query query = createQuery(new ConvertingParameterAccessor(operations.getConverter(), accessor));

    applyQueryMetaAttributesWhenPresent(query);
    ResultProcessor processor = method.getResultProcessor().withDynamicProjection(accessor);
    String collection = method.getEntityInformation().getCollectionName();
    // 構建查詢執行對象
    MongoQueryExecution execution = getExecution(query, accessor,new ResultProcessingConverter(processor, operations, instantiators));
    
    return execution.execute(query, processor.getReturnedType().getDomainType(), collection);
}
複製代碼

MongoQueryExecution#execute方法裏通過層層地調用實際執行而如下代碼:

// AbstractMongoQuery#execute =>
	// MongoQueryExecution.ResultProcessingExecution#execute =>
		// MongoQueryExecution.SingleEntityExecution#execute
@Override
public Object execute(Query query, Class<?> type, String collection) {
	return operations.findOne(query, type, collection);
}
複製代碼

這裏的 operations 就是咱們以前提到的 MongoDBTemplate 實例.因此當執行 自定義方法findByTitile查詢時底層調用的仍是MongoDBTemplate#findOne.

而這裏也有個疑問:構建Query 對象時能獲取到參數值爲sayHi,如何是獲取對應查詢字段爲title的呢?

在方法createQuery是一個模板方法,真正執行在``PartTreeMongoQuery`類上.

@Override
protected Query createQuery(ConvertingParameterAccessor accessor) {
	MongoQueryCreator creator = new MongoQueryCreator(tree, accessor, context, isGeoNearQuery);
	Query query = creator.createQuery();
	//...
	return query
}
複製代碼

這裏在構建MongoQueryCreator時有個 tree 屬性,這個對象就是構建條件查詢的關係.

image-20181021092719052

tree 對象的初始化在PartTreeMongoQuery這個類的構造器中完成的, 根據方法名, PartTree又是如何構造出來的呢.

//PartTree.java
public PartTree(String source, Class<?> domainClass) {

    Assert.notNull(source, "Source must not be null");
    Assert.notNull(domainClass, "Domain class must not be null");

    Matcher matcher = PREFIX_TEMPLATE.matcher(source);
    if (!matcher.find()) {
        this.subject = new Subject(null);
        this.predicate = new Predicate(source, domainClass);
    } else {
        this.subject = new Subject(matcher.group(0));
        // 構造查詢字段的關鍵
        this.predicate = new Predicate(source.substring(matcher.group().length()), domainClass);
    }
}
複製代碼

從上面代碼能夠看到 , 用正則方式匹配方法名,其中 PREFIX_TEMPLATE表示着 ^(find|read|get|query|stream|count|exists|delete|remove)((\p{Lu}.*?))??By, 若是匹配到了就將 By 後面緊跟的單詞提取出來,內部再根據該名稱去匹配對應類的屬性,找到構建完成後就會放在一個 ArrayList 集合裏存放,等待後續查詢的時候使用.

因此也能夠看出 咱們自定義的方法 findByTitle符合框架默認的正則要求,因此能自動提取到Posttitle 字段做爲查詢字段. 除此以外,使用相似queryBy,getBy等等也能夠達到一樣效果, 這裏體現的就是 Spring Framework 約定因爲配置的思想, 若是咱們隨意定義方法名,那框架就沒法直接識別出查詢字段了.

好了到這裏, 咱們再次總結一下源碼分析成果:

  • 定義postRepository實現MongoRepository接口,操做MongoDB 數據的底層使用的 MongoDBTemplate, 而實際使用時經過JDK 動態代理和 AOP 攔截器方式層層調用.
  • postRepository中自定義查詢方法是要符合spring-boot-data-mongodb框架的方法命名規則,才能達到徹底自動處理的效果.

結語

到這裏,咱們的 Spring BootMongoDB 的實戰分析就結束了,細看內部源碼,雖然結構層次清晰,但因爲模塊間複雜調用關係,也每每容易迷失於源碼中,這時候耐心和明確的目標就相當重要.這算也是本次源碼分析的收穫吧,但願這篇文章能有更多收穫,咱們下篇再見吧.😁😁😁

參考

相關文章
相關標籤/搜索