本文参考了Spring Data JPA官方文档,引用了部分文档的代码。html
Spring Data JPA是Spring基于Hibernate开发的一个JPA框架。若是用过Hibernate或者MyBatis的话,就会知道对象关系映射(ORM)框架有多么方便。可是Spring Data JPA框架功能更进一步,为咱们作了 一个数据持久层框架几乎能作的任何事情。下面来逐步介绍它的强大功能。java
咱们能够简单的声明Spring Data JPA的单独依赖项。以Gradle为例,依赖项以下,Spring Data JPA会自动添加它的Spring依赖项。当前版本须要Spring框架版本为4.3.7.RELEASE
或更新,使用旧版本的Spring框架可能会出现bug。因为Spring Data JPA基于Hibernate,因此别忘了添加Hibernate的依赖项。mysql
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。重点在于<jpa:repositories base-package="yitian.study.dao"/>
一句,它告诉Spring去哪里寻找并建立这些接口类。web
<!--启用注解配置和包扫描--> <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>
前几天学了一点Groovy,再回头看看Java,实在是麻烦。因此这里我用Groovy写的实体类,不过语法和Java很类似。你们能看懂意思便可。不过确实Groovy能比Java少些不少代码,对开发挺有帮助的。有兴趣的同窗能够看看个人Groovy学习笔记。spring
Groovy类的字段默认是私有的,方法默认是公有的,分号能够省略,对于默认字段Groovy编译器还会自动生成Getter和Setter,能够减小很多代码量。只不过equals等方法不能自动生成,多少有点遗憾。这里使用了JPA注解,创建了一个实体类和数据表的映射。sql
@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> { }
而后测试一下,会发生什么事情呢?查看一下数据库就会发现数据已经成功插入了。好吧,好像没什么有魔力的事情。express
@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结尾的全部用户!app
@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 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<T>类型,咱们能够调用它的方法获取总页数和可迭代的数据集合。下面是一个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…By
和 get…By
作开头。在By以前能够添加Distinct表示查找不重复数据。By以后是真正的查询条件。Between
, LessThan
, GreaterThan
, Like
,And
,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);
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 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会注册HandlerMethodArgumentResolver
、PageableHandlerMethodArgumentResolver
和SortHandlerMethodArgumentResolver
等几个实例。它们支持从请求参数中读取分页和排序信息。
@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对象。请求参数的详细信息以下。
?sort=firstname&sort=lastname,asc
若是须要多个分页对象,咱们能够用@Qualifier注解,而后请求对象就能够写成foo_page
,bar_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"; } }
若是查询方法不能彻底知足须要,咱们可使用自定义查询来知足需求。使用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/orm_2_0.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)
在上面的方法中,查询方法和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注解用来指定某个查询是一个更新操做,这样可让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)));
前段时间在研究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