Overview

Spring Data JPA为Java Persistence API(JPA)提供了存储库支持。 它简化了应用程序需要访问JPA数据源的开发。

Dependencies

<dependencies>
  <dependency>
    <groupId>org.springframework.data</groupId>
    <artifactId>spring-data-jpa</artifactId>
  </dependency>
<dependencies>

Working with Spring Data Repositories

Introduction

定义接口类继承基础的暴露CRUD操作的基础接口。

interface PersonRepository extends Repository<Person, Long> { … }

在接口中定义方法扩展自定义CRUD方法。

interface PersonRepository extends Repository<Person, Long> {
  List<Person> findByLastname(String lastname);
}

设置Spring为这些接口创建代理实例,有 xml 和 JavaConfig 两种配置方式,以下以 JavaConfig 配置为例。

import org.springframework.data.jpa.repository.config.EnableJpaRepositories;

@EnableJpaRepositories
class Config {}

在操作数据层的业务层中注入该接口,并根据业务调用相应方法。

class SomeClient {

  private final PersonRepository repository;

  SomeClient(PersonRepository repository) {
    this.repository = repository;
  }

  void doSomething() {
    List<Person> persons = repository.findByLastname("Matthews");
  }
}

Defining Repository Interfaces

Fine-tuning Repository Definition

在项目中DAO层类一般是有一些共同的CRUD方法的,如 findById()、save()等方法,Spring已经抽出了几个基础的interface,有RepositoryCrudRepository,和PagingAndSortingRepository 接口,但我们也可以自定义一个基础的reppsitory base interface,方法如下:

@NoRepositoryBean
interface MyBaseRepository<T, ID extends Serializable> extends Repository<T, ID> {

  Optional<T> findById(ID id);

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

interface UserRepository extends MyBaseRepository<User, Long> {
  User findByEmailAddress(EmailAddress emailAddress);
}

注: @NoRepositoryBean注解标明该 bean 非完整的实现 RepositoryBean 。

Null Handling of Repository Methods

可以根据需求给编写的Repository interfaces 方法的入参和返回值添加空处理,具体操作见如下:

@org.springframework.lang.NonNullApi
package com.acme;

package-info.java 上添加了包全局的 Null 处理,为空则抛出EmptyResultDataAccessExceptionIllegalArgumentException 异常。

package com.acme;

import org.springframework.lang.Nullable;

interface UserRepository extends Repository<User, Long> {

  User getByEmailAddress(EmailAddress emailAddress); // 入参及返回值都不能为空

  @Nullable
  User findByEmailAddress(@Nullable EmailAddress emailAdress); //入参和返回值都可空

  Optional<User> findOptionalByEmailAddress(EmailAddress emailAddress); //入参不能为空,返回值能为空
}

Using Repositories with Multiple Spring Data Modules

一些需要灵活匹配的及分页功能的特殊 repository 可以继承 JPARepository 接口, 例子如下:

interface MyRepository extends JpaRepository<User, Long> { }

@NoRepositoryBean
interface MyBaseRepository<T, ID extends Serializable> extends JpaRepository<T, ID> {
  …
}

interface UserRepository extends MyBaseRepository<User, Long> {
  …
}

而一些需要常用查询及CRUD的可以继承 CRUDRepository 接口,例子如下:

interface AmbiguousRepository extends Repository<User, Long> {
 …
}

@NoRepositoryBean
interface MyBaseRepository<T, ID extends Serializable> extends CrudRepository<T, ID> {
  …
}

interface AmbiguousUserRepository extends MyBaseRepository<User, Long> {
  …
}

当我们使用多个Spring Data 模块时,如Spring Data JPA 及 Spring Data MongoDB,应当将存储库分开,具体例子如下:

interface PersonRepository extends Repository<Person, Long> {
 …
}

@Entity
class Person {
  …
}

interface UserRepository extends Repository<User, Long> {
 …
}

@Document
class User {
  …
}

另外我们也可以开启注解去显式地引入对应 Spring Data 模块的 Repository 接口,如下所示:

@EnableJpaRepositories(basePackages = "com.acme.repositories.jpa")
@EnableMongoRepositories(basePackages = "com.acme.repositories.mongo")
interface Configuration { }

Defining Query Methods

Query Lookup Strategies

可以通过注解@Enable${store}Repositories 中的 queryLookupStrategy 属性配置,默认值为 CREATE_IF_NOT_FOUND ,属性的可选值有以下三种:

