暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

Spring data JPA实践和原理浅析

领创集团Advance Group 2022-04-29
551


前言

从我们进⼊编程的世界,成为程序员到现在为⽌,总有⼏个感觉神奇和激动的时刻,其中肯定包括你第⼀次程序连上数据库可以实现 CURD 功能的时候,就算那时的我们写着千遍⼀律 JDBC 模板代码也是乐此不疲。
时代在不断进步,技术也不断在发展,市面上已经有很多优秀的数据库持久化框架供我们使用,今天我将带大家来了解JPA的使用。




基本使用

在我们现在Spring Boot横行无忌的时代,在项目中引入JPA非常简单,我们以Maven以及常用的MySQL数据库为例
在pom.xml文件添加以下依赖
    <parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.5.6</version>
    <relativePath/>
    </parent>


    <!-- jpa -->
    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jpa</artifactId>
    </dependency>
    <!-- mysql -->
    <dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    </dependency>
    在Spring Boot的YML文件中添加以下内容
      spring:
      datasource:
      driver-class-name: com.mysql.cj.jdbc.Driver
      url: jdbc:mysql://localhost:3306/sakila?useUnicode=true&zeroDateTimeBehavior=convertToNull&autoReconnect=true&characterEncoding=utf-8
      username: root
      password: root
      定义Entity类,我测试过程使用的数据源为MySQL官方提供的样例数据库sakila,大家可以在https://dev.mysql.com/doc/index-other.html自行下载
        @Entity
        @Data lombok注解
        public class Actor {


        @Id
        Column注解不是必须的,如果满足字段驼峰形式
        与数据库字段以下划线分隔形式对应即可
        @Column(name = "actor_id", nullable = false)
        private Integer actorId;


        @Column(name = "first_name", nullable = false, length = 45)
        private String firstName;
        private String lastName;
        private Timestamp lastUpdate;
        }
        定义Respository接口,一般我们通过继承JpaRepository接口即能满足我们一般的CURD操作,如果需要支持复杂逻辑查询
        比如:动态SQL;联表查询,则需要继承 JpaSpecificationExecutor 接口,并配合Specification的接口方法。
          @Repository
          public interface ActorRepository extends JpaRepository<Actor, Integer> {
          }
          在JpaRepository的中我们可以看到有findAll、getById、findById、save、saveAll、deleteById...这些默认定义,这是JPA为我们提供的常用的一些数据操作,我们可以直接使用,极大提高了日常的开发效率。
          2.1通过方法名称直接生成查询
          正是因为这一便利的让人着迷的特性,让我们乐于去使用JPA这一持久化框架,接下来让我们通过几个例子去欣赏它的迷人之处吧。
          2.1.1一般写法
            /**
            * 示例1
            * SQL: SELECT * FROM actor WHERE first_name = ?
            * 参数名的定义不影响程序的运行
            */
            List<Actor> findByFirstName(String name);


            /**
            * 示例2
            * SQL: SELECT * FROM actor WHERE first_name = ? AND last_name = ?
            * 如果明确知道查询结果返回唯一一条记录时,建议使用单一实体类作为返回类型
            */
            List<Actor> findByFirstNameAndLastName(String name1, String name2);


            /**
            * 示例3
            * SQL: SELECT * FROM actor WHERE actor_id <= ?
            */
            List<Actor> findByActorIdLessThanEqual(Integer id);
            遵照JPA的规范,通过定义类似以上接口方法的形式就可以零SQL实现我们需要的单表查询(不能实现DML操作)操作。JPA对此类查询方式有很丰富的支持,受限于篇幅,我们就不一一讲述了,详细的内容可以阅读官方文档
            地址:
            https://docs.spring.io/spring-data/jpa/docs/2.5.6/reference/html/#repository-query-keywords

            Tips
            1.在查询场景中,自定义的查询接口中,find关键词(也可以是search、query、get)后面必须跟随By关键词
            2.Between适用于数值、日期字段,用于日期时,参数类型可以是java.util.Date或java.sql.Timestamp
              List<Film> findByLengthBetween(Integer low, Integer up);
              List<Film> findByLastUpdateBetween(Date startDate, Date endDate);
              3.IsEmpty IsNotEmpty只能用于集合类型的字段
              4.Before或者After可用于日期、数值类型的字段
                List<Film> findByLengthBefore(Integer length)
                5.涉及到删除和修改时,需要在方法定义上加上@Modifying

                2.2基于@Query 注解的操作
                2.2.1使用JPQL
                JPQL是通过Hibernate的HQL演变过来的,它和HQL语法及其相似。
                  /**
                  * 示例1
                  * SQL: SELECT * FROM actor WHERE first_name = ?
                  */
                  @Query("FROM Actor WHERE firstName = ?1")
                  List<Actor> findByFirstName(String name);


                  /**
                  * 示例2
                  * SQL: SELECT * FROM actor WHERE first_name = ? AND last_name = ?
                  */
                  @Query("FROM Actor WHERE firstName = ?1 AND lastName = ?2")
                  List<Actor> findByFirstNameAndLastName(String name1, String name2);


                  /**
                  * 示例3
                  * SQL: SELECT * FROM actor WHERE actor_id <= ?
                  */
                  @Query("FROM Actor WHERE actorId <= ?1")
                  List<Actor> findByActorIdLessThanEqual(Integer id);


                  /**
                  * 示例4
                  * SQL: SELECT * FROM actor
                  * 不能写"SELECT *" 要写"SELECT 别名"
                  */
                  @Query("SELECT a FROM Actor a")
                  List<Actor> findAll()
                  通过以上例子我们发现,JPQL与SQL的区别是,SQL是面向对象关系数据库,它操作的是数据表和数据列,而JPQL操作的对象是实体对象和实体属性,区分大小写,出现的SQL关键字还是原有的意思,不区分大小写。JPQL也可以支持复杂的联表查询。下面是JPQL的基本格式:
                  SELECT 实体别名.属性名, 实体别名.属性名 FROM 实体名 AS 实体别名 WHERE 实体别名.实体属性 op 比较值
                  看完查询的写法,我们来看看DML操作的写法
                    @Modifying
                    @Query(value = "UPDATE Film SET description = :description WHERE filmId = :id")
                    void update(@Param("id") Integer filmId, @Param("description") String description);
                    2.2.2使用原生SQL
                    在Query注解中,有一个属性字段nativeQuery,默认情况下为false,即为JPQL模式,如果我们设置为true,则我们可以value属性中定义原生SQL语句实现数据库操作。
                      @Query("SELECT * FROM actor WHERE first_name = ?1", nativeQuery = true)
                      List<Actor> findByFirstName(String name);


                      @Query("SELECT * FROM actor WHERE first_name = ?1 AND last_name = ?2", nativeQuery = true)
                      List<Actor> findByFirstNameAndLastName(String name1, String name2);


                      @Query("SELECT * FROM actor WHERE actor_id <= ?", nativeQuery = true)
                      List<Actor> findByActorIdLessThanEqual(Integer id);


                      @Query("SELECT first_name, last_name FROM actor", nativeQuery = true)
                      List<Map<String, Object>> findAll();
                      // List<String[]> findAll();
                      通过上述示例,我们总结几点编写原生SQL需要注意的地方
                      1.如果接口方法返回的是实体对象,如List<Actor>、Actor这样的,则SELECT部分不能指定字段名,必须为*
                      2.如果只想查询指定列的数据,方法定义时的返回类型可以是Map<String, Object>、String[],返回数据如果是多条,则用集合类嵌套
                      3.关于JPA接口方法的返回类型我们可以参考官方文档,地址为:
                      https://docs.spring.io/spring-data/jpa/docs/current/reference/html/#repository-query-return-types


                      JPA的原理浅析

                      通过前面的学习,我们已经对JPA有了比较清晰的了解,不再是门外汉了,当然我们还是会有很多的疑问:
                      ○为什么我们只是加入了一个Maven的依赖,就能直接使用JPA?
                      ○为什么我们只是定义了一个Repository的接口就能直接使用它来实现数据库操作逻辑?
                      ○为什么我们只是按照JPA的规范,定义了一个接口方法就能在使用时生成想要的SQL?
                      在接下来的学习中一一解开。
                      3.1JPA的自动配置
                      虽然这一节是要讲JPA的自动配置,其实本质要说的是Spring Boot自动加载配置的原理。在我们使用Spring Boot创建一个项目时,必不可少的一个注解就是@SpringBootApplication,看着它我们既熟悉又陌生。我们通过它的源码来一探究竟。
                        package org.springframework.boot.autoconfigure;


                        @Target(ElementType.TYPE)
                        @Retention(RetentionPolicy.RUNTIME)
                        @Documented
                        @Inherited
                        @SpringBootConfiguration
                        @EnableAutoConfiguration
                        @ComponentScan(excludeFilters = { @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
                        @Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })
                        public @interface SpringBootApplication {


                        @AliasFor(annotation = EnableAutoConfiguration.class)
                        Class<?>[] exclude() default {};


                        @AliasFor(annotation = EnableAutoConfiguration.class)
                        String[] excludeName() default {};


                        @AliasFor(annotation = ComponentScan.class, attribute = "basePackages")
                        String[] scanBasePackages() default {};


                        @AliasFor(annotation = ComponentScan.class, attribute = "basePackageClasses")
                        Class<?>[] scanBasePackageClasses() default {};


                        @AliasFor(annotation = ComponentScan.class, attribute = "nameGenerator")
                        Class<? extends BeanNameGenerator> nameGenerator() default BeanNameGenerator.class;


                        @AliasFor(annotation = Configuration.class)
                        boolean proxyBeanMethods() default true;


                        }
                        通过源码我们发现其实SpringBootApplication也被一些注解所修饰,其中就有EnableAutoConfiguration注解,一看名字我们就知道它就是我们今天要找的正主。同样的,我们也不会放过它的源码的。
                          package org.springframework.boot.autoconfigure;


                          @Target(ElementType.TYPE)
                          @Retention(RetentionPolicy.RUNTIME)
                          @Documented
                          @Inherited
                          @AutoConfigurationPackage
                          @Import(AutoConfigurationImportSelector.class)
                          public @interface EnableAutoConfiguration {


                          String ENABLED_OVERRIDE_PROPERTY = "spring.boot.enableautoconfiguration";


                          Class<?>[] exclude() default {};


                          String[] excludeName() default {};


                          }
                          我们又看到了熟悉的身影,@Import注解,它意味着在我们的IOC容器中引入一个AutoConfigurationImportSelector对象。

                          通过源码我们又发现AutoConfigurationImportSelector最终实现了ImportSelector接口。因此在程序的启动过程中,会执行selectImports方法,在当前的实现中,这个方法的主要逻辑是去读取一个 spring.factories下key为EnableAutoConfiguration对应的全限定名的值。

                          spring.factories里面配置的那些类,主要作用是告诉 Spring Boot这个stareter所需要加载哪些xxxAutoConfiguration类,也就是你真正的要自动注册的那些bean或功能。
                          而在SpringBoot中的META-INF/spring.factories(完整路径:spring-boot/spring-boot-autoconfigure/src/main/resources/META-INF/spring.factories)中关于EnableAutoConfiguration的这段配置如下 :


                          可以发现有JpaRepositoriesAutoConfiguration和HibernateJpaAutoConfiguration帮我们配置了JPA 相关的配置。至此,我们的第一个疑惑可以解开了。
                          3.2SimpleJpaRepository类
                          接下来我们要来解决第二个疑惑,其实本节的标题已经告诉了我们答案。先上源码:
                            @Repository
                            @Transactional(readOnly = true)
                            public class SimpleJpaRepository<T, ID> implements JpaRepositoryImplementation<T, ID> {
                            }
                            我们不用看具体实现的方法逻辑了,单从SimpleJpaRepository的声明来看,它是一个类,不是抽象类是一个实现了JpaRepositoryImplementation接口的类
                            实际上JpaRepositoryImplementation接口也继承了JpaRepository和JpaSpecificationExecutor接口。
                            到此,我们可以大胆的猜测,为什么我们定义的repository接口只是继承了JpaRepository和JpaSpecificationExecutor接口就能使用一系列的接口调用,其实是SimpleJpaRepository类帮我们做了要做的事情。

                            那到底是不是,如果是的,那JPA是怎么做到了的呢?我们继续一步一步分析。首先我们得有一个分析的起点,上一节我们讲了JPA的自动配置,那我们是不是需要看看它到底配置了什么,看看能不能找到我们想要的。我们直接看JpaRepositoriesAutoConfiguration,因为它的名字里带Repositories。
                              @Configuration(proxyBeanMethods = false)
                              @ConditionalOnBean(DataSource.class)
                              @ConditionalOnClass(JpaRepository.class)
                              @ConditionalOnMissingBean({ JpaRepositoryFactoryBean.class, JpaRepositoryConfigExtension.class })
                              @ConditionalOnProperty(prefix = "spring.data.jpa.repositories", name = "enabled", havingValue = "true",
                              matchIfMissing = true)
                              @Import(JpaRepositoriesRegistrar.class)
                              @AutoConfigureAfter({ HibernateJpaAutoConfiguration.class, TaskExecutionAutoConfiguration.class })
                              public class JpaRepositoriesAutoConfiguration {
                              }
                              我们好像运气不错,果然发现有一个Import注解引入了一个JpaRepositoriesRegistrar类,从名字看,JPA repository注册器,好像越来越接近了,直接上源码:
                                class JpaRepositoriesRegistrar extends AbstractRepositoryConfigurationSourceSupport {
                                }


                                public abstract class AbstractRepositoryConfigurationSourceSupport
                                implements ImportBeanDefinitionRegistrar, BeanFactoryAware, ResourceLoaderAware, EnvironmentAware {


                                private ResourceLoader resourceLoader;


                                private BeanFactory beanFactory;


                                private Environment environment;


                                @Override
                                public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry,
                                BeanNameGenerator importBeanNameGenerator) {
                                RepositoryConfigurationDelegate delegate = new RepositoryConfigurationDelegate(
                                getConfigurationSource(registry, importBeanNameGenerator), this.resourceLoader, this.environment);
                                delegate.registerRepositoriesIn(registry, getRepositoryConfigurationExtension());
                                }


                                @Override
                                public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
                                registerBeanDefinitions(importingClassMetadata, registry, null);
                                }


                                其他源码
                                ...
                                }
                                其实分析了这么久,如果我们经常去看看Spring系的一些源码,会发现很多老朋友。我们看到其中一个关键接口ImportBeanDefinitionRegistrar,它的关键方法就是registerBeanDefinitions了,就是按照我们的实现逻辑把一些我们需要的bean注册到IOC容器中。分析到这,我们通过测试代码调试来看看吧。

                                我们可以看到debug窗口中,在测试类中注入的CustomerRepository接口的实际对象是JdkDynamicAopProxy类型,这是一个jdk动态代理类,这确实比较符合我们对Spring IOC容器对接口类注入处理的认识。我们看一下它代理的目标类,发现是SimpleJpaRepository类,如此就证实了我们之前的猜测。第一次调试到此结束。

                                我们再回过头来看我们之前找到的AbstractRepositoryConfigurationSourceSupport类中registerBeanDefinitions方法,这个是在项目启动过程中执行的,我们加上断点进行第二次调试。

                                registerBeanDefinitions方法中主要的逻辑在delegate.registerRepositoriesIn中实现,我们直接在里面标记上断点。
                                当执行到上面断点后,我们看到configurations对象中已经有了我们自己定义的repository接口的信息了,接下来我们将进入for循环分别处理我们定义的接口了,我们进到下一个断点继续看。

                                在当前断点,我们可以看到,beanName是符合我们正常创建IOC容器中bean的命名规则的,但是JPA为我们创建的BeanDefinition对象是org.springframework.data.jpa.repository.support.JpaRepositoryFactoryBean类型,它是一个FactoryBean。我们继续看187、189行的代码

                                JPA将我们的BeanDefinition对象设置了一个name为factoryBeanObjectType,value为当前被处理的repository接口的全限定类名(比如ai.advance.jpademo.repository.FilmRepository)的attribute

                                最后将BeanDefinition对象注册到了我们的IOC容器中。因此当for循环执行完后,如果我们使用repository接口对应的beanName从IOC容器中获取一个实例,最开始我们拿到的是一个JpaRepositoryFactoryBean类型的Bean,当然我们最后真正拿到的bean对象是由getObject方法返回的对象。我们先来看一下它的继承关系:

                                JpaRepositoryFactoryBean既是一个工厂,又是一个bean。其作用类似于@Bean注解,但比其能实现更多复杂的功能,可以对对象增强。重要方法是getObject(),返回一个bean,因此我们去看一下getObject方法的实现,实际它的getObject方法的实现在其父类RepositoryFactoryBeanSupport中。
                                  @Nonnull
                                  public T getObject() {
                                  return this.repository.get();
                                  }
                                  好像没什么东西,有点懵,那我们先忽略它,我们再一想,JpaRepositoryFactoryBean对象也是一个Bean,那有没有一点和bean实例化过程相关的代码,然后我们找到了这块代码:
                                    @Override
                                    public void afterPropertiesSet() {


                                    Assert.state(entityManager != null, "EntityManager must not be null!");


                                    super.afterPropertiesSet();
                                    }

                                    而确实JpaRepositoryFactoryBean的父类RepositoryFactoryBeanSupport也实现了InitializingBean接口,它自身也是调用了父类的afterPropertiesSet方法。
                                      // 以下代码实现在org.springframework.data.repository.core.support.RepositoryFactoryBeanSupport中
                                      public void afterPropertiesSet() {


                                      this.factory = createRepositoryFactory();
                                      this.factory.setQueryLookupStrategyKey(queryLookupStrategyKey);
                                      this.factory.setNamedQueries(namedQueries);
                                      this.factory.setEvaluationContextProvider(
                                      evaluationContextProvider.orElseGet(() -> QueryMethodEvaluationContextProvider.DEFAULT));
                                      this.factory.setBeanClassLoader(classLoader);
                                      this.factory.setBeanFactory(beanFactory);


                                      if (publisher != null) {
                                      this.factory.addRepositoryProxyPostProcessor(new EventPublishingRepositoryProxyPostProcessor(publisher));
                                      }


                                      repositoryBaseClass.ifPresent(this.factory::setRepositoryBaseClass);


                                      this.repositoryFactoryCustomizers.forEach(customizer -> customizer.customize(this.factory));


                                      RepositoryFragments customImplementationFragment = customImplementation
                                      .map(RepositoryFragments::just)
                                      .orElseGet(RepositoryFragments::empty);


                                      RepositoryFragments repositoryFragmentsToUse = this.repositoryFragments
                                      .orElseGet(RepositoryFragments::empty)
                                      .append(customImplementationFragment);


                                      this.repositoryMetadata = this.factory.getRepositoryMetadata(repositoryInterface);


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


                                      Make sure the aggregate root type is present in the MappingContext (e.g. for auditing)
                                      this.mappingContext.ifPresent(it -> it.getPersistentEntity(repositoryMetadata.getDomainType()));


                                      if (!lazyInit) {
                                      this.repository.get();
                                      }
                                      }
                                      逻辑有点多,我们先看到这一行代码:
                                        this.repository = Lazy.of(() -> this.factory.getRepository(repositoryInterface, repositoryFragmentsToUse));
                                        先提一句Lazy类继承了java.util.function.Supplier接口。这个this.repository我们是不是在getObject方法有看到,我们还发现它的get方法返回的结果是由this.factory.getRepository()得到的,那这个this.factory又是谁呢,这时我们就要返回来看afterPropertiesSet方法的第一行代码:
                                          this.factory = createRepositoryFactory();
                                          直接看createRepositoryFactory方法,它是在JpaRepositoryFactoryBean的直接父类TransactionalRepositoryFactoryBeanSupport中实现的。
                                            protected final RepositoryFactorySupport createRepositoryFactory() {


                                            RepositoryFactorySupport factory = doCreateRepositoryFactory();


                                            RepositoryProxyPostProcessor exceptionPostProcessor = this.exceptionPostProcessor;


                                            if (exceptionPostProcessor != null) {
                                            factory.addRepositoryProxyPostProcessor(exceptionPostProcessor);
                                            }


                                            RepositoryProxyPostProcessor txPostProcessor = this.txPostProcessor;


                                            if (txPostProcessor != null) {
                                            factory.addRepositoryProxyPostProcessor(txPostProcessor);
                                            }


                                            return factory;
                                            }
                                            我们直接看关键代码 -- doCreateRepositoryFactory方法,这个是由JpaRepositoryFactoryBean类自己实现的。
                                              protected RepositoryFactorySupport doCreateRepositoryFactory() {


                                              Assert.state(entityManager != null, "EntityManager must not be null!");


                                              return createRepositoryFactory(entityManager);
                                              }


                                              /**
                                              * Returns a {@link RepositoryFactorySupport}.
                                              */
                                              protected RepositoryFactorySupport createRepositoryFactory(EntityManager entityManager) {


                                              JpaRepositoryFactory jpaRepositoryFactory = new JpaRepositoryFactory(entityManager);
                                              jpaRepositoryFactory.setEntityPathResolver(entityPathResolver);
                                              jpaRepositoryFactory.setEscapeCharacter(escapeCharacter);


                                              if (queryMethodFactory != null) {
                                              jpaRepositoryFactory.setQueryMethodFactory(queryMethodFactory);
                                              }


                                              return jpaRepositoryFactory;
                                              }
                                              我们直接看到createRepositoryFactory方法,它最后返回是一个JpaRepositoryFactory类型的对象,其他的我们就先不关心了,直接看JpaRepositoryFactory类。我们先来一张类关系图:

                                              我们直奔我们的主题-- getRepository方法,它是在JpaRepositoryFactory的父类RepositoryFactorySupport中实现的。
                                                public <T> T getRepository(Class<T> repositoryInterface, RepositoryFragments fragments) {


                                                ...


                                                RepositoryInformation information = getRepositoryInformation(metadata, composition);


                                                ...


                                                Object target = getTargetRepository(information);


                                                ...


                                                ProxyFactory result = new ProxyFactory();
                                                result.setTarget(target);
                                                result.setInterfaces(repositoryInterface, Repository.class, TransactionalProxy.class);


                                                ...


                                                T repository = (T) result.getProxy(classLoader);


                                                ...


                                                return repository;
                                                }
                                                实现代码有点多,这里仅展示我们关注的关键代码,去掉其他多余的代码之后,有没有发现上面的逻辑很熟悉,其实就是构建一个代理类实例的过程,因此也解释了为什么我们在第一次调试看到实际注入的是一个JdkDynamicAopProxy类型的实体。那我们得好好看看代理对象的目标对象是怎么得到的,请看getTargetRepository方法,发现他需要一个RepositoryInformation类型的传参,我继续往上找,看到了getRepositoryInformation方法。
                                                  private RepositoryInformation getRepositoryInformation(RepositoryMetadata metadata,
                                                  RepositoryComposition composition) {


                                                  RepositoryInformationCacheKey cacheKey = new RepositoryInformationCacheKey(metadata, composition);


                                                  return repositoryInformationCache.computeIfAbsent(cacheKey, key -> {


                                                  Class<?> baseClass = repositoryBaseClass.orElse(getRepositoryBaseClass(metadata));


                                                  return new DefaultRepositoryInformation(metadata, baseClass, composition);
                                                  });
                                                  }
                                                  其中,可以看到一个getRepositoryBaseClass方法
                                                    protected Class<?> getRepositoryBaseClass(RepositoryMetadata metadata) {
                                                    return SimpleJpaRepository.class;
                                                    }
                                                    好了,什么也不用说了,它直接给我们返回了SimpleJpaRepository的Class对象。其他的逻辑我们也不看了,虽然还有一些包装和判断的过程,但是我们今天的目的已经达到了,第二个疑惑也算比较好的解答了。最后顺便提一句,如果你的项目是手动使用了@EnableJpaRepositories注解,可能你的调试过程开局会有点不一样,但是后续的逻辑是相同的,自己可以去试试。代码如下:
                                                      @EnableJpaRepositories(basePackages = "ai.advance.jpademo.repository")
                                                      @SpringBootApplication
                                                      public class JpaDemoApplication {


                                                      public static void main(String[] args) {
                                                      SpringApplication.run(JpaDemoApplication.class, args);
                                                      }


                                                      }

                                                      3.3自定义查询
                                                      我们还有一个疑问,为什么我们自己定义的那些不属于SimpleJpaRepository类的方法也能被调用,并且被正确生成SQL?我们又要回到上节分析的getRepository方法,同样的我们去掉不需要关心的代码。
                                                        public <T> T getRepository(Class<T> repositoryInterface, RepositoryFragments fragments) {


                                                        ...


                                                        ProxyFactory result = new ProxyFactory();


                                                        ...


                                                        Optional<QueryLookupStrategy> queryLookupStrategy = getQueryLookupStrategy(queryLookupStrategyKey,
                                                        evaluationContextProvider);
                                                        result.addAdvice(new QueryExecutorMethodInterceptor(information, projectionFactory, queryLookupStrategy,
                                                        namedQueries, queryPostProcessors, methodInvocationListeners));


                                                        result.addAdvice(
                                                        new ImplementationMethodExecutionInterceptor(information, compositionToUse, methodInvocationListeners));


                                                        T repository = (T) result.getProxy(classLoader);


                                                        ...


                                                        return repository;
                                                        }
                                                        从上面的源码我们可以看到ProxyFactory对象在最后有两次addAdvice方法的调用,目的是为了增加QueryExecutorMethodInterceptor
                                                        和ImplementationMethodExecutionInterceptor两个拦截器,它们都实现了MethodInterceptor接口,其中ImplementationMethodExecutionInterceptor为RepositoryFactorySupport的静态内部类。
                                                        3.3.1QueryExecutorMethodInterceptor
                                                        QueryExecutorMethodInterceptor这个拦截器是用来拦截处理我们在repository接口中自定义的方法的。在QueryExecutorMethodInterceptor的成员变量中有一个定义为Map<Method, RepositoryQuery>类型的queries变量,这个变量主要保存了自定义方法对象与一个RepositoryQuery对象的映射关系。

                                                        RepositoryQuery的直接抽象子类是AbstractJpaQuery,可以看到,一个RepositoryQuery实例持有一个JpaQueryMethod实例,JpaQueryMethod又持有一个Method实例,所以RepositoryQuery实例的用途很明显,一个RepositoryQuery代表了Repository接口中的一个方法,根据方法头上注解不同的形态,将每个Repository接口中的方法分别映射成相对应的RepositoryQuery实例。我们通过类关系图来熟悉一下RepositoryQuery具体的实现类有哪些。

                                                        下面我们看看JPA在哪些情况下创建对应的那个RepositoryQuery对象
                                                        1.SimpleJpaQuery
                                                        方法头上@Query注解的nativeQuery属性缺省值为false,也就是使用JPQL,此时会创建SimpleJpaQuery实例,并通过两个StringQuery类实例分别持有query JPQL语句和根据query JPQL计算拼接出来的countQuery JPQL语句
                                                        2.NativeJpaQuery
                                                        方法头上@Query注解的nativeQuery属性如果显式的设置为nativeQuery=true,也就是使用原生SQL的时候
                                                        3.PartTreeJpaQuery
                                                        方法头上未进行@Query注解,将使用spring-data-jpa独创的方法名识别的方式进行sql语句拼接
                                                        4.NamedQuery
                                                        使用javax.persistence.NamedQuery注解访问数据库的形式的时候

                                                        5.StoredProcedureJpaQuery
                                                        在Repository接口的方法头上使用org.springframework.data.jpa.repository.query.Procedure注解,也就是调用存储过程的方式访问数据库的时候
                                                        所以QueryExecutorMethodInterceptor最终的目的就是根据当前需要调用的自定义的Repository的方法找到对应的RepositoryQuery对象,并构建调用信息并使用invoke方法触发调用,主要逻辑在QueryExecutorMethodInterceptor类的doInvoke方法中。
                                                          private Object doInvoke(MethodInvocation invocation) throws Throwable {


                                                          Method method = invocation.getMethod();


                                                          if (hasQueryFor(method)) {


                                                          RepositoryMethodInvoker invocationMetadata = invocationMetadataCache.get(method);


                                                          if (invocationMetadata == null) {
                                                          invocationMetadata = RepositoryMethodInvoker.forRepositoryQuery(method, queries.get(method));
                                                          invocationMetadataCache.put(method, invocationMetadata);
                                                          }


                                                          return invocationMetadata.invoke(repositoryInformation.getRepositoryInterface(), invocationMulticaster,
                                                          invocation.getArguments());
                                                          }


                                                          return invocation.proceed();
                                                          }

                                                          其实最终调用的是RepositoryMethodInvoker类中的doInvoke方法,我们打上断点来看一下

                                                          3.3.2ImplementationMethodExecutionInterceptor
                                                          ImplementationMethodExecutionInterceptor这个拦截器是来处理SimpleJpaRepository类本身实现的方法调用的。它是RepositoryFactorySupport类的静态内部类,只要没有被QueryExecutorMethodInterceptor拦截器处理的方法调用都会由它来处理,最终也是调用invoke方法。
                                                            public Object invoke(@SuppressWarnings("null") MethodInvocation invocation) throws Throwable {


                                                            Method method = invocation.getMethod();
                                                            Object[] arguments = invocation.getArguments();


                                                            try {
                                                            return composition.invoke(invocationMulticaster, method, arguments);
                                                            } catch (Exception e) {
                                                            org.springframework.data.repository.util.ClassUtils.unwrapReflectionException(e);
                                                            }


                                                            throw new IllegalStateException("Should not occur!");
                                                            }

                                                            其实这里面的逻辑属于中规中矩的代理类的拦截调用。我们拿findById这个方法作为例子,打上断点看看

                                                            从上面信息可以看出来ImplementationMethodExecutionInterceptor类的invoke方法调用的是RepositoryComposition类的invoke方法,如果继续深入,其实最终也是调用的RepositoryMethodInvoker类中的doInvoke方法


                                                            至此,我们开头的几大疑惑基本都得到了解释。



                                                            总结

                                                            我也是尽量把⾃⼰知道的知识写明⽩,奈何本⼈⽔平有限,接触JPA也不久,如果存在纰漏欢迎指正。关于JPA更深层次的东⻄我也还在学习中,希望之后有机会再分享。

                                                            感谢阅读「技术创想」第45期文章
                                                            领创集团正在春季招聘中
                                                            期待你的加入
                                                            点击文末
                                                            阅读原文
                                                            获取更多
                                                            招聘信息

                                                            关于领创集团

                                                            (Advance Intelligence Group)
                                                            领创集团成立于 2016年,致力于通过科技创新的本地化应用,改造和重塑金融和零售行业,以多元化的业务布局打造一个服务于消费者、企业和商户的生态圈。集团旗下包含企业业务和消费者业务两大板块,企业业务包含 ADVANCE.AI 和 Ginee,分别为银行、金融、金融科技、零售和电商行业客户提供基于 AI 技术的数字身份验证、风险管理产品和全渠道电商服务解决方案;消费者业务 Atome Financial 包括亚洲领先的先享后付平台 Atome 和数字金融服务。2021年 9月,领创集团宣布完成超4亿美元 D 轮融资,融资完成后领创集团估值已超 20亿美元,成为新加坡最大的独立科技创业公司之一。
                                                            文章转载自领创集团Advance Group,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

                                                            评论