Spring Data JPA從入門到精通(第二部分)


Spring Data JPA從入門到精通(第一部分)
Spring Data JPA從入門到精通(第二部分)
Spring Data JPA從入門到精通(第三部分)


所有代碼均源自spring-data#2.2.6版本

這一部分由于篇幅較少,看完還有很多疑問,就按照自己的思路重新整理了下.

一.整體認(rèn)識JPA

推薦:

什么是JPA

JPA的根本目標(biāo)是實(shí)現(xiàn)將面向?qū)ο蟮拇鎯?chǔ)與底層所提供的持久化機(jī)構(gòu)解耦椒惨;也就是說,從面向?qū)ο蟪绦蜷_發(fā)者的角度來說,我不需要知道你底層的數(shù)據(jù)庫是什么,Oracle也好、MySQL也好谭企、DB2也好淹办,我不關(guān)心, 我只需要你(JPA)幫我將我需要的對象數(shù)據(jù)(Object)持久化(寫入存儲(chǔ))或反序列化(讀出存儲(chǔ))即可。

JPA 只關(guān)注與關(guān)系型數(shù)據(jù)庫, 非關(guān)系型(如Redis,MongoDB)則由Spring Data 來定義.

Spring的視野下幾個(gè)Lib的關(guān)系:

image.png

可以看出Spring-Data-Jpa作為Spring-Data的子項(xiàng)目,把工作職責(zé)限定在關(guān)系型DB,它依賴于Spring-Data-Commons和Spring-orm,而orm又依賴于Hibernate實(shí)現(xiàn).換句話說,也就是 Commons 比較超然,并不會(huì)和ORM模式直接關(guān)聯(lián).

二. JPA的 Spring Boot自動(dòng)裝配

  • 自動(dòng)裝備

Spring-Boot-JPA支持自動(dòng)裝配,只需要在dependency中加上spring-boot-starter-data-jpa,而無需任何配置,即可自動(dòng)識別@Entity和@Repository(甚至這個(gè)都可以沒有),把倉庫組裝好.

SpringBoot 中的 META-INF/spring.factories(完整路徑:spring-boot/spring-boot-autoconfigure/src/main/resources/META-INF/spring.factories)中關(guān)于 EnableAutoConfiguration 的這段配置如下 :

org.springframework.boot.autoconfigure.admin.SpringApplicationAdminJmxAutoConfiguration,\
org.springframework.boot.autoconfigure.aop.AopAutoConfiguration,\
org.springframework.boot.autoconfigure.data.jpa.JpaRepositoriesAutoConfiguration,\
org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration

可以發(fā)現(xiàn) JpaRepositoriesAutoConfiguration 和 HibernateJpaAutoConfiguration 幫我們裝置了 JPA 相關(guān)的配置焚志。

  1. 在@SpringBootApplication中就包含@EnableAutoConfiguration,而它就包含@Import(AutoConfigurationImportSelector.class)
  2. 在AbstractApplicationContext#invokeBeanFactoryPostProcessors()方法中會(huì)調(diào)用AutoConfigurationImportSelector#process(),它會(huì)遍歷所有jar中的META-INF/spring.factories.
  3. 在getCandidateConfigurations()方法中,就會(huì)找到spring-boot-autoconfigure-××.jar中配置的org.springframework.boot.autoconfigure.EnableAutoConfiguration一節(jié)內(nèi)容,變成List<String> configurations.這個(gè)list包含了我們關(guān)心的JdbcRepositoriesAutoConfiguration和JpaRepositoriesAutoConfiguration
  4. 看看代碼
// 默認(rèn)的proxyBeanMethods模式是false
@Configuration(proxyBeanMethods模式是false = false)
// 需要配置JDBC先
@ConditionalOnBean(DataSource.class)
@ConditionalOnClass(JpaRepository.class)
@ConditionalOnMissingBean({ JpaRepositoryFactoryBean.class, JpaRepositoryConfigExtension.class })
// 留個(gè)口子,即時(shí)加了Spring-data-jpa的依賴,也可以通過配置關(guān)閉,默認(rèn)是開啟的.
@ConditionalOnProperty(prefix = "spring.data.jpa.repositories", name = "enabled", havingValue = "true",
        matchIfMissing = true)
// 這個(gè)是核心注解      
@Import(JpaRepositoriesRegistrar.class)
@AutoConfigureAfter({ HibernateJpaAutoConfiguration.class, TaskExecutionAutoConfiguration.class })
public class JpaRepositoriesAutoConfiguration {