  • CREATE : 通过方法名以特定规则获取查询;
  • USE_DECLARED_QUERY :手动显式地在方法的在 @Query 注解上添加查询语句(HQL语句);
  • CREATE_IF_NOT_FOUND : 默认值,即先通过 @Query 注解获取,没有的话再通过方法名获取。

Query Creation

查找机制为寻找 Repository 类内以 find…Byread…Byquery…Bycount…Byget…By 开头的方法名,并解析其名称余下部分,上述方法的 部分可以包含一些表达式,如 Distinctby 是一个重要的分隔符,其后部分将会解析为具体的 Query Criteria。

以下是一些定义查询的方法名示例:

interface PersonRepository extends Repository<User, Long> {

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

  // Enables the distinct flag for the query
  List<Person> findDistinctPeopleByLastnameOrFirstname(String lastname, String firstname);
  List<Person> findPeopleDistinctByLastnameOrFirstname(String lastname, String firstname);

  // Enabling ignoring case for an individual property
  List<Person> findByLastnameIgnoreCase(String lastname);
  // Enabling ignoring case for all suitable properties
  List<Person> findByLastnameAndFirstnameAllIgnoreCase(String lastname, String firstname);

  // Enabling static ORDER BY for a query
  List<Person> findByLastnameOrderByFirstnameAsc(String lastname);
  List<Person> findByLastnameOrderByFirstnameDesc(String lastname);
}

注意:

  1. 可以使用 ANDOR 将条件串联起来,或者 BetweenLessThanGreaterThan 等表达式限制条件,但具体支持情况会因为存储数据库的有相应差异;
  2. IgnoreCase 表达式可以忽略查询条件的大小写,是否支持同样需要存储数据库支持;
  3. OrderBy 可以实现对于指定字段的升降序排序;

Property Expressions

List<Person> findByAddressZipCode(ZipCode zipCode);

假设一个 Person 类中有一个级联对象为 Address 带有一个 ZipCode 属性,该定义的方法名是查询该属性。

  1. 解析过程是首先去除 findBy 部分对余下部分 AddressZipCode 转化为 addressZipCode 并解析,假设此时 Person 类有该名称的属性,则查询该属性;
  2. 假如没有,则依照从右往左的顺序截取第一个大写开头的词,此处为 Code 并查询余下部分 AddressZip 是否为实体 Person 的一个属性,没有则按此规则继续截取;
  3. 最后假如 Address 为实体属性,余下部分为 ZipCode ,一样,首先判断该部分是否为 Address 属性对应类型的一个属性, 没有对该部分继续按从右往左截取并按照规则判断。最终可以得到查询的完整属性为 address.zipCooe

为了更直观地表达查询意图,可以按照以下方式显示定义查询属性 address.zipCooe

List<Person> findByAddress_ZipCode(ZipCode zipCode);

因此,Spring 官方建议实体类定义字段名称规范最好使用驼峰而不是下划线,以免与解析规则产生冲突。

Special parameter handling

上述定义查询方法名后可以通过传入一些特殊参数,如 PageableSort,动态地实现自动化分页或排序,示例如下:

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

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

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

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

传入参数 Pageable 得到 Page,可以获取到元素总数和是否分页可用的信息。分页的返回类型有两种,分别为PageSlice。关于两者的区别,Slice 继承了Streamable 接口,有迭代器的功能,而 Page 继承了 Slice。 两者区别可以简单理解为 List 和其迭代器的关系,Page 实体在逻辑上更符合分页查询的响应。

Limiting Query Results

JPA支持使用 First 或者 top 关键字用于限制查询结果,关键字后可以接阿拉伯数字指定返回数目,缺省放回第一条数据。

需要注意的是,在方法入参和方法名同时限制了查询条数的情况下,方法名指定方式的优先级更高。

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);

Streaming query results

JPA支持使用 JAVA 8 的 Stream<T> 作为返回类型,示例如下:

@Query("select u from User u")
Stream<User> findAllByCustomQueryAndStream();

Stream<User> readAllByFirstnameNotNull();

@Query("select u from User u")
Stream<User> streamAllPaged(Pageable pageable);

注意

  1. Stream<T> 可能会用特定的数据结构包装指定的资源,因此需要在调用完资源后将其 close 掉,或者使用 Java7 的 try-with-resource 特性处理;
  2. 不是所有的Spring Data 模块都支持 Stream<T> 作为返回类型。

Async query results

JPA支持异步查询,即立即响应并将查询任务交由Spring TaskExecutor 执行,另外异步查询和响应式查询不同,不能够混用。以下为示例:

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

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

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

Creating Repository Instances

