Spring Data JPA 介绍和使用

本文参考了Spring Data JPA官方文档,引用了部分文档的代码。

Spring Data JPA 是 Spring 基于 Hibernate 开发的一个 JPA 框架。如果用过 Hibernate 或者 MyBatis 的话,就会知道对象关系映射(ORM)框架有多么方便。但是 Spring Data JPA 框架功能更进一步,为我们做了 一个数据持久层框架几乎能做的任何事情。下面来逐步介绍它的强大功能。

添加依赖

我们可以简单的声明 Spring Data JPA 的单独依赖项。以 Gradle 为例,依赖项如下,Spring Data JPA 会自动添加它的 Spring 依赖项。当前版本需要 Spring 框架版本为4.3.7.RELEASE或更新,使用旧版本的 Spring 框架可能会出现 bug。由于 Spring Data JPA 基于 Hibernate,所以别忘了添加 Hibernate 的依赖项。

compile group: 'org.springframework.data', name: 'spring-data-jpa', version: '1.11.1.RELEASE'
compile group: 'org.hibernate', name: 'hibernate-core', version: '5.2.8.Final'

基本使用

创建环境

Spring Data JPA 也是一个 JPA 框架,因此我们需要数据源、JPA Bean、数据库驱动、事务管理器等等。下面以 XML 配置为例,我们来配置一下所需的 Bean。重点在于 `` 一句,它告诉 Spring 去哪里寻找并创建这些接口类。

<!--启用注解配置和包扫描-->
<context:annotation-config/>
<context:component-scan base-package="yitian.study"/>
<!--创建Spring Data JPA实例对象-->
<jpa:repositories base-package="yitian.study.dao"/>
<!--数据源-->
<bean id="dataSource"
      class="com.mysql.jdbc.jdbc2.optional.MysqlConnectionPoolDataSource">
    <property name="useSSL" value="false"/>
    <property name="url" value="jdbc:mysql://localhost:3306/test"/>
    <property name="user" value="root"/>
    <property name="password" value="12345678"/>
</bean>
<!--JPA工厂对象-->
<bean id="entityManagerFactory"
      class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
    <property name="dataSource" ref="dataSource"/>
    <property name="packagesToScan" value="yitian.study.entity"/>
    <property name="jpaVendorAdapter">
        <bean class="org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter">
            <property name="generateDdl" value="true"/>
            <property name="showSql" value="true"/>
        </bean>
    </property>
</bean>
<!--事务管理器-->
<bean id="transactionManager"
      class="org.springframework.orm.jpa.JpaTransactionManager">
    <property name="entityManagerFactory" ref="entityManagerFactory"/>
</bean>
<!--事务管理-->
<tx:advice id="transactionAdvice"
           transaction-manager="transactionManager">
    <tx:attributes>
        <tx:method name="get*" read-only="true"/>
        <tx:method name="*"/>
    </tx:attributes>
</tx:advice>
<aop:config>
    <aop:pointcut id="daoPointCut" expression="execution(* yitian.study.dao.*.*(..))"/>
    <aop:advisor advice-ref="transactionAdvice" pointcut-ref="daoPointCut"/>
</aop:config>

创建 DAO 对象

前几天学了一点 Groovy,再回头看看 Java,实在是麻烦。所以这里我用 Groovy 写的实体类,不过语法和 Java 很相似。大家能看懂意思即可。不过确实 Groovy 能比 Java 少些很多代码,对开发挺有帮助的。有兴趣的同学可以看看我的Groovy 学习笔记

Groovy 类的字段默认是私有的,方法默认是公有的,分号可以省略,对于默认字段 Groovy 编译器还会自动生成 Getter 和 Setter,可以减少不少代码量。只不过 equals 等方法不能自动生成,多少有点遗憾。这里使用了 JPA 注解,建立了一个实体类和数据表的映射。

@Entity
class User {
    @Id
    @GeneratedValue
    int id
    @Column(unique = true, nullable = false)
    String username
    @Column(nullable = false)
    String nickname
    @Column
    String email
    @Column
    LocalDate birthday
    @Column(nullable = false)
    LocalDateTime registerTime

    String toString() {
        "User(id:$id,username:$username,nickname:$nickname,email:$email,birthday:$birthday,registerTime:$registerTime)"
    }
}