    @Bean
    @Conditional(BootstrapExecutorCondition.class)
    public EntityManagerFactoryBuilderCustomizer entityManagerFactoryBootstrapExecutorCustomizer(
            Map<String, AsyncTaskExecutor> taskExecutors) {
        ...
    }
}

我們發(fā)現(xiàn),這里又Import了另外一個(gè)配置JpaRepositoriesRegistrar

  1. JpaRepositoriesRegistrar從名字我們就可以看出,他是一個(gè)配置注冊器,其擴(kuò)展了AbstractRepositoryConfigurationSourceSupport,在這個(gè)類可以看到熟悉的registerBeanDefinitions方法.

而且在JpaRepositoriesRegistrar還有個(gè)內(nèi)部靜態(tài)Class,它@EnableJpaRepositories,spring-boot把這個(gè)藏在這里了,因此我們不需要自己去Enable

  • 解析用戶倉庫接口

既然找到了注冊器,那么接下來就來看看如果解析并生成倉庫類,也就是JPA最牛的地方,我們只需要定義接口,其他由JPA替我們實(shí)現(xiàn).

  1. AbstractRepositoryConfigurationSourceSupport#registerBeanDefinitions()方法就是入口了.
  2. 跟蹤代碼進(jìn)入RepositoryConfigurationDelegate#registerRepositoriesIn(),其中
Collection<RepositoryConfiguration<RepositoryConfigurationSource>> configurations = extension
                .getRepositoryConfigurations(configurationSource, resourceLoader, inMultiStoreMode);

這句會(huì)找到我們寫的接口(extends JpaRepository<>)

圖:

  1. 找到了列表,就開始逐個(gè)遍歷,這里通過BeanDefinitionBuilder構(gòu)造出org.springframework.data.jpa.repository.support.JpaRepositoryFactoryBean,并注冊到registry,也就是DefaultListableBeanFactory中去.這樣我們的BeanFactory也知道這個(gè)倉庫接口了.
  • 創(chuàng)建接口實(shí)現(xiàn)

  1. refresh()方法的finishBeanFactoryInitialization()中,創(chuàng)建JpaRepositoryFactoryBean的實(shí)例.
  2. AbstractBeanFactory#doGetBean()創(chuàng)建beanName為"userRepository"的實(shí)例
                // Create bean instance.
                if (mbd.isSingleton()) {
                    // 我們的Repository都是單例的
                    sharedInstance = getSingleton(beanName, () -> {
                        try {
                            // 這里構(gòu)造Bean
                            return createBean(beanName, mbd, args);
                        }
                        catch (BeansException ex) {
                            // Explicitly remove instance from singleton cache: It might have been put there
                            // eagerly by the creation process, to allow for circular reference resolution.
                            // Also remove any beans that received a temporary reference to the bean.
                            destroySingleton(beanName);
                            throw ex;
                        }
                    });
                    bean = getObjectForBeanInstance(sharedInstance, name, beanName, mbd);
                }

這里就會(huì)創(chuàng)建出這個(gè)工廠factory,在工廠bean創(chuàng)建后的afterPropertiesSet,會(huì)調(diào)用

this.repository = Lazy.of(() -> this.factory.getRepository(repositoryInterface, repositoryFragmentsToUse));

此時(shí),使用工廠方法根據(jù)我們定義的接口,創(chuàng)建出Repository代理類.之后再調(diào)用get()方法創(chuàng)建出實(shí)例.

注意:創(chuàng)建代理類使用的是RepositoryFactorySupport#getRepository()方法,其中QueryExecutorMethodInterceptor負(fù)責(zé)拆開我們自定義的方法如(findByName),如果名字有錯(cuò)誤,那么在

new QueryExecutorMethodInterceptor(information, projectionFactory)

this.queries = lookupStrategy //
    .map(it -> mapMethodsToQuery(repositoryInformation, it, projectionFactory)) //
    .orElse(Collections.emptyMap());

