Spring Data JPA从入门到精通(第二部分)


Spring Data JPA从入门到精通(第一部分)
Spring Data JPA从入门到精通(第二部分)
Spring Data JPA从入门到精通(第三部分)


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

这一部分由于篇幅较少,看完还有很多疑问,就按照自己的思路重新整理了下.

一.整体认识JPA

推荐:

什么是JPA

JPA的根本目标是实现将面向对象的存储与底层所提供的持久化机构解耦;也就是说,从面向对象程序开发者的角度来说,我不需要知道你底层的数据库是什么,Oracle也好、MySQL也好、DB2也好,我不关心, 我只需要你(JPA)帮我将我需要的对象数据(Object)持久化(写入存储)或反序列化(读出存储)即可。

JPA 只关注与关系型数据库, 非关系型(如Redis,MongoDB)则由Spring Data 来定义.

Spring的视野下几个Lib的关系:

image.png

可以看出Spring-Data-Jpa作为Spring-Data的子项目,把工作职责限定在关系型DB,它依赖于Spring-Data-Commons和Spring-orm,而orm又依赖于Hibernate实现.换句话说,也就是 Commons 比较超然,并不会和ORM模式直接关联.

二. JPA的 Spring Boot自动装配

  • 自动装备

Spring-Boot-JPA支持自动装配,只需要在dependency中加上spring-boot-starter-data-jpa,而无需任何配置,即可自动识别@Entity和@Repository(甚至这个都可以没有),把仓库组装好.

SpringBoot 中的 META-INF/spring.factories(完整路径:spring-boot/spring-boot-autoconfigure/src/main/resources/META-INF/spring.factories)中关于 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

可以发现 JpaRepositoriesAutoConfiguration 和 HibernateJpaAutoConfiguration 帮我们装置了 JPA 相关的配置。

  1. 在@SpringBootApplication中就包含@EnableAutoConfiguration,而它就包含@Import(AutoConfigurationImportSelector.class)
  2. 在AbstractApplicationContext#invokeBeanFactoryPostProcessors()方法中会调用AutoConfigurationImportSelector#process(),它会遍历所有jar中的META-INF/spring.factories.
  3. 在getCandidateConfigurations()方法中,就会找到spring-boot-autoconfigure-××.jar中配置的org.springframework.boot.autoconfigure.EnableAutoConfiguration一节内容,变成List<String> configurations.这个list包含了我们关心的JdbcRepositoriesAutoConfiguration和JpaRepositoriesAutoConfiguration
  4. 看看代码
// 默认的proxyBeanMethods模式是false
@Configuration(proxyBeanMethods模式是false = false)
// 需要配置JDBC先
@ConditionalOnBean(DataSource.class)
@ConditionalOnClass(JpaRepository.class)
@ConditionalOnMissingBean({ JpaRepositoryFactoryBean.class, JpaRepositoryConfigExtension.class })
// 留个口子,即时加了Spring-data-jpa的依赖,也可以通过配置关闭,默认是开启的.
@ConditionalOnProperty(prefix = "spring.data.jpa.repositories", name = "enabled", havingValue = "true",
        matchIfMissing = true)
// 这个是核心注解      
@Import(JpaRepositoriesRegistrar.class)
@AutoConfigureAfter({ HibernateJpaAutoConfiguration.class, TaskExecutionAutoConfiguration.class })
public class JpaRepositoriesAutoConfiguration {

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

我们发现,这里又Import了另外一个配置JpaRepositoriesRegistrar

  1. JpaRepositoriesRegistrar从名字我们就可以看出,他是一个配置注册器,其扩展了AbstractRepositoryConfigurationSourceSupport,在这个类可以看到熟悉的registerBeanDefinitions方法.

而且在JpaRepositoriesRegistrar还有个内部静态Class,它@EnableJpaRepositories,spring-boot把这个藏在这里了,因此我们不需要自己去Enable