配置创建 Repository 可以通过XML或JavaConfig方式配置。以下以JavaConfig方式示例:

@Configuration
@EnableJpaRepositories(
    value = "com.acme.repositories",
    excludeFilters = {
        @ComponentScan.Filter(value = {BaseRepository.class})
    }
)
class ApplicationConfiguration {

  @Bean
  EntityManagerFactory entityManagerFactory() {
    // …
  }
}

同时JPA也支持跳出Spring容器创建得到对应的Repository。

RepositoryFactorySupport factory = … // Instantiate factory here
UserRepository repository = factory.getRepository(UserRepository.class);

Custom Implementations for Spring Data Repositories

Customizing Individual Repositories

首先定义一个接口,如下:

interface CustomizedUserRepository {
  void someCustomMethod(User user);
}

在创建一个实现该接口的实现类,方法实现本身可以不依赖Spring Data,例如可以注入 JdbcTemplate 执行查询。

class CustomizedUserRepositoryImpl implements CustomizedUserRepository {

  public void someCustomMethod(User user) {
    // Your custom implementation
  }
}

接口还继承如 CrudRepository 接口,继承后该接口可以使用 CrudRepository 的默认实现方法。

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

  // Declare query methods here
}

自定义的Repository 接口默认查找 名为“接口名 + Impl”的实现类,例如可以按照以下方式创建两个实现了通用基础的 HumanRepository 和 ContactRepository 接口的实现类,继承使用其默认实现方法。

interface HumanRepository {
  void someHumanMethod(User user);
}

class HumanRepositoryImpl implements HumanRepository {

  public void someHumanMethod(User user) {
    // Your custom implementation
  }
}

interface ContactRepository {

  void someContactMethod(User user);

  User anotherContactMethod(User user);
}

class ContactRepositoryImpl implements ContactRepository {

  public void someContactMethod(User user) {
    // Your custom implementation
  }

  public User anotherContactMethod(User user) {
    // Your custom implementation
  }
}

interface UserRepository extends CrudRepository<User, Long>, HumanRepository, ContactRepository {

  // Declare query methods here
}

可以活用泛型抽象出通用的Repository 操作,继承并复用。

interface CustomizedSave<T> {
  <S extends T> S save(S entity);
}

class CustomizedSaveImpl<T> implements CustomizedSave<T> {

  public <S extends T> S save(S entity) {
    // Your custom implementation
  }
}

interface UserRepository extends CrudRepository<User, Long>, CustomizedSave<User> {
}

interface PersonRepository extends CrudRepository<Person, Long>, CustomizedSave<Person> {
}

可以自配置实现类的查找后缀,默认为Impl。

@EnableJpaRepositories(repositoryImplementationPostfix = "MyPostfix" )
public class Application { ... }

当同一个Repository 接口有多个实现时可以使用注解 @Component 指定选择的实现类。

package com.acme.impl.one;

class CustomizedUserRepositoryImpl implements CustomizedUserRepository {

  // Your custom implementation
}

...

package com.acme.impl.two;

@Component("specialCustomImpl")
class CustomizedUserRepositoryImpl implements CustomizedUserRepository {

  // Your custom implementation
}

Customize the Base Repository

上一节描述方法,需要为每个存储库,定义Repository 基类

class MyRepositoryImpl<T, ID extends Serializable>
  extends SimpleJpaRepository<T, ID> {

  private final EntityManager entityManager;

  MyRepositoryImpl(JpaEntityInformation entityInformation,
                          EntityManager entityManager) {
    super(entityInformation, entityManager);

    // Keep the EntityManager around to used from the newly introduced methods.
    this.entityManager = entityManager;
  }

  @Transactional
  public <S extends T> S save(S entity) {
    // implementation goes here
  }
}

再在配置中指定该基类。

@Configuration
@EnableJpaRepositories(repositoryBaseClass = MyRepositoryImpl.class)
class ApplicationConfiguration { … }

Publishing Events from Aggregate Roots

看不太懂,大概就是model类内定义方法并打上注解,方法会在该modle类执行 save(...) 方法保存到数据库前后触发,具体用处或使用场景没搞懂。

class AnAggregateRoot {

    @DomainEvents
    Collection<Object> domainEvents() {
        // … return events you want to get published here
    }

    @AfterDomainEventPublication 
    void callbackMethod() {
       // … potentially clean up domain events list
    }
}

Spring Data Extensions

Querydsl Extension

Querydsl是一个可以通过其流畅的API构建静态类型SQL类查询的框架。