時(shí)就會(huì)出錯(cuò),這里用了好多l(xiāng)ambda表達(dá)式.繼續(xù)跟蹤,發(fā)現(xiàn)是JpaQueryMethod負(fù)責(zé),構(gòu)造JpaQueryMethod后,調(diào)用JpaQueryLookupStrategy#resolveQuery()解析.

        @Override
        protected RepositoryQuery resolveQuery(JpaQueryMethod method, EntityManager em, NamedQueries namedQueries) {

            RepositoryQuery query = JpaQueryFactory.INSTANCE.fromQueryAnnotation(method, em, evaluationContextProvider);

            if (null != query) {
                return query;
            }

            query = JpaQueryFactory.INSTANCE.fromProcedureAnnotation(method, em);

            if (null != query) {
                return query;
            }
            // 這個(gè)name就是解析出的參數(shù)名了,例如findByName,就會(huì)解析出name
            String name = method.getNamedQueryName();
            if (namedQueries.hasQuery(name)) {
                return JpaQueryFactory.INSTANCE.fromMethodWithQueryString(method, em, namedQueries.getQuery(name),
                        evaluationContextProvider);
            }

            query = NamedQuery.lookupFrom(method, em);

            if (null != query) {
                return query;
            }
            // 發(fā)現(xiàn)name找不到具體對應(yīng)的屬性,這里拋出異常
            throw new IllegalStateException(
                    String.format("Did neither find a NamedQuery nor an annotated query for method %s!", method));
        }

也就是說,它是在啟動(dòng)時(shí)將接口方法解析為HQL,如果接口方法參數(shù)有問題,在"啟動(dòng)"階段而非"編譯"階段會(huì)發(fā)現(xiàn)錯(cuò)誤.

最重要的圖3-3

image.png

推薦閱讀: CSDN---Spring-data-jpa 圖文課

Spring Data JPA源碼分析-方法命名查詢

spring data jpa 全面解析(實(shí)踐 + 源碼分析)

三. JPA規(guī)范與接口

JPA接口規(guī)范

從javax.persistence-api-2.2規(guī)范里面可以看出,JPA比起JDBC規(guī)范真是復(fù)雜多了;暫時(shí)沒有實(shí)力去逐個(gè)了解.抓住幾個(gè)重點(diǎn)先,雖然可能并不完全正確,但是有利于理解后面看JPA實(shí)現(xiàn)的源碼:

  • criteria包定義了JPA對于SQL的抽象(如CriteriaQuery代表查詢,CriteriaUpdate代表更新)

    • Expression是表達(dá)式,具體來說,可以代表SQL中Id=1這一小段.
    • Predicate是一組Boolean型的Expression,代表Where后面一整串.
    • Root和Path,Root代表整個(gè)查詢的Entity,也就是表對象,而Path是其中一部分,也就是某一列.
    • CriteriaBuilder是一個(gè)構(gòu)建器,可以方便地構(gòu)造出CriteriaQuery,CriteriaUpdate并且?guī)е髯詷?gòu)建Expression和組合Expresssion的方法.
  • metamodel包定義了JPA對于Object的抽象,類似于Spring的BeanDefinition

    • ManagedType擴(kuò)展了Type,代表了一個(gè)受控的元模型
    • Metamodel是JPA的元模型類,可以根據(jù)Class類型獲得EntityType或managedType
  • spi包定義要實(shí)現(xiàn)SPA需要實(shí)現(xiàn)的一些接口

    • PersistenceProvider規(guī)定了createEntityManagerFactory,generateSchema并能獲得當(dāng)前的ProviderUtil實(shí)例.

頂層包倒是我們比較熟悉的一些注解,如 @Entity, @Id, @OneToMany, @SequenceGenerator等等.

可以看出,JPA的抽象簡直是智力游戲級別的,就是朝著讓人看不懂方向去的,不是Hibernate的專家,應(yīng)該搞不懂為啥要這么去做吧.因此只能大概過一下,有個(gè)概念.

JpaRepository繼承樹與實(shí)現(xiàn)

圖: 繼承樹

image.png

