Spring Data JPA 原理和第七天 JpaRepoitory进阶、Auditing使用
11 JpaRepoitory 如何自定义?
通过前面课时的内容,相信你已经掌握了很多 Repository 的高级用法,但是在实际工作场景中也难免会出现自定义 Repsitory 实现类的场景,这一课时我们就来看一下如何定义自己的 Repository 实现类。要知道 JPA 的操作核心是 EntityManager,那么我们先看看 Entitymanager 究竟为何物。
EntityManager 介绍
Java Persistence API 规定,操作数据库实体必须要通过 EntityManager 进行,而我们前面看到了所有的 Repository 在 JPA 里面的实现类是 SimpleJpaRepository,它在真正操作实体的时候都是调用 EntityManager 里面的方法。
我们在 SimpleJpaRepository 里面设置一个断点,这样可以很容易看得出来 EntityManger 是 JPA 的接口协议,而其现类是 Hibernate 里面的 SessionImpl,如下图所示:
那么我们看看 EntityManager 给我们提供了哪些方法。
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里面怎么用的,我们怎么用就可以了;
}
这一课时我们先知道 EntityManager 的语法和用法就好,在之后的第 21 课时介绍 Persistence Context 的时候,再详细讲一下其对实体状态的影响,以及每种状态代表什么意思。
那么现在你知道了这些语法,该怎么使用呢?
EntityManager 如何使用?
它的使用方法很简单,我们在任何地方只要能获得 EntityManager,就可以进行里面的操作。
获得 EntityManager 的方式:通过 @PersistenceContext 注解。
将 @PersistenceContext 注解标注在 EntityManager 类型的字段上,这样得到的 EntityManager 就是容器管理的 EntityManager。由于是容器管理的,所以我们不需要、也不应该显式关闭注入的 EntityManager 实例。
下面是关于这种方式的例子,我们想要在测试类中获得 @PersistenceContext 里面的 EntityManager,看看代码应该怎么写。
@DataJpaTest
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
public class UserRepositoryTest {
//利用该方式获得entityManager
@PersistenceContext
private EntityManager entityManager;
@Autowired
private UserRepository userRepository;
/**
* 测试entityManager用法
*
* @throws JsonProcessingException
*/
@Test
@Rollback(false)
public void testEntityManager() throws JsonProcessingException {
//测试找到一个User对象
User user = entityManager.find(User.class,2L);
Assertions.assertEquals(user.getAddresses(),"shanghai");
<span class="hljs-comment">//我们改变一下user的删除状态</span>
user.setDeleted(<span class="hljs-keyword">true</span>);
<span class="hljs-comment">//merger方法</span>
entityManager.merge(user);
<span class="hljs-comment">//更新到数据库里面</span>
entityManager.flush();
<span class="hljs-comment">//再通过createQuery创建一个JPQL,进行查询</span>
List<User> users = entityManager.createQuery(<span class="hljs-string">"select u From User u where u.name=?1"</span>)
.setParameter(<span class="hljs-number">1</span>,<span class="hljs-string">"jack"</span>)
.getResultList();
Assertions.assertTrue(users.get(<span class="hljs-number">0</span>).getDeleted());
}
}
我们通过这个测试用例,可以知道 EntityManager 使用起来还是比较容易的。不过在实际工作中,我不建议直接操作 EntityManager,因为如果你操作不熟练的话,会出现一些事务异常。因此我还是建议你通过 Spring Data JPA 给我们提供的 Repositories 方式进行操作。
提示一下,你在写框架的时候可以直接操作 EntityManager,切记不要在任何业务代码里面都用到 EntityManager,否则自己的代码到最后就会很难维护。
EntityManager 我们了解完了,那么我们再看下 @EnableJpaRepositories 对自定义 Repository 起了什么作用。
@EnableJpaRepositories 详解
下面分别从 @EnableJpaRepositories 的语法,以及其默认加载方式来详细介绍一下。
@EnableJpaRepositories 语法
我们还是直接看代码,如下所示:
public @interface EnableJpaRepositories {
String[] value() default {};
String[] basePackages() default {};
Class<?>[] basePackageClasses() default {};
Filter[] includeFilters() default {};
Filter[] excludeFilters() default {};
String repositoryImplementationPostfix() default "Impl";
String namedQueriesLocation() default "";
Key queryLookupStrategy() default Key.CREATE_IF_NOT_FOUND;
Class<?> repositoryFactoryBeanClass() default JpaRepositoryFactoryBean.class;
Class<?> repositoryBaseClass() default DefaultRepositoryBaseClass.class;
String entityManagerFactoryRef() default "entityManagerFactory";
String transactionManagerRef() default "transactionManager";
boolean considerNestedRepositories() default false;
boolean enableDefaultTransactions() default true;
}
下面我对里面的 10 个方法进行一下具体说明:
1)value 等于 basePackage
用于配置扫描 Repositories 所在的 package 及子 package。
可以配置为单个字符串。
@EnableJpaRepositories(basePackages = "com.example")
也可以配置为字符串数组形式,即多个情况。
@EnableJpaRepositories(basePackages = {"com.sample.repository1", "com.sample.repository2"})
默认 @SpringBootApplication 注解出现目录及其子目录。
2)basePackageClasses
指定 Repository 类所在包,可以替换 basePackage 的使用。
一样可以单个字符,下面例子表示 BookRepository.class 所在 Package 下面的所有 Repositories 都会被扫描注册。
@EnableJpaRepositories(basePackageClasses = BookRepository.class)
也可以多个字符,下面的例子代表 ShopRepository.class, OrganizationRepository.class 所在的 package下面的所有 Repositories 都会被扫描。
@EnableJpaRepositories(basePackageClasses = {ShopRepository.class, OrganizationRepository.class})
3)includeFilters
指定包含的过滤器,该过滤器采用 ComponentScan 的过滤器,可以指定过滤器类型。
下面的例子表示只扫描带 Repository 注解的类。
@EnableJpaRepositories( includeFilters={@ComponentScan.Filter(type=FilterType.ANNOTATION, value=Repository.class)})
4)excludeFilters
指定不包含过滤器,该过滤器也是采用 ComponentScan 的过滤器里面的类。
下面的例子表示,带 @Service 和 @Controller 注解的类,不用扫描进去,当我们的项目变大了之后可以加快应用的启动速度。
@EnableJpaRepositories(excludeFilters={@ComponentScan.Filter(type=FilterType.ANNOTATION, value=Service.class),@ComponentScan.Filter(type=FilterType.ANNOTATION, value=Controller.class)})
5)repositoryImplementationPostfix
当我们自定义 Repository 的时候,约定的接口 Repository 的实现类的后缀是什么,默认是 Impl。例子我在下面详细讲解。
6)namedQueriesLocation
named SQL 存放的位置,默认为 META-INF/jpa-named-queries.properties
例子如下:
Todo.findBySearchTermNamedFile=SELECT t FROM Table t WHERE LOWER(t.description) LIKE LOWER(CONCAT('%', :searchTerm, '%')) ORDER BY t.title ASC
这个你知道就行了,我建议不要用,因为它虽然功能很强大,但是,当我们使用了这么复杂的方法时,你需要想一想是否有更简单的方法。
7)queryLookupStrategy
构建条件查询的查找策略,包含三种方式:CREATE、USE_DECLARED_QUERY、CREATE_IF_NOT_FOUND。
正如我们前几课时介绍的:
-
CREATE:按照接口名称自动构建查询方法,即我们前面说的 Defining Query Methods;
-
USE_DECLARED_QUERY:用 @Query 这种方式查询;
-
CREATE_IF_NOT_FOUND:如果有 @Query 注解,先以这个为准;如果不起作用,再用 Defining Query Methods;这个是默认的,基本不需要修改,我们知道就行了。
8)repositoryFactoryBeanClass
指定生产 Repository 的工厂类,默认 JpaRepositoryFactoryBean。JpaRepositoryFactoryBean 的主要作用是以动态代理的方式,帮我们所有 Repository 的接口生成实现类。例如当我们通过断点,看到 UserRepository 的实现类是 SimpleJpaRepository 代理对象的时候,就是这个工厂类干的,一般我们很少会去改变这个生成代理的机制。
9)entityManagerFactoryRef
用来指定创建和生产 EntityManager 的工厂类是哪个,默认是 name=“entityManagerFactory” 的 Bean。一般用于多数据配置。
10)Class<?> repositoryBaseClass()
用来指定我们自定义的 Repository 的实现类是什么。默认是 DefaultRepositoryBaseClass,即表示没有指定的 Repository 的实现基类。
11)String transactionManagerRef() default "transactionManager"
用来指定默认的事务处理是哪个类,默认是 transactionManager,一般用于多数据源。
@EnableJpaRepositories 默认加载方式
默认情况下是 spring boot 的自动加载机制,通过 spring.factories 的文件加载 JpaRepositoriesAutoConfiguration,如下图:
JpaRepositoriesAutoConfiguration 里面再进行 @Import(JpaRepositoriesRegistrar.class) 操作,显示如下:
而 JpaRepositoriesRegistrar.class 里面配置了 @EnableJpaRepositories,从而使默认值产生了如下效果:
这样关于 @EnableJpaRepositories 的语法以及默认加载方式就介绍完了,你就可以知道通过 @EnableJpaRepositories 可以完成很多我们自定义的需求。那么到底如何定义自己的 Repository 的实现类呢?我们接着看。
自定义 Repository 的 impl 的方法
定义自己的 Repository 的实现,有以下两种方法。
第一种方法:定义独立的 Repository 的 Impl 实现类
我们通过一个实例说明一下,假设我们要实现一个逻辑删除的功能,看看应该怎么做?
第一步:定义一个 CustomizedUserRepository 接口。
此接口会自动被 @EnableJpaRepositories 开启之后扫描到,代码如下:
package com.example.jpa.example1.customized;
import com.example.jpa.example1.User;
public interface CustomizedUserRepository {
User logicallyDelete(User user);
}
第二步:创建一个 CustomizedUserRepositoryImpl 实现类。
并且实现类用我们上面说过的 Impl 结尾,如下所示:
package com.example.jpa.example1.customized;
import com.example.jpa.example1.User;
import javax.persistence.EntityManager;
public class CustomizedUserRepositoryImpl implements CustomizedUserRepository {
private EntityManager entityManager;
public CustomizedUserRepositoryImpl(EntityManager entityManager) {
this.entityManager = entityManager;
}
@Override
public User logicallyDelete(User user) {
user.setDeleted(true);
return entityManager.merge(user);
}
}
其中我们也发现了 EntityManager 的第二种注入方式,即直接放在构造方法里面,通过 Spring 自动注入。
第三步:当用到 UserRepository 的时候,直接继承我们自定义的 CustomizedUserRepository 接口即可。
public interface UserRepository extends JpaRepository<User,Long>, JpaSpecificationExecutor<User>, CustomizedUserRepository {
}
第四步:写一个测试用例测试一下。
@Test
public void testCustomizedUserRepository() {
//查出来一个User对象
User user = userRepository.findById(2L).get();
//调用我们的逻辑删除方法进行删除
userRepository.logicallyDelete(user);
//我们再重新查出来,看看值变了没有
List<User> users = userRepository.findAll();
Assertions.assertEquals(users.get(0).getDeleted(),Boolean.TRUE);
}
最后调用刚才我们自定义的逻辑删除方法 logicallyDelete,跑一下测试用例,结果完全通过。那么此种方法的实现原理是什么呢?我们通过 debug 分析一下。
原理分析
我们在上面讲过 Class<?> repositoryFactoryBeanClass() default JpaRepositoryFactoryBean.class,repository 的动态代理创建工厂是: JpaRepositoryFactoryBean,它会帮我们生产 repository 的实现类,那么我们直接看一下JpaRepositoryFactoryBean 的源码,分析其原理。
设置一个断点,就会发现,每个 Repository 都会构建一个 JpaRepositoryFactory,当 JpaRepositoryFactory 加载完之后会执行 afterPropertiesSet() 方法,找到 UserRepository 的 Fragment(即我们自定义的 CustomizedUserRepositoryImpl),如下所示:
我们再看 RepositoryFactory 里面的所有方法,如下图,一看就是动态代理生成 Repository 的实现类,我们进到这个方法里面设置个断点继续观察。
然后我们通过断点可以看到,fragments 放到了 composition 里面,最后又放到了 advice 里面,最后才生成了我们的 repository 的代理类。这时我们再打开 repository 详细地看看里面的值。
可以看到 repository 里面的 interfaces,就是我们刚才测试 userRepository 里面的接口定义的。
我们可以看到 advisors 里面第六个就是我们自定义的接口的实现类,从这里可以得出结论:spring 通过扫描所有 repository 的接口和实现类,并且通过 aop 的切面和动态代理的方式,可以知道我们自定义的接口的实现类是什么。
针对不同的 repository 自定义的接口和实现类,需要我们手动去 extends,这种比较适合不同的业务场景有各自的 repository 的实现情况。还有一种方法是我们直接改变动态代理的实现类,我们接着看。
第二种方法:通过 @EnableJpaRepositories 定义默认的 Repository 的实现类
当面对复杂业务的时候,难免会自定义一些公用的方法,或者覆盖一些默认实现的情况。举个例子:很多时候线上的数据是不允许删除的,所以这个时候需要我们覆盖 SimpleJpaRepository 里面的删除方法,换成更新,进行逻辑删除,而不是物理删除。那么接下来我们看看应该怎么做?
第一步:正如上面我们讲的利用 @EnableJpaRepositories 指定 repositoryBaseClass,代码如下:
@SpringBootApplication
@EnableWebMvc
@EnableJpaRepositories(repositoryImplementationPostfix = "Impl",repositoryBaseClass = CustomerBaseRepository.class)
public class JpaApplication {
public static void main(String[] args) {
SpringApplication.run(JpaApplication.class, args);
}
}
可以看出,在启动项目的时候,通过 @EnableJpaRepositories 指定我们 repositoryBaseClass 的基类是 CustomerBaseRepository。
第二步:创建 CustomerBaseRepository 继承 SimpleJpaRepository 即可。
继承 SimpleJpaRepository 之后,我们直接覆盖 delete 方法即可,代码如下:
package com.example.jpa.example1.customized;
import org.springframework.data.jpa.repository.support.JpaEntityInformation;
import org.springframework.data.jpa.repository.support.SimpleJpaRepository;
import org.springframework.transaction.annotation.Transactional;
import javax.persistence.EntityManager;
@Transactional(readOnly = true)
public class CustomerBaseRepository<T extends BaseEntity,ID> extends SimpleJpaRepository<T,ID> {
private final JpaEntityInformation<T, ?> entityInformation;
private final EntityManager em;
public CustomerBaseRepository(JpaEntityInformation<T, ?> entityInformation, EntityManager entityManager) {
super(entityInformation, entityManager);
this.entityInformation = entityInformation;
this.em = entityManager;
}
public CustomerBaseRepository(Class<T> domainClass, EntityManager em) {
super(domainClass, em);
entityInformation = null;
this.em = em;
}
//覆盖删除方法,实现逻辑删除,换成更新方法
@Transactional
@Override
public void delete(T entity) {
entity.setDeleted(Boolean.TRUE);
em.merge(entity);
}
}
需要注意的是,这里需要覆盖父类的构造方法,接收 EntityManager,并赋值给自己类里面的私有变量。
第三步:写一个测试用例测试一下。
@Test
public void testCustomizedBaseRepository() {
User user = userRepository.findById(2L).get();
userRepository.logicallyDelete(user);
userRepository.delete(user);
List<User> users = userRepository.findAll();
Assertions.assertEquals(users.get(0).getDeleted(),Boolean.TRUE);
}
你可以发现,我们执行完“删除”之后,数据库里面的 User 还在,只不过 deleted,变成了已删除状态。那么这是为什么呢?我们分析一下原理。
原理分析
还是打开 RepositoryFactory 里面的父类方法,它会根据 @EnableJpaRepositories 里面我们配置的 repositoryBaseClass,加载我们自定义的实现类,关键方法如下:
我们还看刚才的方法的断点,如下:
可以看到 information 已经变成了我们扩展的基类了,而最终生成的 repository 的实现类也换成了 CustomerBaseRepository。
自定义的方法,我们讲完了,那么它都会在哪些实际场景用到呢?接着看一下。
实际应用场景是什么?
在实际工作中,有哪些场景会用到自定义 Repository 呢?
-
首先肯定是我们做框架的时候、解决一些通用问题的时候,如逻辑删除,正如我们上面的实例所示的样子。
-
在实际生产中经常会有这样的场景:对外暴露的是 UUID 查询方法,而对内暴露的是 Long 类型的 ID,这时候我们就可以自定义一个 FindByIdOrUUID 的底层实现方法,可以选择在自定义的 Respository 接口里面实现。
-
Defining Query Methods 和 @Query 满足不了我们的查询,但是我们又想用它的方法语义的时候,就可以考虑实现不同的 Respository 的实现类,来满足我们不同业务场景的复杂查询。我见过有团队这样用过,不过个人感觉一般用不到,如果你用到了说明你的代码肯定有优化空间,代码不应该过于复杂。
上面我们讲到了逻辑删除,还有一个是利用 @SQLDelete 也可以做到,用法如下:
@SQLDelete(sql = "UPDATE user SET deleted = true where deleted =false and id = ?")
public class User implements Serializable {
....
}
这个时候不需要我们自定义 Respository 也可做到,这个方法的优点就是灵活,而缺点就是需要我们一个一个配置在实体上面。你可以根据实际场景自由选择方式。
总结
到这里,本课时的内容也介绍完了。我们通过介绍 EntityManager 和 @EnableJpaRepositories,实现了我们自定义 Repository 的两种方法,你可以学习一下我的分析问题思路,进而应用到自身,学会举一反三。
也希望你踊跃留言,我们一起讨论,一起进步。下一课时我要讲讲实战过程中,我们的基类应该如何设计。
点击下方链接查看源码(不定时更新)
https://github.com/zhangzhenhuajack/spring-boot-guide/tree/master/spring-data/spring-data-jpa
12 JPA 的审计功能解决了哪些问题?
今天我们来讲一下 JPA 的审计功能,即 Auditing,通过了解这一概念及其实现原理,分析这一功能可以帮我们解决哪些问题。
在学习的过程中,希望你可以跟着我的步骤去思考,并动动手自己去实践一下。希望通过本课时的学习,你可以掌握 JPA 审计功能的相关内容,在实操中运用起来会更加得心应手。
Auditing 指的是什么?
Auditing 是帮我们做审计用的,当我们操作一条记录的时候,需要知道这是谁创建的、什么时间创建的、最后修改人是谁、最后修改时间是什么时候,甚至需要修改记录……这些都是 Spring Data JPA 里面的 Auditing 支持的,它为我们提供了四个注解来完成上面说的一系列事情,如下:
-
@CreatedBy 是哪个用户创建的。
-
@CreatedDate 创建的时间。
-
@LastModifiedBy 最后修改实体的用户。
-
@LastModifiedDate 最后一次修改的时间。
这就是 Auditing 了,那么它具体怎么实现呢?
Auditing 如何实现?
利用上面的四个注解实现方法,一共有三种方式实现 Auditing,我们分别看看。
第一种方式:直接在实例里面添加上述四个注解
我们还用之前的例子,把 User 实体添加四个字段,分别记录创建人、创建时间、最后修改人、最后修改时间。
第一步:在 @Entity:User 里面添加四个注解,并且新增 @EntityListeners(AuditingEntityListener.class) 注解。
添加完之后,User 的实体代码如下:
@Entity
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
@ToString(exclude = "addresses")
@EntityListeners(AuditingEntityListener.class)
public class User implements Serializable {
@Id
@GeneratedValue(strategy= GenerationType.AUTO)
private Long id;
private String name;
private String email;
@Enumerated(EnumType.STRING)
private SexEnum sex;
private Integer age;
@OneToMany(mappedBy = "user")
@JsonIgnore
private List<UserAddress> addresses;
private Boolean deleted;
@CreatedBy
private Integer createUserId;
@CreatedDate
private Date createTime;
@LastModifiedBy
private Integer lastModifiedUserId;
@LastModifiedDate
private Date lastModifiedTime;
}
在 @Entity 实体中我们需要做两点操作:
1.其中最主要的四个字段分别记录创建人、创建时间、最后修改人、最后修改时间,代码如下:
@CreatedBy
private Integer createUserId;
@CreatedDate
private Date createTime;
@LastModifiedBy
private Integer lastModifiedUserId;
@LastModifiedDate
private Date lastModifiedTime;
2.其中 AuditingEntityListener 不能少,必须通过这段代码:
@EntityListeners(AuditingEntityListener.class)
在 Entity 的实体上面进行注解。
第二步:实现 AuditorAware 接口,告诉 JPA 当前的用户是谁。
我们需要实现 AuditorAware 接口,以及 getCurrentAuditor 方法,并返回一个 Integer 的 user ID。
public class MyAuditorAware implements AuditorAware<Integer> {
//需要实现AuditorAware接口,返回当前的用户ID
@Override
public Optional<Integer> getCurrentAuditor() {
ServletRequestAttributes servletRequestAttributes =
(ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
Integer userId = (Integer) servletRequestAttributes.getRequest().getSession().getAttribute("userId");
return Optional.ofNullable(userId);
}
}
这里关键的一步,是实现 AuditorAware 接口的方法,如下所示:
public interface AuditorAware<T> {
T getCurrentAuditor();
}
需要注意的是:这里获得用户 ID 的方法不止这一种,实际工作中,我们可能将当前的 user 信息放在 Session 中,可能把当前信息放在 Redis 中,也可能放在 Spring 的 security 里面管理。此外,这里的实现会有略微差异,我们以 security 为例:
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication == null || !authentication.isAuthenticated()) {
return null;
}
Integer userId = ((LoginUserInfo) authentication.getPrincipal()).getUser().getId();
这时获取 userId 的代码可能会变成上面这样子,你了解一下就好。
第三步:通过 @EnableJpaAuditing 注解开启 JPA 的 Auditing 功能。
第三步是最重要的一步,如果想使上面的配置生效,我们需要开启 JPA 的 Auditing 功能(默认没开启)。这里需要用到的注解是 @EnableJpaAuditing,代码如下:
@Inherited
@Documented
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Import(JpaAuditingRegistrar.class)
public @interface EnableJpaAuditing {
//auditor用户的获取方法,默认是找AuditorAware的实现类;
String auditorAwareRef() default "";
//是否在创建修改的时候设置时间,默认是true
boolean setDates() default true;
//在创建的时候是否同时作为修改,默认是true
boolean modifyOnCreate() default true;
//时间的生成方法,默认是取当前时间(为什么提供这个功能呢?因为测试的时候有可能希望时间保持不变,它提供了一种自定义的方法);
String dateTimeProviderRef() default "";
}
在了解了@EnableJpaAuditing注解之后,我们需要创建一个Configuration 文件,添加 @EnableJpaAuditing 注解,并且把我们的 MyAuditorAware 加载进去即可,如下所示:
@Configuration
@EnableJpaAuditing
public class JpaConfiguration {
@Bean
@ConditionalOnMissingBean(name = "myAuditorAware")
MyAuditorAware myAuditorAware() {
return new MyAuditorAware();
}
}
经验之谈:
-
这里说一个 Congifuration 的最佳实践的写法。我们为什么要单独写一个JpaConfiguration的配置文件,而不是把@EnableJpaAuditing 放在 JpaApplication 的类里面呢?因为这样的话 JpaConfiguration 文件可以单独加载、单独测试,如果都放在 Appplication 类里面的话,岂不是每次测试都要启动整个应用吗?
-
MyAuditorAware 也可以通过 @Component 注解进行加载,我为什么推荐 @Bean 的方式呢?因为这种方式可以让使用的人直接通过我们的配置文件知道我们自定义了哪些组件,不会让用的人产生不必要的惊讶,这是一点写 framework 的经验,供你参考。
第四步:我们写个测试用例测试一下。
@DataJpaTest
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
@Import(JpaConfiguration.class)
public class UserRepositoryTest {
@Autowired
private UserRepository userRepository;
@MockBean
MyAuditorAware myAuditorAware;
@Test
public void testAuditing() {
//由于测试用例模拟web context环境不是我们的重点,我们这里利用@MockBean,mock掉我们的方法,期待返回13这个用户ID
Mockito.when(myAuditorAware.getCurrentAuditor()).thenReturn(Optional.of(13));
//我们没有显式的指定更新时间、创建时间、更新人、创建人
User user = User.builder()
.name("jack")
.email("123456@126.com")
.sex(SexEnum.BOY)
.age(20)
.build();
userRepository.save(user);
//验证是否有创建时间、更新时间,UserID是否正确;
List<User> users = userRepository.findAll();
Assertions.assertEquals(13,users.get(0).getCreateUserId());
Assertions.assertNotNull(users.get(0).getLastModifiedTime());
System.out.println(users.get(0));
}
}
需要注意的是:
-
我们利用 @MockBean 模拟 MyAuditorAware 返回结果 13 这个 UserID;
-
我们测试并验证 create_user_id 是否是我们预期的。
测试结果如下:
User(id=1, name=jack, email=123456@126.com, sex=BOY, age=20, deleted=null, createUserId=13, createTime=Sat Oct 03 21:19:57 CST 2020, lastModifiedUserId=13, lastModifiedTime=Sat Oct 03 21:19:57 CST 2020)
结果完全符合我们的预期。
那么现在是不是学会了 Auditing 的第一种方式呢?此外,Spring Data JPA 还给我们提供了第二种方式:实体直接实现 Auditable 接口即可,我们来看一下。
第二种方式:实体里面实现Auditable 接口
我们改一下上面的 User 实体对象,如下:
@Entity
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
@ToString(exclude = "addresses")
@EntityListeners(AuditingEntityListener.class)
public class User implements Auditable<Integer,Long, Instant> {
@Id
@GeneratedValue(strategy= GenerationType.AUTO)
private Long id;
private String name;
private String email;
@Enumerated(EnumType.STRING)
private SexEnum sex;
private Integer age;
@OneToMany(mappedBy = "user")
@JsonIgnore
private List<UserAddress> addresses;
private Boolean deleted;
private Integer createUserId;
private Instant createTime;
private Integer lastModifiedUserId;
private Instant lastModifiedTime;
@Override
public Optional<Integer> getCreatedBy() {
return Optional.ofNullable(this.createUserId);
}
@Override
public void setCreatedBy(Integer createdBy) {
this.createUserId = createdBy;
}
@Override
public Optional<Instant> getCreatedDate() {
return Optional.ofNullable(this.createTime);
}
@Override
public void setCreatedDate(Instant creationDate) {
this.createTime = creationDate;
}
@Override
public Optional<Integer> getLastModifiedBy() {
return Optional.ofNullable(this.lastModifiedUserId);
}
@Override
public void setLastModifiedBy(Integer lastModifiedBy) {
this.lastModifiedUserId = lastModifiedBy;
}
@Override
public void setLastModifiedDate(Instant lastModifiedDate) {
this.lastModifiedTime = lastModifiedDate;
}
@Override
public Optional<Instant> getLastModifiedDate() {
return Optional.ofNullable(this.lastModifiedTime);
}
@Override
public boolean isNew() {
return id==null;
}
}
与第一种方式的差异是,这里我们要去掉上面说的四个注解,并且要实现接口 Auditable 的方法,代码会变得很冗余和啰唆。
而其他都不变,我们再跑一次刚才的测试用例,发现效果是一样的。从代码的复杂程度来看,这种方式我不推荐使用。那么我们再看一下第三种方式。
第三种方式:利用 @MappedSuperclass 注解
我们在第 6 课时讲对象的多态的时候提到过这个注解,它主要是用来解决公共 BaseEntity 的问题,而且其代表的是继承它的每一个类都是一个独立的表。
我们先看一下 @MappedSuperclass 的语法。
它注解里面什么都没有,其实就是代表了抽象关系,即所有子类的公共字段而已。那么接下来我们看一下实例。
第一步:创建一个 BaseEntity,里面放一些实体的公共字段和注解。
package com.example.jpa.example1.base;
import org.springframework.data.annotation.*;
import javax.persistence.MappedSuperclass;
import java.time.Instant;
@Data
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public class BaseEntity {
@CreatedBy
private Integer createUserId;
@CreatedDate
private Instant createTime;
@LastModifiedBy
private Integer lastModifiedUserId;
@LastModifiedDate
private Instant lastModifiedTime;
}
注意: BaseEntity里面需要用上面提到的四个注解,并且加上@EntityListeners(AuditingEntityListener.class),这样所有的子类就不需要加了。
第二步:实体直接继承 BaseEntity 即可。
我们修改一下上面的 User 实例继承 BaseEntity,代码如下:
@Entity
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
@ToString(exclude = "addresses")
public class User extends BaseEntity {
@Id
@GeneratedValue(strategy= GenerationType.AUTO)
private Long id;
private String name;
private String email;
@Enumerated(EnumType.STRING)
private SexEnum sex;
private Integer age;
@OneToMany(mappedBy = "user")
@JsonIgnore
private List<UserAddress> addresses;
private Boolean deleted;
}
这样的话,User 实体就不需要关心太多,我们只关注自己需要的逻辑即可,如下:
-
去掉了 @EntityListeners(AuditingEntityListener.class);
-
去掉了 @CreatedBy、@CreatedDate、@LastModifiedBy、@LastModifiedDate 四个注解的公共字段。
接着我们再跑一下上面的测试用例,发现效果还是一样的。
这种方式,是我最推荐的,也是实际工作中使用最多的一种方式。它的好处显而易见就是公用性强,代码简单,需要关心的少。
通过上面的实际案例,我们其实也能很容易发现 Auditing 帮我们解决了什么问题,下面总结一下。
JPA 的审计功能解决了哪些问题?
1.可以很容易地让我们写自己的 BaseEntity,把一些公共的字段放在里面,不需要我们关心太多和业务无关的字段,更容易让我们公司的表更加统一和规范,就是统一加上 @CreatedBy、@CreatedDate、@LastModifiedBy、@LastModifiedDate 等。
实际工作中,BaseEntity 可能还更复杂一点,比如说把 ID 和 @Version 加进去,会变成如下形式:
@Data
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public class BaseEntity {
@Id
@GeneratedValue(strategy= GenerationType.AUTO)
private Long id;
@CreatedBy
private Integer createUserId;
@CreatedDate
private Instant createTime;
@LastModifiedBy
private Integer lastModifiedUserId;
@LastModifiedDate
private Instant lastModifiedTime;
@Version
private Integer version;
}
其中 @Version 的详细使用方法,我们在 14 课时讲乐观锁的机制时再详细讲解。
2.Auditing 在实战应用场景中,比较适合做后台管理项目,对应纯粹的 RestAPI 项目,提供给用户直接查询的 API 的话,可以考虑一个特殊的 UserID。
到这里,JPA 的审计功能解决了哪些问题,你都清楚了吗?
Auditing 的实现原理
方法你应该已经掌握了,其实这个时候我们应该好奇一下,其原理是怎么实现的?我们来操作一下。
第一步:还是从 @EnableJpaAuditing 入手分析。
我们前面讲了它的使用方法,这次我们分析一下其加载原理,看下面的图:
我们可以知道,首先 Auditing 这套封装是 Spring Data JPA 实现的,而不是 Java Persistence API 规定的,其注解里面还有一项重要功能就是 @Import(JpaAuditingRegistrar.class) 这个类,它帮我们处理 Auditing 的逻辑。
我们看其源码,一步一步地 debug 下去可以发现如下所示:
进一步进入到如下方法中:
可以看到 Spring 容器给 AuditingEntityListener.class 注入了一个 AuditingHandler 的处理类。
第二步:打开 AuditingEntityListener.class 的源码分析 debug 一下。
@Configurable
public class AuditingEntityListener {
private @Nullable ObjectFactory<AuditingHandler> handler;
public void setAuditingHandler(ObjectFactory<AuditingHandler> auditingHandler) {
Assert.notNull(auditingHandler, "AuditingHandler must not be null!");
this.handler = auditingHandler;
}
@PrePersist
public void touchForCreate(Object target) {
Assert.notNull(target, "Entity must not be null!");
if (handler != null) {
AuditingHandler object = handler.getObject();
if (object != null) {
object.markCreated(target);
}
}
}
@PreUpdate
public void touchForUpdate(Object target) {
Assert.notNull(target, "Entity must not be null!");
if (handler != null) {
AuditingHandler object = handler.getObject();
if (object != null) {
object.markModified(target);
}
}
}
}
从源码我们可以看到,AuditingEntityListener 的实现还是比较简单的,利用了 Java Persistence API 里面的@PrePersist、@PreUpdate 回调函数,在更新和创建之前通过AuditingHandler 添加了用户信息和时间信息。
那么通过原理,我们能得出什么结论呢?
原理分析结论
-
查看 Auditing 的实现源码,其实给我们提供了一个思路,就是怎么利用 @PrePersist、@PreUpdate 等回调函数和 @EntityListeners 定义自己的框架代码。这是值得我们学习和参考的,比如说 Auditing 的操作日志场景等。
-
想成功配置 Auditing 功能,必须将 @EnableJpaAuditing 和 @EntityListeners(AuditingEntityListener.class) 一起使用才有效。
-
我们是不是可以不通过 Spring data JPA 给我们提供的 Auditing 功能,而是直接使用 @PrePersist、@PreUpdate 回调函数注解在实体上,也可以达到同样的效果呢?答案是肯定的,因为回调函数是实现的本质。
总结
到这里,关于 JPA 的审计功能我们就介绍完了,不知道你有没有理解透彻。
本课时我们详细讲解了 Auditing 的使用方法,以及最佳实践是什么,还分析了 Auditing 的实现原理。那么我在下一课时会为你讲解 Java Persistence API 给我们提供的回调函数有哪些,还有最佳实践及其原理,这样就会让我们对 JPA 的使用更加游刃有余。
最后如果你觉得有收获就动动手指分享吧,也欢迎在下方留言、讨论,多思考、多实践,相信你可以做得更好。
点击下方链接查看源码(不定时更新)
https://github.com/zhangzhenhuajack/spring-boot-guide/tree/master/spring-data/spring-data-jpa
这篇好文章是转载于:学新通技术网
- 版权申明: 本站部分内容来自互联网,仅供学习及演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,请提供相关证据及您的身份证明,我们将在收到邮件后48小时内删除。
- 本站站名: 学新通技术网
- 本文地址: /boutique/detail/tanhhieikg
-
photoshop保存的图片太大微信发不了怎么办
PHP中文网 06-15 -
word里面弄一个表格后上面的标题会跑到下面怎么办
PHP中文网 06-20 -
《学习通》视频自动暂停处理方法
HelloWorld317 07-05 -
photoshop扩展功能面板显示灰色怎么办
PHP中文网 06-14 -
Android 11 保存文件到外部存储,并分享文件
Luke 10-12 -
TikTok加速器哪个好免费的TK加速器推荐
TK小达人 10-01 -
微信公众号没有声音提示怎么办
PHP中文网 03-31 -
excel下划线不显示怎么办
PHP中文网 06-23 -
excel打印预览压线压字怎么办
PHP中文网 06-22 -
微信运动停用后别人还能看到步数吗
PHP中文网 07-22