通过继承QuerydslPredicateExecutor 接口可以使用Querydsl

public interface QuerydslPredicateExecutor<T> {

  Optional<T> findById(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);
Web Support

Spring Data JPA 可以提供一些和Spring MVC 组件结合起来使用的一些 web 方面的支持。

Config

以 JavaConfig 方式配置为例。

@Configuration
@EnableWebMvc
@EnableSpringDataWebSupport
class WebConfiguration {}
Base support
  • DomainClassConverter:使Spring MVC从请求参数或路径变量中解析存储库管理的域类实例。
  • HandlerMethodArgumentResolver:使Spring MVC从请求参数中解析Pageable和Sort实例。
@Controller
@RequestMapping("/users")
class UserController {

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

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

以上的示例代码会寻找Userd的repository的 findById(...) 方法,调用并得到查询结果,需要注意的是UserRepository需要继承CrudRepository接口。

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

  private final UserRepository repository;

  UserController(UserRepository repository) {
    this.repository = repository;
  }

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

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

Pageable 从以下表格中参数及规则获取:

Name Description
page 页数,默认0
size 页面数据条数,默认20
sort 排序,默认排序规则为升序,规则为sort=<fieldName,sort>,如?sort=firstname&sort=lastname,asc

该web support 还支持获取带下一页数据超链接的数据格式PagedResources,、多Pageable参数传入、JSONPath或XPath的数据绑定、及QueryDSL Web Support。

JPA Repositories

Introduction

EntityManagerFactory & PlatformTransactionManager

EntityManagerFactoryPlatformTransactionManager 是使用JPA中两个重要的类,分别为实体管理器工厂和事务管理器,默认配置ORM框架为hibernate,注解方式显式配置方式如下:

@Configuration
@EnableJpaRepositories
@EnableTransactionManagement
class ApplicationConfig {

  @Bean
  public DataSource dataSource() {

    EmbeddedDatabaseBuilder builder = new EmbeddedDatabaseBuilder();
    return builder.setType(EmbeddedDatabaseType.HSQL).build();
  }

  @Bean
  public LocalContainerEntityManagerFactoryBean entityManagerFactory() {

    HibernateJpaVendorAdapter vendorAdapter = new HibernateJpaVendorAdapter();
    vendorAdapter.setGenerateDdl(true);

    LocalContainerEntityManagerFactoryBean factory = new LocalContainerEntityManagerFactoryBean();
    factory.setJpaVendorAdapter(vendorAdapter);
    factory.setPackagesToScan("com.acme.domain");
    factory.setDataSource(dataSource());
    return factory;
  }

  @Bean
  public PlatformTransactionManager transactionManager(EntityManagerFactory entityManagerFactory) {

    JpaTransactionManager txManager = new JpaTransactionManager();
    txManager.setEntityManagerFactory(entityManagerFactory);
    return txManager;
  }
}

Bootstrap Mode

@EnableJpaRepositories 的bootstrapMode属性可以控制Application启动时的Repository策略。

  • DEFAULT:默认策略,除非使用@Lazy显式指定,否则将在启动时实例化repository实例,假如没有其他bean依赖到打上该注解的bean,懒加载将生效。
  • LAZY:隐式声明了所有的repository bean 都为懒加载,其他依赖该bean的bean只要初始化时不适用该bean方法,就不会实例化bean,实例化及验证将推迟到repository第一次方法调用时。
  • DEFERRED:基本与LAZY一致的操作模式,但在返回一个ContextRefreshedEvent的响应时会触发repository实例化,以便于在application完全启动前验证repository。

选择建议:

  1. 假如不需要异步引导JPA,选择DEFAULT模式即可;
  2. 需要异步引导JPA,那么DEFERRED是个不错的选择;
  3. LAZY是测试环境和开发环境一个不错的选择,假如你确保repository可以被正确引导,因为可以启动只是为了测试某个或几个repository bean。

Persisting Entities

Saving Entities

继承了CrudRepository接口的repository调用CrudRepository.save(…)方法保存实体,假如判断该实体为新的,则调用EntityManagerentityManager.persist(…)方法,不是则调用entityManager.merge(…)

JPA提供了三种判断entity是否为新实体方法。