然后就是 Spring Data JPA 的魔法部分了!我们继承 Spring 提供的一个接口,放到前面jpa:repositories指定的包下。

interface CommonUserRepository extends CrudRepository<User, Integer> {
}

然后测试一下,会发生什么事情呢?查看一下数据库就会发现数据已经成功插入了。好吧,好像没什么有魔力的事情。

@RunWith(SpringRunner)
@ContextConfiguration("file:src/main/webapp/WEB-INF/applicationContext.xml")
class DaoTest {
    @Autowired
    CommonUserRepository commonUserRepository

    @Test
    void testCrudRepository() {
        User user = new User(username: 'yitian', nickname: '易天', registerTime: LocalDateTime.now())
        commonUserRepository.save(user)

    }
}

这次我们在接口中再定义一个方法。

interface CommonUserRepository extends CrudRepository<User, Integer> {
    List<User> getByUsernameLike(String username)
}

我们再测试一下。这里也是用的 Groovy 代码,意思应该很容易懂,就是循环 20 次,然后插入 20 个用户,用户的名字和邮箱都是由循环变量生成的。然后调用我们刚刚的方法。这次真的按照我们的要求查询出了用户名以 2 结尾的所有用户!

    @Test
    void testCrudRepository() {
        (1..20).each {
            User user = new User(username: "user$it", nickname: "用户$it", email: "user$it@yitian.com", registerTime: LocalDateTime.now())
            commonUserRepository.save(user)
        }
        List<User> users = commonUserRepository.getByUsernameLike('%2')
        println(users)
    }
//结果如下
//[User(id:3,username:user2,nickname:用户2,email:user2@yitian.com,birthday:null,registerTime:2017-03-08T20:25:58), User(id:13,username:user12,nickname:用户12,email:user12@yitian.com,birthday:null,registerTime:2017-03-08T20:25:59)]

Spring Data 接口

从上面的例子中我们可以看到 Spring Data JPA 的真正功能了。我们只要继承它提供的接口,然后按照命名规则定义相应的查询方法。Spring 就会自动创建实现了该接口和查询方法的对象,我们直接使用就可以了。也就是说,Spring Data JPA 连查询方法都可以帮我们完成,我们几乎什么也不用干了。

下面来介绍一下 Spring 的这些接口。上面的例子中,我们继承了CrudRepository接口。CrudRepository接口的定义如下。如果我们需要增删查改功能。只需要继承该接口就可以立即获得该接口的所有功能。CrudRepository接口有两个泛型参数,第一个参数是实际储存的类型,第二个参数是主键。

public interface CrudRepository<T, ID extends Serializable>
    extends Repository<T, ID> {

    <S extends T> S save(S entity); 

    T findOne(ID primaryKey);       

    Iterable<T> findAll();          

    Long count();                   

    void delete(T entity);          

    boolean exists(ID primaryKey);  

    // … more functionality omitted.
}

CrudRepository接口虽然方便,但是暴露了增删查改的所有方法,如果你的 DAO 层不需要某些方法,就不要继承该接口。Spring 提供了其他几个接口,org.springframework.data.repository.Repository接口没有任何方法。

如果对数据访问需要详细控制,就可以使用该接口。PagingAndSortingRepository接口则提供了分页和排序功能。PagingAndSortingRepository接口的方法接受额外的 Pagable 和 Sort 对象,用来指定获取结果的页数和排序方式。返回类型则是 Page 类型,我们可以调用它的方法获取总页数和可迭代的数据集合。下面是一个 Groovy 写的例子。注意 Pageable 是一个接口,如果我们需要创建 Pageable 对象,使用 PageRequest 类并指定获取的页数和每页的数据量。页是从 0 开始计数的。

    @Test
    void testPagingRepository() {
        int countPerPage = 5
        long totalCount = pageableUserRepository.count()
        int totalPage = totalCount % 5 == 0L ? totalCount / 5 : totalCount / 5 + 1
        (0..totalPage - 1).each {
            Page<User> users = pageableUserRepository.findAll(new PageRequest(it, countPerPage))
            println "第${it}页数据,共${users.totalPages}页"
            users.each {
                println it
            }
        }

    }

查询方法