從繼承樹可以看出,QuerydslJpaRepository已被廢棄,而我們需要查看的實(shí)現(xiàn)類是SimpleJpaRepository,那么具體的查詢是如何執(zhí)行的呢,可以分以下兩種情況:

  1. 如果是JPA的標(biāo)準(zhǔn)實(shí)現(xiàn)如findById()這種,就直接調(diào)用使用代理攔截,由SimpleJpaRepository代替接口完成查詢工作.

  2. 如果是自定義的方法,就需要結(jié)合之前解析的內(nèi)容,在resolveQuery后進(jìn),把查詢條件都保存在
    QueryExecutorMethodInterceptor的queries中,它是一個(gè)private final Map<Method, RepositoryQuery>,這樣我們自定義的方法,就可以根據(jù)名字,對應(yīng)到一個(gè)RepositoryQuery上面了.
    之前在構(gòu)造Bean時(shí)候,已經(jīng)解析好了.因而在程序中調(diào)用的時(shí)候,自然就進(jìn)入了JdkDynamicAopProxy#invoke()方法,然后它會(huì)調(diào)用RepositoryFactorySupport#doInvoke()方法.

        @Nullable
        private Object doInvoke(MethodInvocation invocation) throws Throwable {

            Method method = invocation.getMethod();

            if (hasQueryFor(method)) {
                return queries.get(method).execute(invocation.getArguments());
            }

            return invocation.proceed();
        }
        

就可以找到對應(yīng)的PartTreeJpaQuery#execute()進(jìn)行查詢了,這種就沒有過SimpleJpaRepository.

四.Spring-Data-JPA查詢方式的使用

  • @Query(JPQL標(biāo)準(zhǔn))

  • 方法名稱查詢

上面的QueryExecutorMethodInterceptor完成

  • Example查詢(6.2-QueryByExpampleExecutor)

  1. 通過傳入一個(gè)Example樣本S,去查找類似的數(shù)據(jù).當(dāng)然最終也是通過Specification實(shí)現(xiàn).
  2. ExampleMatcher通過相當(dāng)簡單的規(guī)則,對樣本數(shù)據(jù)進(jìn)行篩選條件的限制;包括nullHandler(空值處理),StringMatcher(字符串匹配方式),IgnoreCase(大小寫忽略方式),properSepcifiers(屬性特定查詢方式),ignoredPaths(忽略屬性列表).
  • Specification查詢查詢(6.3-JpaSpecificationExecutor)

  1. Specification核心方法
/**
     * Creates a WHERE clause for a query of the referenced entity in form of a {@link Predicate} for the given
     * {@link Root} and {@link CriteriaQuery}.
     *
     * @param root must not be {@literal null}.
     * @param query must not be {@literal null}.
     * @param criteriaBuilder must not be {@literal null}.
     * @return a {@link Predicate}, may be {@literal null}.
     */
    @Nullable
    Predicate toPredicate(Root<T> root, CriteriaQuery<?> query, CriteriaBuilder criteriaBuilder);

至于怎么用,書里面說的并不詳細(xì),建議看看官方文檔了.

五.Spring-Data-JPA事務(wù)的實(shí)現(xiàn)

JPA實(shí)際也是通過JpaRepositoryFactory實(shí)現(xiàn)的.

六.擴(kuò)展能力

  1. Audit擴(kuò)展

Spring Data JPA為我們提供了審計(jì)功能的架構(gòu)實(shí)現(xiàn),提供了4個(gè)專門的注解:@CreateBy,@CreateDayte,@LastModifiedBy,@LastModifiedDate

具體使用步驟:

一. @Entity增加AutityListener,并增加上述4個(gè)注解
二. 實(shí)現(xiàn)AuditorAware接口,告訴JPA當(dāng)前用戶(推薦統(tǒng)一從Request中取)
三. 通過@EnableJpaAuditing注解開啟JPA的審計(jì)功能

這樣,每次在修改表的同時(shí),也自動(dòng)添加了審計(jì)的信息

  1. Listener
    從審計(jì)的實(shí)現(xiàn)可以看出,他是通過定義事件處理完成的.

JPA提供了CallBack鉤子(這個(gè)好像GORM的實(shí)現(xiàn)),在數(shù)據(jù)庫操作過程可以自定義EntityListener,并且回填entity對象,這樣就能很方便擴(kuò)展.

@Prepersist注解的方法 映凳,完成save之前的操作胆筒。
@Preupdate注解的方法 娘纷,完成update之前的操作锋爪。
@PreRemove注解的方法 ,完成remove之前的操作膳沽。
@Postpersist注解的方法 矫渔,完成save之后的操作彤蔽。
@Postupdate注解的方法 ,完成update之后的操作庙洼。
@PostRemovet注解的方法 顿痪,完成remove之后的操作。

image.png
  1. Version

JPA通過@Version幫我們處理樂觀鎖,只需要增加一個(gè)long類型的version字段,每次save的時(shí)候,JPA都會(huì)幫我們自增該字段.(這樣當(dāng)出現(xiàn)多線程同時(shí)save,就有可能出現(xiàn)樂觀鎖更新失敗的情況)
需要我們自行捕獲ObjectOptimisticLockingFailureException處理.