  • ID属性检查:默认策略,实体的主键属性假如为null,则为新实体,相反则不是。
  • Persistable接口方法检查:实体实现Persistable接口,调用接口方法isNew()可得知是否为新实体。
  • EntityInformation接口方法检查:通过继承JpaRepositoryFactory类并复写其getEntityInformation(...)方法,并实例化为BEAN,调用EntityInformation接口isNew(T t)方法传入对应实体判断。

Query Methods

Query Creation

以下表格展示了JPA支持的方法中的关键词以及它们会被翻译成的JPQL语句片段:

Keyword Simple JPQL snippet
And findByLastnameAndFirstname … where x.lastname = ?1 and x.firstname = ?2
Or findByLastnameOrFirstname … where x.lastname = ?1 or x.firstname = ?2
Is,Equals findByFirstname,findByFirstnameIs,findByFirstnameEquals … where x.firstname = ?1
Between findByStartDateBetween … where x.startDate between ?1 and ?2
LessThan findByAgeLessThan … where x.age < ?1
LessThanEqual findByAgeLessThanEqual … where x.age <= ?1
GreaterThan findByAgeGreaterThan … where x.age > ?1
GreaterThanEqual findByAgeGreaterThanEqual … where x.age >= ?1
After findByStartDateAfter … where x.startDate > ?1
Before findByStartDateBefore … where x.startDate < ?1
IsNull findByAgeIsNull … where x.age is null
IsNotNull,NotNull findByAge(Is)NotNull … where x.age not null
Like findByFirstnameLike … where x.firstname like ?1
NotLike findByFirstnameNotLike … where x.firstname not like ?1
StartingWith findByFirstnameStartingWith … where x.firstname like ?1
(parameter bound with appended %)
EndingWith findByFirstnameEndingWith … where x.firstname like ?1
(parameter bound with prepended %)
Containing findByFirstnameContaining … where x.firstname like ?1 (parameter bound wrapped in %)
OrderBy findByAgeOrderByLastnameDesc … where x.age = ?1 order by x.lastname desc
Not findByLastnameNot … where x.lastname <> ?1
In findByAgeIn(Collection<Age> ages) … where x.age in ?1
NotIn findByAgeNotIn(Collection<Age> ages) … where x.age not in ?1
True findByActiveTrue() … where x.active = true
False findByActiveFalse() findByActiveFalse()
IgnoreCase findByFirstnameIgnoreCase … where UPPER(x.firstame) = UPPER(?1)

Using JPA Named Queries

使用@NameQuery定义方法名解析规则,映射指定的JPQL语句上。

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

}

...

public interface UserRepository extends JpaRepository<User, Long> {

  List<User> findByLastname(String lastname);

  User findByEmailAddress(String emailAddress);
}

Using @Query

@Query注解可以直接定义JPQL语句查询,语句方法名上 @Query 定义的Query优先级是最高的。

public interface UserRepository extends JpaRepository<User, Long> {

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

  @Query("select u from User u where u.firstname like %?1")
  List<User> findByFirstnameEndsWith(String firstname);
}

@Query 除了可以使用JPQL语句外,还可以使用原生的SQL语句,方法如下。

public interface UserRepository extends JpaRepository<User, Long> {

  @Query(value = "SELECT * FROM USERS WHERE EMAIL_ADDRESS = ?1", nativeQuery = true)
  User findByEmailAddress(String emailAddress);
}

在原生SQL定义下,Spring Data JPA 目前暂不支持动态排序,但分页查询可以通过多定义一个查总数的SQL。

public interface UserRepository extends JpaRepository<User, Long> {

  @Query(value = "SELECT * FROM USERS WHERE LASTNAME = ?1",
    countQuery = "SELECT count(*) FROM USERS WHERE LASTNAME = ?1",
    nativeQuery = true)
  Page<User> findByLastname(String lastname, Pageable pageable);
}

Using Sort

在使用@Query的JPQL查询时,可以结合传参Sort排序,也可以调用JpaSort.unsafe(…)方法进行一些带函数的字段排序。

public interface UserRepository extends JpaRepository<User, Long> {

  @Query("select u from User u where u.lastname like ?1%")
  List<User> findByAndSort(String lastname, Sort sort);

  @Query("select u.id, LENGTH(u.firstname) as fn_len from User u where u.lastname like ?1%")
  List<Object[]> findByAsArrayAndSort(String lastname, Sort sort);
}

repo.findByAndSort("lannister", new Sort("firstname")); // 合法,指向了domain的某字段
repo.findByAndSort("stark", new Sort("LENGTH(firstname)")); // 非法,包含函数调用,会抛异常
repo.findByAndSort("targaryen", JpaSort.unsafe("LENGTH(firstname)")); // 合法,带有显式不安全的函数调用
repo.findByAsArrayAndSort("bolton", new Sort("fn_len")); // 合法,表达式指向了别名函数

Using Named Parameters

前面演示的JPA例子@Query中传参都是基于参数位置指定的,这样不够准确,JPA可以使用@Param以名称指定JPQL的传参。

public interface UserRepository extends JpaRepository<User, Long> {

