• Spring Data JPA 之如何自定义 Repository


    11 如何自定义 Repository

    11.1 EntityManager 简介

    Java Persistence API 规定,操作数据库实体必须要通过 EntityManager 进⾏,⽽我们前⾯看到了所有的 Repository 在 JPA ⾥⾯的实现类是 SimpleJpaRepository,它在真正操作实体的时候都是调⽤ EntityManager ⾥⾯的⽅法。

    我们在 SimpleJpaRepository ⾥⾯设置⼀个断点,这样可以很容易看得出来 EntityManger 是 JPA 的接⼝协议,⽽其现类是 Hibernate ⾥⾯的 SessionImpl,如下图所示:

    在这里插入图片描述

    11.1.1 EntityManager 的常用方法

    public interface EntityManager {
        // ⽤于将新创建的 Entity 纳⼊ EntityManager 的管理。该⽅法执⾏后,传⼊ persist() ⽅法的 Entity 对象转换成持久化状态。
        public void persist(Object entity);
        // 将游离态的实体 merge 到当前的 persistence context ⾥⾯,⼀般⽤于更新。
        public <T> T merge(T entity);
        // 将实体对象删除,物理删除
        public void remove(Object entity);
        // 将当前的 persistence context 中的实体,同步到数据库⾥⾯,只有执⾏了这个⽅法,上⾯的 EntityManager 的操作才会 DB ⽣效;
        public void flush();
        // 根据实体类型和主键查询⼀个实体对象;
        public <T> T find(Class<T> entityClass, Object primaryKey);
        // 根据 JPQL 创建⼀个 Query 对象
        public Query createQuery(String qlString);
        // 利⽤ CriteriaUpdate 创建更新查询
        public Query createQuery(CriteriaUpdate updateQuery);
        // 利⽤原⽣的 sql 语句创建查询,可以是查询、更新、删除等 sql
        public Query createNativeQuery(String sqlString);
        // 其他⽅法我就不⼀⼀列举了,⽤法很简单,我们只要参看 SimpleJpaRepository ⾥⾯怎么⽤的,我们怎么⽤就可以了;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    11.1.2 EntityManager的使用

    它的使⽤⽅法很简单,我们在任何地⽅只要能获得 EntityManager,就可以进⾏⾥⾯的操作。

    获得 EntityManager 的⽅式:通过 @PersistenceContext 注解。

    将 @PersistenceContext 注解标注在 EntityManager 类型的字段上,这样得到的 EntityManager 就是容器管理的 EntityManager。由于是容器管理的,所以我们不需要、也不应该显式关闭注⼊的 EntityManager 实例。

    下⾯是关于这种⽅式的例⼦,我们想要在测试类中获得 @PersistenceContext ⾥⾯的 EntityManager,看看代码应该怎么写。

    @Slf4j
    @DataJpaTest
    @TestInstance(TestInstance.Lifecycle.PER_CLASS)
    public class UserRepositoryTest {
    
        /**
         * 利⽤该⽅式获得entityManager
         */
        @PersistenceContext
        private EntityManager entityManager;
    
        /**
         * 测试entityManager⽤法
         */
        @Test
        @Rollback(false)
        void testEntityManager() {
            // 测试找到⼀个 User 对象
            User user = entityManager.find(User.class, 2L);
            Assertions.assertNotNull(user);
    
            // 我们改变⼀下 user 的删除状态
            user.setDeleted(true);
            // merger ⽅法
            entityManager.merge(user);
            // 更新到数据库⾥⾯
            entityManager.flush();
            // 再通过 createQuery 创建⼀个 JPQL,进⾏查询
            List<User> users = entityManager.createQuery("select u From User u where u.name=?1")
                .setParameter(1, "jackxx")
                .getResultList();
            Assertions.assertTrue(users.get(0).isDeleted());
        }
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35

    我们通过这个测试⽤例,可以知道 EntityManager 使⽤起来还是⽐较容易的。不过在实际⼯作中,我不建议直接操作 EntityManager,因为如果你操作不熟练的话,会出现⼀些事务异常。因此我还是建议你通过 Spring Data JPA 给我们提供的 Repositories ⽅式进⾏操作。

    提示⼀下,你在写框架的时候可以直接操作 EntityManager,切记不要在任何业务代码⾥⾯都⽤到 EntityManager,否则⾃⼰的代码到最后就会很难维护。

    EntityManager 我们了解完了,那么我们再看下 @EnableJpaRepositories 对⾃定义 Repository 起了什么作⽤。

    11.2 @EnableJpaRepositories 详解

    下⾯分别从 @EnableJpaRepositories 的语法,以及其默认加载⽅式来详细介绍⼀下。

    11.2.1 @EnableJpaRepositories 的语法

    我们还是直接看代码,如下所示:

    public @interface EnableJpaRepositories {
        /**
         * value 等于 basePackages
         */
        String[] value() default {};
    
        /**
         * ⽤于配置扫描 Repositories 所在的 package 及⼦ package。
         * 可以配置为单个字符串。
         * eg: @EnableJpaRepositories(basePackages = "com.example")
         * 也可以配置为字符串数组形式,即多个情况。
         * eg: @EnableJpaRepositories(basePackages = {"com.sample.repository1", "com.sample.repository2"})
         * 默认 @SpringBootApplication 注解出现⽬录及其⼦⽬录。
         */
        String[] basePackages() default {};
    
        /**
         * 指定 Repository 类所在包,可以替换 basePackage 的使⽤。
         * ⼀样可以单个字符,下⾯例⼦表示 BookRepository.class 所在 Package 下⾯的所有
         * Repositories 都会被扫描注册。
         * eg: @EnableJpaRepositories(basePackageClasses = BookRepository.class)
         * 也可以多个字符,下⾯的例⼦代表 ShopRepository.class, OrganizationRepository.class 所在的 package下⾯的所有 Repositories 都会被扫描。
         * eg: @EnableJpaRepositories(basePackageClasses = {ShopRepository.class, OrganizationRepository.class})
         */
        Class<?>[] basePackageClasses() default {};
    
        /**
         * 指定包含的过滤器,该过滤器采⽤ ComponentScan 的过滤器,可以指定过滤器类型。
         * 下⾯的例⼦表示只扫描带 Repository 注解的类。
         * eg: @EnableJpaRepositories(includeFilters={@ComponentScan.Filter(type=FilterType.ANNOTATION, value=Repository.class)})
         */
        Filter[] includeFilters() default {};
    
        /**
         * 指定不包含过滤器,该过滤器也是采⽤ ComponentScan 的过滤器⾥⾯的类。
         * 下⾯的例⼦表示,带 @Service 和 @Controller 注解的类,不⽤扫描进去,当我们的项⽬变
         * ⼤了之后可以加快应⽤的启动速度。
         * eg: @EnableJpaRepositories(excludeFilters = {
         *
         * @ComponentScan.Filter(type = FilterType.ANNOTATION, value = Service.class),
         * @ComponentScan.Filter(type = FilterType.ANNOTATION, value = Controller.class)
         * })
         */
        Filter[] excludeFilters() default {};
    
        /**
         * 当我们⾃定义 Repository 的时候,约定的接⼝ Repository 的实现类的后缀是什么,默认是 Impl。
         */
        String repositoryImplementationPostfix() default "Impl";
    
        /**
         * named SQL 存放的位置,默认为 META-INF/jpa-named-queries.properties
         * eg: Todo.findBySearchTermNamedFile=SELECT t FROM Table t WHERE LOWER(t.description) LIKE LOWER(CONCAT('%', :searchTerm, '%')) ORDER BY t.title ASC
         * 这个你知道就⾏了,我建议不要⽤,因为它虽然功能很强⼤,但是,当我们使⽤了这么复杂的⽅法时,你需要想⼀想是否有更简单的⽅法。
         */
        String namedQueriesLocation() default "";
    
        /**
         * 构建条件查询的查找策略,包含三种⽅式:CREATE、USE_DECLARED_QUERY、
         * CREATE_IF_NOT_FOUND。
         * 正如我们前⼏课时介绍的:
         * - CREATE:按照接⼝名称⾃动构建查询⽅法,即我们前⾯说的 Defining Query Methods;
         * - USE_DECLARED_QUERY:⽤ @Query 这种⽅式查询;
         * - CREATE_IF_NOT_FOUND:如果有 @Query 注解,先以这个为准;如果不起作⽤,再⽤ Defining Query Methods;这个是默认的,基本不需要修改,我们知道就⾏了。
         */
        Key queryLookupStrategy() default Key.CREATE_IF_NOT_FOUND;
    
        /**
         * 指定⽣产 Repository 的⼯⼚类,默认 JpaRepositoryFactoryBean。
         * JpaRepositoryFactoryBean 的主要作⽤是以动态代理的⽅式,帮我们所有 Repository 的接⼝⽣成实现类。
         * 例如当我们通过断点,看到 UserRepository 的实现类是 SimpleJpaRepository 代理对象的时候,就是这个⼯⼚类⼲的,⼀般我们很少会去改变这个⽣成代理的机制。
         */
        Class<?> repositoryFactoryBeanClass() default JpaRepositoryFactoryBean.class;
    
        /**
         * ⽤来指定我们⾃定义的 Repository 的实现类是什么。
         * 默认是 DefaultRepositoryBaseClass,即表示没有指定的 Repository 的实现基类。
         */
        Class<?> repositoryBaseClass() default DefaultRepositoryBaseClass.class;
    
        /**
         * ⽤来指定创建和⽣产 EntityManager 的⼯⼚类是哪个
         * 默认是 name=“entityManagerFactory” 的 Bean。⼀般⽤于多数据配置。
         */
        String entityManagerFactoryRef() default "entityManagerFactory";
    
        /**
         * ⽤来指定默认的事务处理是哪个类
         * 默认是 transactionManager,⼀般⽤于多数据源
         */
        String transactionManagerRef() default "transactionManager";
    
        boolean considerNestedRepositories() default false;
    
        boolean enableDefaultTransactions() default true;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96

    11.2.2 @EnableJpaRepositories 的默认加载方式

    默认情况下是 spring boot 的⾃动加载机制,通过 spring.factories 的⽂件加载 JpaRepositoriesAutoConfiguration,如下图:

    在这里插入图片描述

    JpaRepositoriesAutoConfiguration ⾥⾯再进⾏ @Import(JpaRepositoriesImportSelector.class) 操作,而在 JpaRepositoriesImportSelector.class 中,则会选择加载类 JpaRepositoriesRegistrar,⽽ JpaRepositoriesRegistrar.class ⾥⾯配置了 @EnableJpaRepositories,从⽽使默认值产⽣了如下效果:

    在这里插入图片描述

    11.3 自定义 Repository 的实现类的方法

    定义⾃⼰的 Repository 的实现,有以下两种⽅法。

    11.3.1 第一种方法:定义独立的 Repository 的 Impl 实现类

    我们通过⼀个实例说明⼀下,假设我们要实现⼀个逻辑删除的功能,看看应该怎么做?

    第⼀步:定义⼀个 CustomizedUserRepository 接⼝。

    此接⼝会⾃动被 @EnableJpaRepositories 开启之后扫描到,代码如下:

    public interface CustomizedUserRepository {
        User logicallyDelete(User user); 
    }
    
    • 1
    • 2
    • 3

    第⼆步:创建⼀个 CustomizedUserRepositoryImpl 实现类。

    并且实现类⽤我们上⾯说过的 Impl 结尾,如下所示:

    public class CustomizedUserRepositoryImpl implements CustomizedUserRepository {
    
        private final EntityManager entityManager;
    
        public CustomizedUserRepositoryImpl(EntityManager entityManager) {
            this.entityManager = entityManager;
        }
    
        @Override
        public User logicallyDelete(User user) {
            user.setDeleted(true);
            return entityManager.merge(user);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    其中我们也发现了 EntityManager 的第⼆种注⼊⽅式,即直接放在构造⽅法⾥⾯,通过 Spring ⾃动注⼊。

    第三步:当⽤到 UserRepository 的时候,直接继承我们⾃定义的 CustomizedUserRepository 接⼝即可

    public interface UserRepository extends JpaRepository<User, Long>, JpaSpecificationExecutor<User>, CustomizedUserRepository {
    }
    
    • 1
    • 2

    第四步:写⼀个测试⽤例测试⼀下。

    @Test
    void testCustomizedUserRepository() {
        // 查出来⼀个 User 对象
        User user = userRepository.getById(2L);
        // 调⽤我们的逻辑删除⽅法进⾏删除
        userRepository.logicallyDelete(user);
        // 我们再重新查出来,看看值变了没有
        List<User> users = userRepository.findAll();
        Assertions.assertTrue(users.get(0).isDeleted());
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    最后调⽤刚才我们⾃定义的逻辑删除⽅法 logicallyDelete,跑⼀下测试⽤例,结果完全通过。那么此种⽅法的实现原理是什么呢?我们通过 debug 分析⼀下。

    11.3.2 第一种方法的原理分析

    我们在上⾯讲过 Class repositoryFactoryBeanClass() default JpaRepositoryFactoryBean.class,repository 的动态代理创建⼯⼚是:JpaRepositoryFactoryBean,它会帮我们⽣产 repository 的实现类,那么我们直接看⼀下 JpaRepositoryFactoryBean 的源码,分析其原理。

    在这里插入图片描述

    设置⼀个断点,就会发现,每个 Repository 都会构建⼀个 JpaRepositoryFactory,当 JpaRepositoryFactory 加载完之后会执⾏ afterPropertiesSet() ⽅法,找到 UserRepository 的 Fragment(即我们⾃定义的 CustomizedUserRepositoryImpl),如下所示:

    在这里插入图片描述

    我们再看 RepositoryFactory ⾥⾯的所有⽅法,如下图,⼀看就是动态代理⽣成 Repository 的实现类,我们进到这个⽅法⾥⾯设置个断点继续观察。org.springframework.data.repository.core.support.RepositoryFactorySupport#getRepository()

    在这里插入图片描述

    然后我们通过断点可以看到,fragments 放到了 composition ⾥⾯,最后⼜放到了 advice ⾥⾯,最后才⽣成了我们的 repository 的代理类。这时我们再打开 repository 详细地看看⾥⾯的值。

    在这里插入图片描述

    可以看到 repository ⾥⾯的 interfaces,就是我们刚才测试 userRepository ⾥⾯的接⼝定义的。

    在这里插入图片描述

    我们可以看到 advisors ⾥⾯第八个就是我们⾃定义的接⼝的实现类,从这⾥可以得出结论:spring 通过扫描所有 repository 的接⼝和实现类,并且通过 aop 的切⾯和动态代理的⽅式,可以知道我们⾃定义的接⼝的实现类是什么。

    针对不同的 repository ⾃定义的接⼝和实现类,需要我们⼿动去 extends,这种⽐较适合不同的业务场景有各⾃的 repository 的实现情况。还有⼀种⽅法是我们直接改变动态代理的实现类,我们接着看。

    11.3.3 第二种方法:通过 @EnableJpaRepositories 定义默认的实现类

    当⾯对复杂业务的时候,难免会⾃定义⼀些公⽤的⽅法,或者覆盖⼀些默认实现的情况。

    举个例⼦:很多时候线上的数据是不允许删除的,所以这个时候需要我们覆盖

    SimpleJpaRepository ⾥⾯的删除⽅法,换成更新,进⾏逻辑删除,⽽不是物理删除。那么接下来我们看看应该怎么做?

    第一步:创建 CustomerBaseRepository 继承 SimpleJpaRepository 即可。

    继承 SimpleJpaRepository 之后,我们直接覆盖 delete ⽅法即可,代码如下:

    public class CustomerBaseRepository<T extends BaseEntity, ID> extends SimpleJpaRepository<T, ID> {
    
        private final EntityManager em;
    
    
        public CustomerBaseRepository(JpaEntityInformation<T, ?> entityInformation, EntityManager entityManager) {
            super(entityInformation, entityManager);
            this.em = entityManager;
        }
    
        public CustomerBaseRepository(Class<T> domainClass, EntityManager em) {
            super(domainClass, em);
            this.em = em;
        }
    
        /**
         * 覆盖删除⽅法,实现逻辑删除,换成更新⽅法
         *
         * @param entity must not be {@literal null}.
         */
        @Transactional
        @Override
        public void delete(T entity) {
            entity.setDeleted(Boolean.TRUE);
            em.merge(entity);
        }
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28

    需要注意的是,这⾥需要覆盖⽗类的构造⽅法,接收 EntityManager,并赋值给⾃⼰类⾥⾯的私有变量。

    第二步:正如上⾯我们讲的利⽤ @EnableJpaRepositories 指定 repositoryBaseClass,

    @EnableJpaRepositories(
            repositoryBaseClass = CustomerBaseRepository.class
    )
    @SpringBootApplication
    public class JpaApplication {
    
        public static void main(String[] args) {
            SpringApplication.run(JpaApplication.class, args);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    可以看出,在启动项⽬的时候,通过 @EnableJpaRepositories 指定我们 repositoryBaseClass 的基类是 CustomerBaseRepository。

    第三步:写⼀个测试⽤例测试⼀下。

    @Test
    void testCustomizedBaseRepository() {
        // 查出来⼀个 User 对象
        User user = userRepository.getById(2L);
        // 调⽤逻辑删除⽅法进⾏删除
        userRepository.delete(user);
        // 重新查出来,看看值变了没有
        List<User> users = userRepository.findAll();
        Assertions.assertTrue(users.get(0).isDeleted());
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    你可以发现,我们执⾏完“删除”之后,数据库⾥⾯的 User 还在,只不过 deleted,变成了已删除状态。那么这是为什么呢?我们分析⼀下原理。

    11.3.4 第二种方法的原理分析

    还是打开 RepositoryFactory ⾥⾯的⽗类⽅法,它会根据 @EnableJpaRepositories ⾥⾯我们配置的 repositoryBaseClass,加载我们⾃定义的实现类,关键⽅法如下:

    org.springframework.data.repository.core.support.RepositoryFactorySupport#getRepositoryInformation()

    在这里插入图片描述

    我们还看刚才的⽅法的断点,如下:

    在这里插入图片描述

    可以看到 information 已经变成了我们扩展的基类了,⽽最终⽣成的 repository 的实现类也换成了 CustomerBaseRepository。

    11.4 实际应用场景

    在实际⼯作中,有哪些场景会⽤到⾃定义 Repository 呢?

    1. ⾸先肯定是我们做框架的时候、解决⼀些通⽤问题的时候,如逻辑删除,正如我们上⾯的实例所示的样⼦。
    2. 在实际⽣产中经常会有这样的场景:对外暴露的是 UUID 查询⽅法,⽽对内暴露的是 Long 类型的 ID,这时候我们就可以⾃定义⼀个 FindByIdOrUUID 的底层实现⽅法,可以选择在⾃定义的 Respository 接⼝⾥⾯实现。
    3. Defining Query Methods 和 @Query 满⾜不了我们的查询,但是我们⼜想⽤它的⽅法语义的时候,就可以考虑实现不同的 Respository 的实现类,来满⾜我们不同业务场景的复杂查询。我⻅过有团队这样⽤过,不过个⼈感觉⼀般⽤不到,如果你⽤到了说明你的代码肯定有优化空间,代码不应该过于复杂。

    上⾯我们讲到了逻辑删除,还有⼀个是利⽤ @SQLDelete 也可以做到,⽤法如下:

    @SQLDelete(sql = "UPDATE user SET is_deleted = TRUE WHERE is_deleted =FALSE AND id = ?")
    public class User extends BaseEntity {
    ....
    }
    
    • 1
    • 2
    • 3
    • 4

    这个时候不需要我们⾃定义 Respository 也可做到,这个⽅法的优点就是灵活,⽽缺点就是需要我们⼀个⼀个配置在实体上⾯。你可以根据实际场景⾃由选择⽅式。

    11.5 本章小结

    本章主要通过介绍 EntityManager 和 @EnableJpaRepositories,实现了我们⾃定义 Repository 的两种⽅法。

  • 相关阅读:
    进程调度的原理和算法探析
    8.词袋和词向量模型
    log4j2漏洞复现
    (免费分享)基于springboot,vue在线考试系统
    ClickHouse 物化视图
    高效防汛决策:山海鲸可视化系统助力城市防洪
    震裕科技-300953 三季报分析(20231108)
    Java中实体与Map的相互转换
    nginx模块
    设计模式学习笔记 - 设计原则 - 2.开闭原则
  • 原文地址:https://blog.csdn.net/qq_40161813/article/details/126081195