通過 JpaMetamodelEntityInformation#IsNew()方法

@Override
    public boolean isNew(T entity) {

        if (!versionAttribute.isPresent()
                || versionAttribute.map(Attribute::getJavaType).map(Class::isPrimitive).orElse(false)) {
            return super.isNew(entity);
        }

        BeanWrapper wrapper = new DirectFieldAccessFallbackBeanWrapper(entity);

        return versionAttribute.map(it -> wrapper.getPropertyValue(it.getName()) == null).orElse(true);
    }
    

這里吧@Version的那個(gè)屬性變成了versionAttribute,這里會(huì)判斷一下.

  1. 分頁排序
    這個(gè)是同Spring Web Mvc配合使用的.
  • 通過@EnableSpringDataWebSupport可以自動(dòng)注入Repository對象.---還是顯式注入的好.
  • 通過handlerMethodArgumentResolvers可以實(shí)現(xiàn)分頁和排序.

參考:

https://gitee.com/staticsnull/Y2T6014/blob/master/ssh_note/ssh_ch14/Spring%20Data%20JPA.markdown

https://www.bilibili.com/video/BV1Jf4y1m7YT
https://www.bilibili.com/video/BV1FC4y1W7Zv

http://www.iocoder.cn/Spring-Data-JPA/good-collection/
http://www.dewafer.com/2016/05/09/reading-src-of-spring-data-jpa/
http://www.reibang.com/p/d05ba90d19e7

http://www.dewafer.com/2019/10/12/WHAT-IS-JPA/

https://my.oschina.net/u/2434456/blog/596938

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末油够,一起剝皮案震驚了整個(gè)濱河市蚁袭,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌石咬,老刑警劉巖揩悄,帶你破解...
    沈念sama閱讀 206,126評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異鬼悠,居然都是意外死亡删性,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,254評論 2 382
  • 文/潘曉璐 我一進(jìn)店門焕窝,熙熙樓的掌柜王于貴愁眉苦臉地迎上來蹬挺,“玉大人,你說我怎么就攤上這事袜啃『骨郑” “怎么了?”我有些...
    開封第一講書人閱讀 152,445評論 0 341
  • 文/不壞的土叔 我叫張陵群发,是天一觀的道長晰韵。 經(jīng)常有香客問我,道長熟妓,這世上最難降的妖魔是什么雪猪? 我笑而不...
    開封第一講書人閱讀 55,185評論 1 278
  • 正文 為了忘掉前任,我火速辦了婚禮起愈,結(jié)果婚禮上只恨,老公的妹妹穿的比我還像新娘。我一直安慰自己抬虽,他們只是感情好官觅,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,178評論 5 371
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著阐污,像睡著了一般休涤。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上笛辟,一...
    開封第一講書人閱讀 48,970評論 1 284
  • 那天功氨,我揣著相機(jī)與錄音,去河邊找鬼手幢。 笑死捷凄,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的围来。 我是一名探鬼主播跺涤,決...
    沈念sama閱讀 38,276評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼监透!你這毒婦竟也來了钦铁?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 36,927評論 0 259
  • 序言:老撾萬榮一對情侶失蹤才漆,失蹤者是張志新(化名)和其女友劉穎牛曹,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體醇滥,經(jīng)...
    沈念sama閱讀 43,400評論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡黎比,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,883評論 2 323
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了鸳玩。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片阅虫。...
    茶點(diǎn)故事閱讀 37,997評論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖不跟,靈堂內(nèi)的尸體忽然破棺而出颓帝,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 33,646評論 4 322
  • 正文 年R本政府宣布购城,位于F島的核電站吕座,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏瘪板。R本人自食惡果不足惜吴趴,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,213評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望侮攀。 院中可真熱鬧锣枝,春花似錦、人聲如沸兰英。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,204評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽畦贸。三九已至陨闹,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間家制,已是汗流浹背正林。 一陣腳步聲響...
    開封第一講書人閱讀 31,423評論 1 260
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留颤殴,地道東北人觅廓。 一個(gè)月前我還...
    沈念sama閱讀 45,423評論 2 352
  • 正文 我出身青樓,卻偏偏與公主長得像涵但,于是被迫代替她去往敵國和親杈绸。 傳聞我的和親對象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,722評論 2 345