  @Query("select u from User u where u.firstname = :firstname or u.lastname = :lastname")
  User findByLastnameOrFirstname(@Param("lastname") String lastname, @Param("firstname") String firstname);
}

Using SpEL Expressions

JPA还支持在@Query中使用SPEL 表达式,如 #{#entityName} 表达式可以指定查询的映射实体名,根据repository对应的实体类名填入,具体如下:

@Entity
public class User {

  @Id
  @GeneratedValue
  Long id;

  String lastname;
}

public interface UserRepository extends JpaRepository<User,Long> {

  @Query("select u from #{#entityName} u where u.lastname = ?1")
  List<User> findByLastname(String lastname);
}

在我们定义一个将关联实体类定义为泛型的repository基类时,可以在该基类中的@Query注解中加入#{#entityName} 在执行时再套入子类的集体类型。

@MappedSuperclass
public abstract class AbstractMappedType {
  …
  String attribute
}

@Entity
public class ConcreteType extends AbstractMappedType { … }

@NoRepositoryBean
public interface MappedTypeRepository<T extends AbstractMappedType>
  extends Repository<T, Long> {

  @Query("select t from #{#entityName} t where t.attribute = ?1")
  List<T> findAllByAttribute(String attribute);
}

public interface ConcreteRepository
  extends MappedTypeRepository<ConcreteType> { … }

Modifying Queries

在执行@Query中定义的如update 或 delete 操作时,需要在方法上打上@Modifying ,以表明该方法为写操作。

interface UserRepository extends Repository<User, Long> {

  void deleteByRoleId(long roleId);

  @Modifying
  @Query("delete from User u where user.role.id = ?1")
  void deleteInBulkByRoleId(long roleId);
}

以上两个方法,区别在于后者为直接执行的JPQL语句,因此它不会涉及到对应表数据的实体的生命周期的一些回调方法,如@Preremove注解的方法,而前者会先查出删除的数据并对应到实体上,最后执行CrudRepository.delete(Iterable<User> users) 方法并执行实体上定义的回调方法。

Applying Query Hints

可以使用@QueryHints 注解禁用分页触发的附加计数查询,如下:

public interface UserRepository extends Repository<User, Long> {

  @QueryHints(value = { @QueryHint(name = "name", value = "value")},
              forCounting = false)
  Page<User> findByLastname(String lastname, Pageable pageable);
}

Configuring Fetch- and LoadGraphs

@EntityGrap@NamedEntityGraph注解可以用来避免执行级联对象查询时的不必要查询,有两种方式:

@Entity
@NamedEntityGraph(name = "GroupInfo.detail",
  attributeNodes = @NamedAttributeNode("members"))
public class GroupInfo {

  // default fetch mode is lazy.
  @ManyToMany
  List<GroupMember> members = new ArrayList<GroupMember>();

  …
}

...

@Repository
public interface GroupRepository extends CrudRepository<GroupInfo, String> {

  @EntityGraph(value = "GroupInfo.detail", type = EntityGraphType.LOAD)
  GroupInfo getByGroupName(String name);

}
@Repository
public interface GroupRepository extends CrudRepository<GroupInfo, String> {

  @EntityGraph(attributePaths = { "members" })
  GroupInfo getByGroupName(String name);

}

Projections

Spring data JPA 还支持控制查询的 projections,查询中只返回指定的字段,提高查询效率,以下一些加入projection 的查询示例。

以下为常规的JPA查询映射。

class Person {

  @Id UUID id;
  String firstname, lastname;
  Address address;

  static class Address {
    String zipCode, city, street;
  }
}

interface PersonRepository extends Repository<Person, UUID> {

  Collection<Person> findByLastname(String lastname);
}

基于接口的Projections。

interface NamesOnly {

  String getFirstname();
  String getLastname();
}

interface PersonRepository extends Repository<Person, UUID> {

  Collection<NamesOnly> findByLastname(String lastname);
}

对于控制实体级联的返回字段,可以用以下方式控制。

interface PersonSummary {

  String getFirstname();
  String getLastname();
  AddressSummary getAddress();

  interface AddressSummary {
    String getCity();
  }
}

以上接口的方法名和映射实体的字段名相对应的 projections 称为 close projection。

而open projection 方式获取字段值则是从@Value上SpEL表达式获取。需要注意的是这种方式会返回映射实体的所有字段信息,因为SpEL可以使用 target 上的所有字段。

interface NamesOnly {