查询方法可以由我们声明的命名查询生成,也可以像前面那样由方法名解析。下面是官方文档的例子。方法名称规则如下。如果需要详细说明的话可以查看官方文档Appendix C: Repository query keywords一节。

  • 方法名以find…By, read…By, query…By, count…Byget…By做开头。在 By 之前可以添加 Distinct 表示查找不重复数据。By 之后是真正的查询条件。
  • 可以查询某个属性,也可以使用条件进行比较复杂的查询,例如Between, LessThan, GreaterThan, LikeAnd,Or等。
  • 字符串属性后面可以跟IgnoreCase表示不区分大小写,也可以后跟AllIgnoreCase表示所有属性都不区分大小写。
  • 可以使用OrderBy对结果进行升序或降序排序。
  • 可以查询属性的属性,直接将几个属性连着写即可,如果可能出现歧义属性,可以使用下划线分隔多个属性。
public interface PersonRepository extends Repository<User, Long> {

  List<Person> findByEmailAddressAndLastname(EmailAddress emailAddress, String lastname);

  // 唯一查询
  List<Person> findDistinctPeopleByLastnameOrFirstname(String lastname, String firstname);
  List<Person> findPeopleDistinctByLastnameOrFirstname(String lastname, String firstname);

  // 对某一属性不区分大小写
  List<Person> findByLastnameIgnoreCase(String lastname);
  // 所有属性不区分大小写
  List<Person> findByLastnameAndFirstnameAllIgnoreCase(String lastname, String firstname);

  // 启用静态排序
  List<Person> findByLastnameOrderByFirstnameAsc(String lastname);
  List<Person> findByLastnameOrderByFirstnameDesc(String lastname);
}
  //查询Person.Address.ZipCode
  List<Person> findByAddressZipCode(ZipCode zipCode);
  //避免歧义可以这样
  List<Person> findByAddress_ZipCode(ZipCode zipCode);

如果需要限制查询结果也很简单。

User findFirstByOrderByLastnameAsc();

User findTopByOrderByAgeDesc();

Page<User> queryFirst10ByLastname(String lastname, Pageable pageable);

Slice<User> findTop3ByLastname(String lastname, Pageable pageable);

List<User> findFirst10ByLastname(String lastname, Sort sort);

List<User> findTop10ByLastname(String lastname, Pageable pageable);

如果查询很费时间,也可以方便的使用异步查询。只要添加 @Async 注解,然后将返回类型设定为异步的即可。

@Async
Future<User> findByFirstname(String firstname);               

@Async
CompletableFuture<User> findOneByFirstname(String firstname); 

@Async
ListenableFuture<User> findOneByLastname(String lastname); 

Spring Data 扩展功能

Querydsl 扩展

Querydsl 扩展能让我们以流式方式代码编写查询方法。该扩展需要一个接口QueryDslPredicateExecutor,它定义了很多查询方法。

public interface QueryDslPredicateExecutor<T> {

    T findOne(Predicate predicate);             

    Iterable<T> findAll(Predicate predicate);   

    long count(Predicate predicate);            

    boolean exists(Predicate predicate);        

    // … more functionality omitted.
}

只要我们的接口继承了该接口,就可以使用该接口提供的各种方法了。

interface UserRepository extends CrudRepository<User, Long>, QueryDslPredicateExecutor<User> {

}

查询方法可以这样简单的编写。

Predicate predicate = user.firstname.equalsIgnoreCase("dave")
    .and(user.lastname.startsWithIgnoreCase("mathews"));

userRepository.findAll(predicate);

Spring Web Mvc 集成

这个功能需要我们引入 Spring Web Mvc 的相应依赖包。然后在程序中启用 Spring Data 支持。使用 Java 配置的话,在配置类上添加 @EnableSpringDataWebSupport 注解。

@Configuration
@EnableWebMvc
@EnableSpringDataWebSupport
class WebConfiguration { }

使用 XML 配置的话,添加下面的 Bean 声明。

<bean class="org.springframework.data.web.config.SpringDataWebConfiguration" />
<!-- 如果使用Spring HATEOAS 的话用下面这个替换上面这个 -->
<bean class="org.springframework.data.web.config.HateoasAwareSpringDataWebConfiguration" />

不管使用哪种方式,都会向 Spring 额外注册几个组件,支持 Spring Data 的额外功能。首先会注册一个DomainClassConverter,它可以自动将查询参数或者路径参数转换为领域模型对象。下面的例子中,Spring Data 会自动用主键查询对应的用户,然后我们直接就可以从处理方法参数中获得用户实例。注意,Spring Data 需要调用findOne方法查询对象,现版本下我们必须继承CrudRepository,才能实现该功能。

