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,有Repository
,CrudRepository
,和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 处理,为空则抛出EmptyResultDataAccessException
或 IllegalArgumentException
异常。
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…By
,read…By
,query…By
,count…By
及 get…By
开头的方法名,并解析其名称余下部分,上述方法的 …
部分可以包含一些表达式,如 Distinct
;by
是一个重要的分隔符,其后部分将会解析为具体的 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);
}
注意:
- 可以使用
AND
和OR
将条件串联起来,或者Between
,LessThan
和GreaterThan
等表达式限制条件,但具体支持情况会因为存储数据库的有相应差异; IgnoreCase
表达式可以忽略查询条件的大小写,是否支持同样需要存储数据库支持;OrderBy
可以实现对于指定字段的升降序排序;
Property Expressions
List<Person> findByAddressZipCode(ZipCode zipCode);
假设一个 Person 类中有一个级联对象为 Address 带有一个 ZipCode 属性,该定义的方法名是查询该属性。
- 解析过程是首先去除
findBy
部分对余下部分AddressZipCode
转化为addressZipCode
并解析,假设此时 Person 类有该名称的属性,则查询该属性; - 假如没有,则依照从右往左的顺序截取第一个大写开头的词,此处为
Code
并查询余下部分AddressZip
是否为实体Person
的一个属性,没有则按此规则继续截取; - 最后假如
Address
为实体属性,余下部分为ZipCode
,一样,首先判断该部分是否为Address
属性对应类型的一个属性, 没有对该部分继续按从右往左截取并按照规则判断。最终可以得到查询的完整属性为address.zipCooe
。
为了更直观地表达查询意图,可以按照以下方式显示定义查询属性 address.zipCooe
:
List<Person> findByAddress_ZipCode(ZipCode zipCode);
因此,Spring 官方建议实体类定义字段名称规范最好使用驼峰而不是下划线,以免与解析规则产生冲突。
Special parameter handling
上述定义查询方法名后可以通过传入一些特殊参数,如 Pageable
或 Sort
,动态地实现自动化分页或排序,示例如下:
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
,可以获取到元素总数和是否分页可用的信息。分页的返回类型有两种,分别为Page
和 Slice
。关于两者的区别,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);
注意:
Stream<T>
可能会用特定的数据结构包装指定的资源,因此需要在调用完资源后将其 close 掉,或者使用 Java7 的try-with-resource
特性处理;- 不是所有的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
EntityManagerFactory
及 PlatformTransactionManager
是使用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。
选择建议:
- 假如不需要异步引导JPA,选择
DEFAULT
模式即可; - 需要异步引导JPA,那么
DEFERRED
是个不错的选择; LAZY
是测试环境和开发环境一个不错的选择,假如你确保repository可以被正确引导,因为可以启动只是为了测试某个或几个repository bean。
Persisting Entities
Saving Entities
继承了CrudRepository接口的repository调用CrudRepository.save(…)
方法保存实体,假如判断该实体为新的,则调用EntityManager
的entityManager.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
,该对象包含两个部分Probe
和ExampleMatcher
,其中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();
}