  @Value("#{target.firstname + ' ' + target.lastname}")
  String getFullName();
  …
}

对于一些简单的表达式 ,在JAVA 8中可以用默认接口方法定义返回值,减少字段查询,如下:

interface NamesOnly {

  String getFirstname();
  String getLastname();

  default String getFullName() {
    return getFirstname.concat(" ").concat(getLastname());
  }
}

SpEL表达式还可以定义用其他类方法传入查询到的实体类,调用并得到返回值。

@Component
class MyBean {

  String getFullName(Person person) {
    …
  }
}

interface NamesOnly {

  @Value("#{@myBean.getFullName(target)}")
  String getFullName();
  …
}

SpEL支持从传参获取值。

interface NamesOnly {

  @Value("#{args[0] + ' ' + target.firstname + '!'}")
  String getSalutation(String prefix);
}

也可以像以下方式直接使用DTO映射。

class NamesOnly {

  private final String firstname, lastname;

  NamesOnly(String firstname, String lastname) {

    this.firstname = firstname;
    this.lastname = lastname;
  }

  String getFirstname() {
    return this.firstname;
  }

  String getLastname() {
    return this.lastname;
  }

  // equals(…) and hashCode() implementations
}

使用Project lombak 可以避免重复模板代码,如下:

@Value
class NamesOnly {
  String firstname, lastname;
}

Dynamic Projections 可以动态地决定返回值。

interface PersonRepository extends Repository<Person, UUID> {

  <T> Collection<T> findByLastname(String lastname, Class<T> type);
}

...

void someMethod(PersonRepository people) {

  Collection<Person> aggregates =
    people.findByLastname("Matthews", Person.class);

  Collection<NamesOnly> aggregates =
    people.findByLastname("Matthews", NamesOnly.class);
}

Stored procedure

JPA 2.1 版本支持使用JPA QUERY API 调用过程。

首先展示的是数据库存储过程定义:

/;
DROP procedure IF EXISTS plus1inout
/;
CREATE procedure plus1inout (IN arg int, OUT res int)
BEGIN ATOMIC
 set res = arg + 1;
END
/;

将存储过程的元数据定义在任一实体类上。

@Entity
@NamedStoredProcedureQuery(name = "User.plus1", procedureName = "plus1inout", parameters = {
  @StoredProcedureParameter(mode = ParameterMode.IN, name = "arg", type = Integer.class),
  @StoredProcedureParameter(mode = ParameterMode.OUT, name = "res", type = Integer.class) })
public class User {}

其中name = "User.plus1" 指定了JPA内调用存储过程的名称,procedureName 则指定了数据库内的存储过程名称。

以下为存储过程调用方式:

@Procedure("plus1inout")  // 显式通过存储过程名调用
Integer explicitlyNamedPlus1inout(Integer arg);

@Procedure(procedureName = "plus1inout")  // 显式通过存储过程名调用
Integer plus1inout(Integer arg);

@Procedure(name = "User.plus1IO") // 隐式通过JPA过程名称调用
Integer entityAnnotatedCustomNamedProcedurePlus1IO(@Param("arg") Integer arg);

@Procedure  //隐式通过类名加方法名查找到“User.plus1”的JPA过程
Integer plus1(@Param("arg") Integer arg);

Specifications

Specifications 是JPA支持的一种较为灵活的查询方式,不同的 Specifications 还可以在查询方法内动态组合成新的条件。

首先执行查询的 repository 需要结成JpaSpecificationExecutor,以使用其中的 Specifications 式查询方法。

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

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

而传参的specification接口的需要实现方法为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);
      }
    };
  }

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

         // build query here
      }
    };
  }
}

其中的_Customer.createdAt 为Customer类名为createAt的字段名。

接下来使用实现的Specification执行查询,后一种为组合查询。

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

...

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

Query by Example

QBE 是一种对用户友好的查询方式。它允许动态创建查询并不要求编写包含字段名的查询。

QBE 的查询传参为Example,该对象包含两个部分ProbeExampleMatcher,其中Prole为有填充字段的域对象,ExampleMatcher是一个包含如何匹配特定字段的匹配规则集。

QBE有以下几个特点:

  • 使用一组静态或动态约束查询;
  • 可频繁地重构域对象而不用担心会破坏现有查询;
  • 独立于底层数据存储API工作。

QBE不是万能的,有以下几个限制:

  • 不支持嵌套或分组的属性约束;
  • 仅支持字符串的开始/包含/结束/正则表达式匹配以及其他属性类型的精确匹配。