@Controller
@RequestMapping("/users")
public class UserController {

  @RequestMapping("/{id}")
  public String showUserForm(@PathVariable("id") User user, Model model) {

    model.addAttribute("user", user);
    return "userForm";
  }
}

另外 Spring 会注册HandlerMethodArgumentResolverPageableHandlerMethodArgumentResolverSortHandlerMethodArgumentResolver等几个实例。它们支持从请求参数中读取分页和排序信息。

@Controller
@RequestMapping("/users")
public class UserController {

  @Autowired UserRepository repository;

  @RequestMapping
  public String showUsers(Model model, Pageable pageable) {

    model.addAttribute("users", repository.findAll(pageable));
    return "users";
  }
}

对于上面的例子,如果在请求参数中包含 sort、page、size 等几个参数,它们就会被映射为 Spring Data 的 Pageable 和 Sort 对象。请求参数的详细信息如下。

  • page 想要获取的页数,默认是 0,以零开始计数的。
  • size 每页的数据大小,默认是 20.
  • 数据的排序规则,默认是升序,也可以对多个属性执行排序,这时候需要多个 sort 参数,例如?sort=firstname&sort=lastname,asc

如果需要多个分页对象,我们可以用 @Qualifier 注解,然后请求对象就可以写成foo_pagebar_page这样的了。

public String showUsers(Model model,
      @Qualifier("foo") Pageable first,
      @Qualifier("bar") Pageable second) { … }

如果需要自定义这些行为,可以让配置类继承SpringDataWebConfiguration基类,然后重写pageableResolver()sortResolver()方法。这样就不需要使用 @EnableXXX 注解了。

最后一个功能就是 Querydsl 了。如果相关 Jar 包在类路径上,@EnableSpringDataWebSupport注解同样会启用该功能。比方说,在前面的例子中,如果在用户用户参数上添加下面的查询参数。

?firstname=Dave&lastname=Matthews

那么就会被QuerydslPredicateArgumentResolver解析为下面的查询语句。

QUser.user.firstname.eq("Dave").and(QUser.user.lastname.eq("Matthews"))

还可以将QuerydslPredicate注解到对应类型的方法参数上,Spring 会自动实例化相应的参数。为了 Spring 能够准确找到应该查找什么领域对象,我们最好指定 root 属性。

@Controller
class UserController {

  @Autowired UserRepository repository;

  @RequestMapping(value = "/", method = RequestMethod.GET)
  String index(Model model, @QuerydslPredicate(root = User.class) Predicate predicate,    
          Pageable pageable, @RequestParam MultiValueMap<String, String> parameters) {

    model.addAttribute("users", repository.findAll(predicate, pageable));

    return "index";
  }
}

官方文档的其他内容

JPA 命名查询

如果查询方法不能完全满足需要,我们可以使用自定义查询来满足需求。使用 XML 配置的话,在类路径下添加META/orm.xml文件,类似下面这样。我们用named-query就定义命名查询了。

<?xml version="1.0" ?>
<entity-mappings
        xmlns="http://java.sun.com/xml/ns/persistence/orm"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://java.sun.com/xml/ns/persistence/orm
        http://java.sun.com/xml/ns/persistence/orm20.xsd"version="2.0"> <named-query name="User.findByNickname"> <query>select u from User u where u.nickname=?1</query> </named-query> </entity-mappings> 

还可以使用注解,在对应实体类上注解命名查询。

@Entity
@NamedQuery(name = "User.findByNickname",
  query = "select u from User u where u.nickname=?1")
public class User {

}

之后,在接口中声明对应名称的查询方法。这样我们就可以使用 JPQL 语法自定义查询方法了。

List<User> findByNickname(String nickname)

使用 Query 注解

在上面的方法中,查询方法和 JPQL 是对应的,但是却不在同一个地方定义。如果查询方法很多的话,查找和修改就很麻烦。这时候可以改用 @Query 注解。下面的例子直接在方法上定义了 JPQL 语句,如果需要引用 orm.xml 文件中的查询语句,使用注解的 name 属性,如果没有指定,会使用领域模型名.方法名作为命名查询语句的名称。