  • 解析用户仓库接口

既然找到了注册器,那么接下来就来看看如果解析并生成仓库类,也就是JPA最牛的地方,我们只需要定义接口,其他由JPA替我们实现.

  1. AbstractRepositoryConfigurationSourceSupport#registerBeanDefinitions()方法就是入口了.
  2. 跟踪代码进入RepositoryConfigurationDelegate#registerRepositoriesIn(),其中
Collection<RepositoryConfiguration<RepositoryConfigurationSource>> configurations = extension
                .getRepositoryConfigurations(configurationSource, resourceLoader, inMultiStoreMode);

这句会找到我们写的接口(extends JpaRepository<>)

图:

  1. 找到了列表,就开始逐个遍历,这里通过BeanDefinitionBuilder构造出org.springframework.data.jpa.repository.support.JpaRepositoryFactoryBean,并注册到registry,也就是DefaultListableBeanFactory中去.这样我们的BeanFactory也知道这个仓库接口了.
  • 创建接口实现

  1. refresh()方法的finishBeanFactoryInitialization()中,创建JpaRepositoryFactoryBean的实例.
  2. AbstractBeanFactory#doGetBean()创建beanName为"userRepository"的实例
                // Create bean instance.
                if (mbd.isSingleton()) {
                    // 我们的Repository都是单例的
                    sharedInstance = getSingleton(beanName, () -> {
                        try {
                            // 这里构造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);
                }

这里就会创建出这个工厂factory,在工厂bean创建后的afterPropertiesSet,会调用

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

此时,使用工厂方法根据我们定义的接口,创建出Repository代理类.之后再调用get()方法创建出实例.

注意:创建代理类使用的是RepositoryFactorySupport#getRepository()方法,其中QueryExecutorMethodInterceptor负责拆开我们自定义的方法如(findByName),如果名字有错误,那么在

new QueryExecutorMethodInterceptor(information, projectionFactory)

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

时就会出错,这里用了好多lambda表达式.继续跟踪,发现是JpaQueryMethod负责,构造JpaQueryMethod后,调用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;
            }
            // 这个name就是解析出的参数名了,例如findByName,就会解析出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;
            }
            // 发现name找不到具体对应的属性,这里抛出异常
            throw new IllegalStateException(
                    String.format("Did neither find a NamedQuery nor an annotated query for method %s!", method));
        }

也就是说,它是在启动时将接口方法解析为HQL,如果接口方法参数有问题,在"启动"阶段而非"编译"阶段会发现错误.

最重要的图3-3

image.png

推荐阅读: CSDN---Spring-data-jpa 图文课

Spring Data JPA源码分析-方法命名查询

spring data jpa 全面解析(实践 + 源码分析)

三. JPA规范与接口

JPA接口规范

从javax.persistence-api-2.2规范里面可以看出,JPA比起JDBC规范真是复杂多了;暂时没有实力去逐个了解.抓住几个重点先,虽然可能并不完全正确,但是有利于理解后面看JPA实现的源码:

  • criteria包定义了JPA对于SQL的抽象(如CriteriaQuery代表查询,CriteriaUpdate代表更新)

    • Expression是表达式,具体来说,可以代表SQL中Id=1这一小段.
    • Predicate是一组Boolean型的Expression,代表Where后面一整串.
    • Root和Path,Root代表整个查询的Entity,也就是表对象,而Path是其中一部分,也就是某一列.
    • CriteriaBuilder是一个构建器,可以方便地构造出CriteriaQuery,CriteriaUpdate并且带着各自构建Expression和组合Expresssion的方法.
  • metamodel包定义了JPA对于Object的抽象,类似于Spring的BeanDefinition

    • ManagedType扩展了Type,代表了一个受控的元模型
    • Metamodel是JPA的元模型类,可以根据Class类型获得EntityType或managedType
  • spi包定义要实现SPA需要实现的一些接口