下面是一个简单的Example构建例子。

public class Person {

  @Id
  private String id;
  private String firstname;
  private String lastname;
  private Address address;

  // … getters and setters omitted
}

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

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

默认情况下,proble中字段值为NULL的字段会被忽略,示例中为相当于where firstname = ?1

为了使用QBE查询方式,repository bean 需要继承接口QueryByExampleExecutor,已使用其中的QBE查询方法。

以下为使用ExampleMatcher的一个例子,

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

ExampleMatcher matcher = ExampleMatcher.matching() // 创建匹配所有字段的匹配规则集
  .withIgnorePaths("lastname") // 忽略字段“lastname”的匹配
  .withIncludeNullValues() // 匹配包含null值的字段
  .withStringMatcherEnding(); // 字符串类型字段配置后缀

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

还可以对特定的字段加上匹配规则。

ExampleMatcher matcher = ExampleMatcher.matching()
  .withMatcher("firstname", endsWith())
  .withMatcher("lastname", startsWith().ignoreCase());
}

...

ExampleMatcher matcher = ExampleMatcher.matching()
  .withMatcher("firstname", endsWith())
  .withMatcher("lastname", startsWith().ignoreCase());
}

ExampleMatcher 的作用范围如下表所示:

Setting Scope
Null-handling ExampleMatcher
String matching ExampleMatcher and property path
Ignoring properties Property path
Case sensitivity ExampleMatcher and property path
Value transformation Property path

最终,QBE形式执行查询方式如下:

public interface PersonRepository extends JpaRepository<Person, String> { … }

public class PersonService {

  @Autowired PersonRepository personRepository;

  public List<Person> findPeople(Person probe) {
    return personRepository.findAll(Example.of(probe));
  }
}

附表,ExampleMatcher 中的 StringMatcher 可选项。

Matching Logical result
DEFAULT (case-sensitive) firstname = ?0
DEFAULT (case-insensitive) LOWER(firstname) = LOWER(?0)
EXACT (case-sensitive) firstname = ?0
EXACT (case-insensitive) LOWER(firstname) = LOWER(?0)
STARTING (case-sensitive) firstname like ?0 + ‘%’
STARTING (case-insensitive) LOWER(firstname) like LOWER(?0) + ‘%’
ENDING (case-sensitive) firstname like ‘%’ + ?0
ENDING (case-insensitive) ENDING (case-insensitive)
CONTAINING (case-sensitive) firstname like ‘%’ + ?0 + ‘%’
CONTAINING (case-insensitive) LOWER(firstname) like ‘%’ + LOWER(?0) + ‘%’

Transactionality

默认情况下,JPA给每一个repository的方法都加上了事务控制并设置readonly=true,这也是为什么需要在@Query定义删除或更新SQL的方法中需要加上@Modifying注解的原因。当然也可以显式地在repository中定义方法的事务控制。

public interface UserRepository extends CrudRepository<User, Long> {

  @Override
  @Transactional(timeout = 10)
  public List<User> findAll();

  // Further query method declarations
}

另一个修改事务行为的方法为service层中设置事务并覆盖内部(repository)事务设置。

@Service
class UserManagementImpl implements UserManagement {

  private final UserRepository userRepository;
  private final RoleRepository roleRepository;

  @Autowired
  public UserManagementImpl(UserRepository userRepository,
    RoleRepository roleRepository) {
    this.userRepository = userRepository;
    this.roleRepository = roleRepository;
  }

  @Transactional
  public void addRoleToAllUsers(String roleName) {

    Role role = roleRepository.findByName(roleName);

    for (User user : userRepository.findAll()) {
      user.addRole(role);
      userRepository.save(user);
    }
}

注: 以上事务控制需要在开启@EnableTransactionManagement注解。

另外,还可以在repository接口上定义类内方法的统一事务控制,再需要特殊事务处理的方法单独设置事务控制。

@Transactional(readOnly = true)
public interface UserRepository extends JpaRepository<User, Long> {

  List<User> findByLastname(String lastname);

  @Modifying
  @Transactional
  @Query("delete from User u where u.active = false")
  void deleteInactiveUsers();
}

Locking

可以给repository的方法加上锁并指定锁类型。

interface UserRepository extends Repository<User, Long> {

  // Plain query method
  @Lock(LockModeType.READ)
  List<User> findByLastname(String lastname);

  // Redeclaration of a CRUD method
  @Lock(LockModeType.READ);
  List<User> findAll();
}