public interface UserRepository extends JpaRepository<User, Long> {

  @Query("select u from User u where u.emailAddress = ?1")
  User findByEmailAddress(String emailAddress);
}

细心的同学会发现,该注解还有一个 nativeQuery 属性,用作直接执行 SQL 使用。如果我们将该属性指定为 true,查询语句也要相应的修改为 SQL 语句。

Modifying 注解

@Modifying 注解用来指定某个查询是一个更新操作,这样可以让 Spring 执行相应的优化。

@Modifying
@Query("update User u set u.firstname = ?1 where u.lastname = ?2")
int setFixedFirstnameFor(String firstname, String lastname);

投影

有时候数据库和实体类之间并不存在一一对应的关系,或者根据某些情况需要隐藏数据库中的某些字段。这可以通过投影实现。来看看 Spring 的例子。

假设有下面的实体类和仓库。我们在获取人的时候会顺带获取它的地址。

@Entity
public class Person {

  @Id @GeneratedValue
  private Long id;
  private String firstName, lastName;

  @OneToOne
  private Address address;
  …
}

@Entity
public class Address {

  @Id @GeneratedValue
  private Long id;
  private String street, state, country;

  …
}

interface PersonRepository extends CrudRepository<Person, Long> {

  Person findPersonByFirstName(String firstName);
}

如果不希望同时获取地址的话,可以定义一个新接口,其中定义一些 Getter 方法,暴露你需要的属性。然后仓库方法也做相应修改。

interface NoAddresses {  

  String getFirstName(); 

  String getLastName();  
}

interface PersonRepository extends CrudRepository<Person, Long> {

  NoAddresses findByFirstName(String firstName);
}

利用 @Value 注解和 SpEl,我们可以灵活的组织属性。例如下面,定义一个接口,重命名了 lastname 属性。关于 Spring 表达式,可以看看我的文章Spring EL 简介

interface RenamedProperty {    

  String getFirstName();       

  @Value("#{target.lastName}")
  String getName();            
}

或者组合多个属性也可以,下面的例子将姓和名组合成全名。Spring El 的使用很灵活,合理使用可以达到事半功倍的效果。

interface FullNameAndCountry {

  @Value("#{target.firstName} #{target.lastName}")
  String getFullName();

  @Value("#{target.address.country}")
  String getCountry();
}

规范

这里说的规范指的是 JPA 2 引入的新的编程方式实现查询的规范。其他框架比如 Hibernate 也废弃了自己的 Criteria 查询方法,改为使用 JPA 规范的 Criteria。这种方式的好处就是完全是编程式的,不需要额外的功能,使用 IDE 的代码提示功能即可。但是我个人不太喜欢,一来没怎么详细了解,二来感觉不如 JPQL 这样的查询简单粗暴。

废话不多说,直接看官方的例子吧。首先仓库接口需要继承 JpaSpecificationExecutor 接口。

public interface CustomerRepository extends CrudRepository<Customer, Long>, JpaSpecificationExecutor {
 …
}

这样仓库接口就继承了一组以 Specification 接口作参数的查询方法,类似下面这样。

List<T> findAll(Specification<T> spec);

而 Specification 又是这么个东西。所以我们要使用 JPA 规范的查询方法,就需要实现 toPredicate 方法。

public interface Specification<T> {
  Predicate toPredicate(Root<T> root, CriteriaQuery<?> query,
            CriteriaBuilder builder);
}

官方文档有这么个例子,这个类中包含了多个静态方法,每个方法都返回一个实现了的 Specification 对象。

public class CustomerSpecs {

  public static Specification<Customer> isLongTermCustomer() {
    return new Specification<Customer>() {
      public Predicate toPredicate(Root<Customer> root, CriteriaQuery<?> query,
            CriteriaBuilder builder) {

         LocalDate date = new LocalDate().minusYears(2);
         return builder.lessThan(root.get(_Customer.createdAt), date);
      }
    };
  }
  //其他方法

}

之后我们将 Specification 对象传递给仓库中定义的方法即可。

List<Customer> customers = customerRepository.findAll(isLongTermCustomer());

多个规范组合起来的查询也可以。

MonetaryAmount amount = new MonetaryAmount(200.0, Currencies.DOLLAR);
List<Customer> customers = customerRepository.findAll(
  where(isLongTermCustomer()).or(hasSalesOfMoreThan(amount)));