    • PersistenceProvider规定了createEntityManagerFactory,generateSchema并能获得当前的ProviderUtil实例.

顶层包倒是我们比较熟悉的一些注解,如 @Entity, @Id, @OneToMany, @SequenceGenerator等等.

可以看出,JPA的抽象简直是智力游戏级别的,就是朝着让人看不懂方向去的,不是Hibernate的专家,应该搞不懂为啥要这么去做吧.因此只能大概过一下,有个概念.

JpaRepository继承树与实现

图: 继承树

image.png

从继承树可以看出,QuerydslJpaRepository已被废弃,而我们需要查看的实现类是SimpleJpaRepository,那么具体的查询是如何执行的呢,可以分以下两种情况:

  1. 如果是JPA的标准实现如findById()这种,就直接调用使用代理拦截,由SimpleJpaRepository代替接口完成查询工作.

  2. 如果是自定义的方法,就需要结合之前解析的内容,在resolveQuery后进,把查询条件都保存在
    QueryExecutorMethodInterceptor的queries中,它是一个private final Map<Method, RepositoryQuery>,这样我们自定义的方法,就可以根据名字,对应到一个RepositoryQuery上面了.
    之前在构造Bean时候,已经解析好了.因而在程序中调用的时候,自然就进入了JdkDynamicAopProxy#invoke()方法,然后它会调用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();
        }
        

就可以找到对应的PartTreeJpaQuery#execute()进行查询了,这种就没有过SimpleJpaRepository.

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

  • @Query(JPQL标准)

  • 方法名称查询

上面的QueryExecutorMethodInterceptor完成

  • Example查询(6.2-QueryByExpampleExecutor)

  1. 通过传入一个Example样本S,去查找类似的数据.当然最终也是通过Specification实现.
  2. ExampleMatcher通过相当简单的规则,对样本数据进行筛选条件的限制;包括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);

至于怎么用,书里面说的并不详细,建议看看官方文档了.

五.Spring-Data-JPA事务的实现

JPA实际也是通过JpaRepositoryFactory实现的.

六.扩展能力

  1. Audit扩展

Spring Data JPA为我们提供了审计功能的架构实现,提供了4个专门的注解:@CreateBy,@CreateDayte,@LastModifiedBy,@LastModifiedDate

具体使用步骤:

一. @Entity增加AutityListener,并增加上述4个注解
二. 实现AuditorAware接口,告诉JPA当前用户(推荐统一从Request中取)
三. 通过@EnableJpaAuditing注解开启JPA的审计功能

这样,每次在修改表的同时,也自动添加了审计的信息

  1. Listener
    从审计的实现可以看出,他是通过定义事件处理完成的.

JPA提供了CallBack钩子(这个好像GORM的实现),在数据库操作过程可以自定义EntityListener,并且回填entity对象,这样就能很方便扩展.

@Prepersist注解的方法 ,完成save之前的操作。
@Preupdate注解的方法 ,完成update之前的操作。
@PreRemove注解的方法 ,完成remove之前的操作。
@Postpersist注解的方法 ,完成save之后的操作。
@Postupdate注解的方法 ,完成update之后的操作。
@PostRemovet注解的方法 ,完成remove之后的操作。

image.png
  1. Version

JPA通过@Version帮我们处理乐观锁,只需要增加一个long类型的version字段,每次save的时候,JPA都会帮我们自增该字段.(这样当出现多线程同时save,就有可能出现乐观锁更新失败的情况)
需要我们自行捕获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的那个属性变成了versionAttribute,这里会判断一下.

  1. 分页排序
    这个是同Spring Web Mvc配合使用的.
  • 通过@EnableSpringDataWebSupport可以自动注入Repository对象.---还是显式注入的好.
  • 通过handlerMethodArgumentResolvers可以实现分页和排序.

参考:

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/
https://www.jianshu.com/p/d05ba90d19e7

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

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,558评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,002评论 3 387
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,036评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,024评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,144评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,255评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,295评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,068评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,478评论 1 305
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,789评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,965评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,649评论 4 336
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,267评论 3 318
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,982评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,223评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,800评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,847评论 2 351