@chris-ren
2016-12-20T09:20:26.000000Z
字数 14701
阅读 2339
未分类
Spring Data旨在简化数据库的访问。针对关系型数据库,KV数据库,Document数据库,Graph数据库,Map-Reduce等一些主流数据库,采用统一技术进行访问,并且尽可能简化访问手段。
针对不同的数据储存访问使用对应的数据库库来操作访问。Spring Data中已经为我们提供了很多业务中常用的一些接口和实现类来帮我们快速构建项目,比如分页、排序、DAO一些常用的操作。

Spring Data 包含多个主要模块(还包括一些社区模块):
首先是Repository的最顶层接口:
public interface Repository<T, ID extends Serializable> {}
该接口只是一个空的接口,目的是为了统一所有Repository类型,该接口使用了泛型,其中T代表实体类型,ID代表实体主键ID类型。
Repository的直接子接口CrudRepository接口:
public interface CrudRepository<T, ID extends Serializable> extends Repository<T, ID> {<S extends T> S save(S entity);<S extends T> Iterable<S> save(Iterable<S> entities);T findOne(ID id);boolean exists(ID id);Iterable<T> findAll();Iterable<T> findAll(Iterable<ID> ids);long count();void delete(ID id);void delete(T entity);void delete(Iterable<? extends T> entities);void deleteAll();}
可以看到在CrudRepository接口中提供了基本的查询、保存、删除等方法。
同时还提供了一个PagingAndSortingRepository接口,增加了分页和排序功能:
public interface PagingAndSortingRepository<T, ID extends Serializable> extends CrudRepository<T, ID> {Iterable<T> findAll(Sort sort);Page<T> findAll(Pageable pageable);}
以上这几个接口都是spring-data-commons提供的核心接口,已经为我们提供了基本的操作,如果需要实现这些方法,只需要定义一个接口继承它即可,后面我们会详细介绍使用方式。
针对spring-data-jpa也提供了一系列repository接口,有JpaRepository和JpaSpecificationExecutor:
JpaRepository:继承PagingAndSortingRepository接口,是针对JPA技术的接口,提供flush(),saveAndFlush(),deleteInBatch(),deleteAllInBatch()等方法。
【将实体和底层数据库进行同步,当调用persist、merge或者remove方法时,更新并不会立刻同步到数据库中,直到容器决定刷新到数据库中时才会执行,可以调用flush强制刷新。】
JpaSpecificationExecutor:JPA2.0提供了Criteria API,可以用于动态生成query。Spring Data JPA支持Criteria查询便是使用JpaSpecificationExecutor,后面会详细介绍。
标准的增删改查需要写SQL语句查询数据库,但是在Spring Data中只需要简单的几个步骤:
1. 声明一个继承Repository或它的子接口的接口,如下:
interface PersonRepository extends Repository<Person, Long> { … }
interface PersonRepository extends Repository<Person, Long> {List<Person> findByLastname(String lastname);}
只需要声明,不需要实现。
3. 进行配置为这些接口创建代理实例:
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;@EnableJpaRepositoriesclass Config {}
public class SomeClient {@Autowiredprivate PersonRepository repository;public void doSomething() {List<Person> persons = repository.findByLastname("Matthews");}}
通常,我们的Repository会继承Repository, CrudRepository 或者PagingAndSortingRepository中的一个。与继承 Repository 等价的一种方式,就是在持久层接口上使用 @RepositoryDefinition 注解,并为其指定 domainClass 和 idClass 属性。如下:
public interface UserDao extends Repository<User, Long> { …… }@RepositoryDefinition(domainClass = User.class, idClass = Long.class)public interface UserDao { …… }
继承CrudRepository接口会让你暴露出很多方法来操作你的实体类。如果你仅仅想暴露几个接口给其他人使用,那么你可以继承Repository,然后从CrudRepository中拷贝几个需要的方法到自己的Repository中。
@NoRepositoryBeaninterface MyBaseRepository<T, ID extends Serializable> extends Repository<T, ID> {T findOne(ID id);T save(T entity);}interface UserRepository extends MyBaseRepository<User, Long> {User findByEmailAddress(EmailAddress emailAddress);}
多个Spring Data模块下使用Repository
Spring Data有两种方式解析用户的查询意图:第一种是直接通过方法的命名规则解析,第二种是通过Query来解析,那么Spring Data如何选择用哪种方式呢?Spring Data有一个查询策略决定到底使用哪种方式(可以通过queryLookupStrategy属性进行配置):
CREATE:通过解析方法名来创建查询。这个策略是根据方法命名规则,删除方法中固定的前缀,然后再解析其余的部分。
USE_DECLARED_QUERY:根据已经定义好的语句去查询,如果找不到,就会抛出异常信息。
CREATE_IF_NOT_FOUND(默认):这个策略结合了以上两个策略。会优先查询是否有定义好的查询语句,如果没有,就根据方法的名字去构建查询。这是一个默认策略。
Spring Data中有一套内置的查询构建器,它非常强大,会从方法名中剔除find…By,read…By,query…By,count…By,以及get…By等前缀,然后开始解析其余的名字。你可以在方法名中加入更多的表达式,例如你需要Distinct的约束,那么你在方法名中加入Distinct即可。在方法名中,第一个By表示查询语句的开始,你也可以用And或者Or来关联多个条件。
public interface PersonRepository extends Repository<User, Long> {List<Person> findByEmailAddressAndLastname(EmailAddress emailAddress, String lastname);// Enables the distinct flag for the queryList<Person> findDistinctPeopleByLastnameOrFirstname(String lastname, String firstname);List<Person> findPeopleDistinctByLastnameOrFirstname(String lastname, String firstname);// Enabling ignoring case for an individual propertyList<Person> findByLastnameIgnoreCase(String lastname);// Enabling ignoring case for all suitable propertiesList<Person> findByLastnameAndFirstnameAllIgnoreCase(String lastname, String firstname);// Enabling static ORDER BY for a queryList<Person> findByLastnameOrderByFirstnameAsc(String lastname);List<Person> findByLastnameOrderByFirstnameDesc(String lastname);}
以下表格中展示出方法名中支持的关键字:

上面说的查询方式只适合实体类上的直接属性,假设一个类Person中有个Address,并且Address还有ZipCode,那么根据ZipCode来查询这个Person应该是下面这种写法:
List<Person> findByAddressZipCode(ZipCode zipCode);
在上面的例子中,框架在解析该方法时,首先剔除 findBy,然后对剩下的属性进行解析,详细规则如下:
先判断 AddressZipCode (根据 POJO 规范,首字母变为小写,下同)是否为 实体的一个属性,如果是,则表示根据该属性进行查询;如果没有该属性,继续第二步;
从右往左截取第一个大写字母开头的字符串(此处为 Code),然后检查剩下的字符串(AddressZip)是否为实体的一个属性,如果是,则表示根据该属性进行查询;如果没有该属性,则重复第二步,继续从右往左截取;最后解析到Address为实体的一个属性;
接着处理剩下部分( ZipCode ),先判断 Address 所对应的类型是否有 ZipCode 属性,如果有,则表示该方法最终是根据 "Person.Address.ZipCode" 的取值进行查询;否则继续按照步骤 2 的规则从右往左截取。
这种解析可能会适合大部分的案例,但是有可能会出错(比如,Person中有一个Address属性,还有一个AddressZip属性),为了避免这种解析的问题,可以用“_”来区分,如:
List<Person> findByAddress_ZipCode(ZipCode zipCode);
上面的例子已经展示了绑定简单的参数,除此之外,我们还可以绑定一些指定的参数,如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);
第一个方法通过传递org.springframework.data.domain.Pageable来实现分页功能。如果需要排序功能,那么需要添加参数org.springframework.data.domain.Sort,返回的对象可以是List,也可以是Page类型。
Page与Slice:Page是知道元素和页的总数的。它是通过基础框架生成一个计数查询来计算总数的。这可能成本较高,此时可以使用Slice。Slice。只知道是不是有下一个Slice,这在遍历一个大结果集时已足够。Page像是个容器,而Slice像只是个迭代器,所以在大数据量时可以考虑用Slice。
可以通过first或top关键字限制查询结果:
//不写数字默认就是1User findFirstByOrderByLastnameAsc();User findTopByOrderByAgeDesc();Slice<User> findTop3ByLastname(String lastname, Pageable pageable);List<User> findFirst10ByLastname(String lastname, Sort sort);List<User> findTop10ByLastname(String lastname, Pageable pageable);
//用java.util.concurrent.Future作为返回结果@AsyncFuture<User> findByFirstname(String firstname);//用Java 8 java.util.concurrent.CompletableFuture 作为返回结果@AsyncCompletableFuture<User> findOneByFirstname(String firstname);//用org.springframework.util.concurrent.ListenableFuture 作为返回结果@AsyncListenableFuture<User> findOneByLastname(String lastname);
要在spring中启用@Async,需要增加@EnableAsync 的配置:
@Configuration@EnableAsyncpublic class SpringAsyncConfig { ... }
通过@Query注解查询,可以通过JPQL语句或原生SQL进行查询。
public interface UserRepository extends JpaRepository<User, Long> {@Query("select u from User u where u.emailAddress = ?1")User findByEmailAddress(String emailAddress);//like查询@Query("select u from User u where u.firstname like %?1")List<User> findByFirstnameEndsWith(String firstname);//使用原生的 SQL,原生SQL目前不支持分页@Query(value = "SELECT * FROM USERS WHERE EMAIL_ADDRESS = ?1",nativeQuery = true)User findByEmailAddress(String emailAddress);//通过": 变量"的格式指定参数,同时在方法的参数前面使用 @Param 将方法参数与JPQL中的命名参数对应@Query("from AccountInfo a where a.accountId = :id")public AccountInfo findByAccountId(@Param("id")Long accountId);//用@Modifying,这样框架最终会生成一个更新的操作,而非查询。@Modifying@Query("update User u set u.firstname = ?1 where u.lastname = ?2")int setFixedFirstnameFor(String firstname, String lastname);}
在Spring Data JPA 1.4以后,支持在@Query中使用SpEL表达式(简介)来接收变量。
SpEL支持的变量:
以下的例子中,我们在查询语句中插入表达式:
@Entitypublic class User {@Id@GeneratedValueLong 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接口,那么可以用这个表达式来处理:
@MappedSuperclasspublic abstract class AbstractMappedType {…String attribute}@Entitypublic class ConcreteType extends AbstractMappedType { … }@NoRepositoryBeanpublic 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 ConcreteRepositoryextends MappedTypeRepository<ConcreteType> { … }
Spring Data 支持以Java 8的Stream作为查询返回结果类型:
@Query("select u from User u")Stream<User> findAllByCustomQueryAndStream();Stream<User> readAllByFirstnameNotNull();@Query("select u from User u")Stream<User> streamAllPaged(Pageable pageable);
流在使用后必须进行关闭,你可以通过close() 方法或者使用Java 7的try-with-resources块:
try (Stream<User> stream = repository.findAllByCustomQueryAndStream()) {stream.forEach(…);}
在查询时,Spring Data Repositories 一般返回实体对象,但有时候你可能需要改变展示的实体视图,这个时候你可以自己定义Projections,如下例子:
@Entitypublic class Person {@Id @GeneratedValueprivate Long id;private String firstName, lastName;@OneToOneprivate Address address;…}@Entitypublic class Address {@Id @GeneratedValueprivate Long id;private String street, state, country;…}
Person里有几个属性:
主键key id;数据属性firstName和lastName;address是链接到其它实体的一个link
interface PersonRepository extends CrudRepository<Person, Long> {Person findPersonByFirstName(String firstName);}
如果按照上面的方式进行查询,返回的结果会包含Person中所有的属性。
如果你不想暴露address的详细信息,你可以通过定义一个或多个 projections来提供另一种选择:
//一个简单的Projection,暴露FirstName和LastName属性interface NoAddresses {String getFirstName();String getLastName();}
查询时使用如下方式:
interface PersonRepository extends CrudRepository<Person, Long> {NoAddresses findByFirstName(String firstName);}
重构数据
从上面可以看出,你可以自己定义projections 来暴露一些信息,你也可以在projection中增加自己的虚拟属性:
interface RenamedProperty {String getFirstName();//增加了name属性,这个属性指向lastName的值@Value("#{target.lastName}")String getName();}
按照需求对属性值进行format:
interface FullNameAndCountry {@Value("#{target.firstName} #{target.lastName}")String getFullName();@Value("#{target.address.country}")String getCountry();}
高级使用,@value中可以使用表达式:
假设有如下的实体:
@Entitypublic class User {@Id @GeneratedValueprivate Long id;private String name;private String password;…}
假设需要对密码特殊处理,有密码时以******显示:
interface PasswordProjection {@Value("#{(target.password == null || target.password.empty) ? null : '******'}")String getPassword();}
JPA 2 引入了criteria API 建立查询,Spring Data JPA使用Specifications来实现这个API。在Repository中,你需要继承JpaSpecificationExecutor:
public interface CustomerRepository extends CrudRepository<Customer, Long>, JpaSpecificationExecutor {…}
JpaSpecificationExecutor接口定义如下:
public interface JpaSpecificationExecutor<T> {T findOne(Specification<T> spec);List<T> findAll(Specification<T> spec);Page<T> findAll(Specification<T> spec, Pageable pageable);List<T> findAll(Specification<T> spec, Sort sort);long count(Specification<T> spec);}
Specification 接口定义如下:
public interface Specification<T> {Predicate toPredicate(Root<T> root, CriteriaQuery<?> query,CriteriaBuilder builder);}
所以,我们要如何实现这个接口呢?如下定义一个Customer的Specifications :
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}};}}
接下来,我们如何去调用这个方法呢?
List<Customer> customers = customerRepository.findAll(isLongTermCustomer());
对于上面的一个条件简单查询,其实没有必要使用Specification,当有多个条件组合查询的时候,才能显示出它的优势:
MonetaryAmount amount = new MonetaryAmount(200.0, Currencies.DOLLAR);List<Customer> customers = customerRepository.findAll(where(isLongTermCustomer()).or(hasSalesOfMoreThan(amount)));
默认的CRUD操作在Repository里面都是事务性的。对于查询操作,默认配置的事务是readOnly的,其他操作则配置为@Transaction。如果你想修改一个Repository的事务,只需要在子接口中重写并且修改它的事务:
public interface UserRepository extends CrudRepository<User, Long> {//这里findAll方法会有10秒的超时,并且不是只读事务@Override@Transactional(timeout = 10)public List<User> findAll();// Further query method declarations}
另一种方式是在调用或者service实现层修改,一般是包含多个repository:
@Serviceclass UserManagementImpl implements UserManagement {private final UserRepository userRepository;private final RoleRepository roleRepository;@Autowiredpublic UserManagementImpl(UserRepository userRepository,RoleRepository roleRepository) {this.userRepository = userRepository;this.roleRepository = roleRepository;}@Transactionalpublic void addRoleToAllUsers(String roleName) {Role role = roleRepository.findByName(roleName);for (User user : userRepository.findAll()) {user.addRole(role);userRepository.save(user);}}
SpringData为操作审计提供了支持。如果想要实现这些支持,仅仅需要使用几个注解或者实现接口即可。
SpringData提供了@CreatedBy,@LastModifiedBy去捕获谁操作了实体,当然还有相应的操作时间@CreatedDate和@LastModifiedDate。
class Customer {@CreatedByprivate User user;@CreatedDateprivate DateTime createdDate;// … further properties omitted}
基于接口的审计
如果你不想用注解来做审计的话,那么你可以实现Auditable接口。他暴露了审计属性的get/set方法。
如果你不想实现接口,那么你可以继承AbstractAuditable,通常来说,注解方式时更加方便的。
如果你在用@CreatedBy或者@LastModifiedBy的时候,想植入当前的业务操作者,那你可以使用AuditorAware接口。
下面给出一个案例,我们将结合SpringSecurity来做:
class SpringSecurityAuditorAware implements AuditorAware<User> {public User getCurrentAuditor() {Authentication authentication = SecurityContextHolder.getContext().getAuthentication();if (authentication == null || !authentication.isAuthenticated()) {return null;}return ((MyUserDetails) authentication.getPrincipal()).getUser();}}
JPA审计
SpringData JPA有一个实体监听器,他可以用于触发捕获审计信息。要用之前,你需要在orm.xml里面注册AuditingEntityListener,当然你还需要引入spring-sapects.jar:
<persistence-unit-metadata><persistence-unit-defaults><entity-listeners><entity-listener class="….data.jpa.domain.support.AuditingEntityListener" /></entity-listeners></persistence-unit-defaults></persistence-unit-metadata>
你也可以用@EntityListeners 注解为单个实体启用AuditingEntityListener :
@Entity@EntityListeners(AuditingEntityListener.class)public class MyEntity {}
要启用这个审计,还需要在配置文件里面增加如下配置:
<jpa:auditing auditor-aware-ref="yourAuditorAwareBean" />
Spring Data JPA 1.5以上,可以通过@EnableJpaAuditing注解启用审计:
@Configuration@EnableJpaAuditingclass Config {@Beanpublic AuditorAware<AuditableUser> auditorProvider() {return new AuditorAwareImpl();}}
这部分主要介绍把SpringData扩展到其他的框架中。
Querydsl是一个Java开源框架,用于构建类型安全的SQL查询语句,它采用API代替拼凑字符串来构造查询语句。
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.}
定义自己的Repository继承QueryDslPredicateExecutor:
interface UserRepository extends CrudRepository<User, Long>, QueryDslPredicateExecutor<User> {}
调用时,通过如下方式调用:
Predicate predicate = user.firstname.equalsIgnoreCase("dave").and(user.lastname.startsWithIgnoreCase("mathews"));userRepository.findAll(predicate);