mysql

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

2020-08-01  本文已影响0人  沉寂之舟

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实现的源码:

顶层包倒是我们比较熟悉的一些注解,如 @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查询方式的使用

上面的QueryExecutorMethodInterceptor完成

  1. 通过传入一个Example样本S,去查找类似的数据.当然最终也是通过Specification实现.
  2. ExampleMatcher通过相当简单的规则,对样本数据进行筛选条件的限制;包括nullHandler(空值处理),StringMatcher(字符串匹配方式),IgnoreCase(大小写忽略方式),properSepcifiers(属性特定查询方式),ignoredPaths(忽略属性列表).
  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配合使用的.

参考:

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

上一篇下一篇

猜你喜欢

热点阅读