Example 查询

前段时间在研究 Spring 的时候,发现 Spring 对 Hibernate 有一个封装类HibernateTemplate,它将 Hibernate 的Session封装起来,由 Spring 的事务管理器管理,我们只需要调用HibernateTemplate的方法即可。在HibernateTemplate中有一组 Example 方法我没搞明白啥意思,后来才发现这是 Spring 提供的一组简便查询方式。不过这种查询方式的介绍居然在 Spring Data 这个框架中。

这种方式的优点就是比较简单,如果使用上面的 JPA 规范,还需要再学习很多知识。使用 Example 查询的话要学习的东西就少很多了。我们只要使用已有的实体对象,创建一个例子,然后在例子上设置各种约束(即查询条件),然后将例子扔给查询方法即可。这种方式也有缺点,就是不能实现所有的查询功能,我们只能进行前后缀匹配等的字符串查询和其他类型属性的精确查询。

首先,仓库接口需要继承QueryByExampleExecutor接口,这样会引入一组以 Example 作参数的方法。然后创建一个ExampleMatcher对象,最后再用Example的 of 方法构造相应的 Example 对象并传递给相关查询方法。我们看看 Spring 的例子。

ExampleMatcher用于创建一个查询对象,下面的代码就创建了一个查询对象。withIgnorePaths方法用来排除某个属性的查询。withIncludeNullValues方法让空值也参与查询,如果我们设置了对象的姓,而名为空值,那么实际查询条件也是这样的。

Person person = new Person();                          
person.setFirstname("Dave");                           

ExampleMatcher matcher = ExampleMatcher.matching()     
  .withIgnorePaths("lastname")                         
  .withIncludeNullValues()                             
  .withStringMatcherEnding();                          

Example<Person> example = Example.of(person, matcher);

withStringMatcher方法用于指定字符串查询。例如下面的例子就是查询所有昵称以 2 结尾的用户。虽然用的 Groovy 代码但是大家应该很容易看懂吧。

    @Test
    void testExamples() {
        User user = new User(nickname: '2')

        ExampleMatcher matcher = ExampleMatcher.matching()
                .withStringMatcher(ExampleMatcher.StringMatcher.ENDING)
                .withIgnorePaths('id')
        Example<User> example = Example.of(user, matcher)
        Iterable<User> users = exampleRepository.findAll(example)
        users.each {
            println it
        }
    }

如果用 Java 8 的话还可以使用 lambda 表达式写出漂亮的 matcher 语句。

ExampleMatcher matcher = ExampleMatcher.matching()
  .withMatcher("firstname", match -> match.endsWith())
  .withMatcher("firstname", match -> match.startsWith());
}

基本的审计

文章写得非常长了,所以这里最后就在写一个小特性吧,那就是审计功能。这里说的是很基本的审计功能,也就是追踪谁创建和修改相关实体类。相关的注解有 4 个:@CreatedBy, @LastModifiedBy,@CreatedDate@LastModifiedDate,分别代表创建和修改实体类的对象和时间。

这几个时间注解支持 JodaTime、java.util.Date、Calender、Java 8 的新 API 以及long基本类型。在我们的程序中这几个注解可以帮我们省不少事情,比如说,一个博客系统中的文章,就可以使用这些注解轻松实现新建和修改文章的时间记录。

class Customer {

  @CreatedBy
  private User user;

  @CreatedDate
  private DateTime createdDate;

  // … further properties omitted
}

当然不是直接用了这两个注解就行了。我们还需要启用审计功能。审计功能需要spring-aspects.jar这个包,因此首先需要引入 Spring Aspects。在 Gradle 项目中是这样的。

compile group: 'org.springframework', name: 'spring-aspects', version: '4.3.7.RELEASE'

如果使用 Java 配置的话,在配置类上使用 @EnableJpaAuditing 注解。

@Configuration
@EnableJpaAuditing
class Config {

如果使用 XML 配置的话,添加下面的一行。

<jpa:auditing/>

最后在实体类上添加@EntityListeners(AuditingEntityListener)注解。这样,以后当我们创建和修改实体类时,不需要管@LastModifiedDate@CreatedDate这种字段,Spring 会帮我们完成一切。

@Entity
@EntityListeners(AuditingEntityListener)
class Article {

版权声明:本文为博主原创文章,转载请注明出处。