8.2.1 点睛Spring Data JPA
1.什么是Spring Data JPA
在介绍Spring Data JPA的时候,我们首先认识下Hibernate。Hibernate是数据访问解决技术的绝对霸主,使用O/R映射(Object-Relational Mapping)技术实现数据访问,O/R映射即将领域模型类和数据库的表进行映射,通过程序操作对象而实现表数据操作的能力,让数据访问操作无须关注数据库相关的技术。
随着Hibernate的盛行,Hibernate主导了EJB3.0的JPA规范,JPA即Java Persistence API。JPA是一个基于O/R映射的标准规范。所谓规范即只定义标准规则(如注解、接口),不提供实现,软件提供商可以按照标准规范来实现,而使用者只需按照规范中定义的方式来使用,而不用和软件提供商的实现打交道。JPA的主要实现由Hibernate、EclipseLink和OpenJPA等,这也意味着我们只要使用JPA来开发,无论是哪一个开发方式都是一样的。
Spring Data JPA是Spring Data的一个子项目,它通过提供基于JPA的Repository极大地减少了JPA作为数据访问方案的代码量。
2.定义数据访问层
使用Spring Data JPA建立数据访问层十分简单,只需定义一个继承JpaRepository的接口即可,定义如下:
public interface PersonRepository extends JpaRepository<Person,Long> {
}
继承JpaRepository接口意味着我们默认已经有了下面的数据访问操作方法
@NoRepositoryBean
public interface JpaRepository<T, ID extends Serializable>
extends PagingAndSortingRepository<T, ID>, QueryByExampleExecutor<T> {
List<T> findAll();
List<T> findAll(Sort sort);
List<T> findAll(Iterable<ID> ids);
<S extends T> List<S> save(Iterable<S> entities);
void flush();
<S extends T> S saveAndFlush(S entity);
void deleteInBatch(Iterable<T> entities);
void deleteAllInBatch();
T getOne(ID id);
<S extends T> List<S> findAll(Example<S> example);
<S extends T> List<S> findAll(Example<S> example, Sort sort);
}
3.配置使用Spring Data JPA
在Spring环境中,使用Spring Data JPA可通过@EnableJpaRepositories注解来开启Spring Data JPA的支持,@EnableJpaRepositories接收的value参数用来扫描数据访问层所在包下的数据访问的接口定义。
4.定义查询方法
在讲解查询方法前,假设我们有一张数据表叫PERSON,有一ID(Number)、NAME(Varchar2)、AGE(Number)、ADDRESS(Varchar2)几个字段;对应的实体类叫Person,分别有id(Long)、name(String)、age(Integer)、address(String)。下面我们就以这个简单的实体查询作为演示。
(1)根据属性名查询
Spring Data JPA支持通过定义在Repository接口中的方法名来定义查询,而方法名的根据实体类的属性名来确定的。
1)常规查询 。根据属性名来定义查询方法,示例如下:
public interface PersonRepository extends JpaRepository<Person,Long> {
List<Person> findByName(String name);
List<Person>findByNameLike(String name);
List<Person>findByNameAndAddress(String name ,String address);
}
从代码可以看出,这里使用了findBy、Like、And这样的关键字。其中findBy可以用find、read、readBy、query、queryBy、get、getBy来代替。
而Like和and这类查询关键字,如表
关键字 | 示例 | 同功能JPQL |
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.first name like ?1(参数前面加%) |
EndingWith | findByFirstnameEndingWith | where x.firstname like ?1(参数后面加%) |
Containing | findByFirstnameContaining | where x.firstname like ?1(参数两边加%) |
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(Conllection<Age>ages) | where x.age not in ?1 |
True | findByActiveTrue() | where x.active =true |
False | findByActiveFalse() | where x.active =false |
IgnoreCase | findByFirstnameIgnoreCase | where UPPER(x.firstname) = UPPER(?1) |
2)限制结果数量。结果数量是用top和first关键字来实现的,例如:
public interface PersonRepository extends JpaRepository<Person,Long>{
/**
*获得符合查询条件的前10条数据
*/
List<Person> findFirst10ByName(String name);
/**
*获得符合查询条件的前30条数据
*/
List<Person> findTop30ByName(String name);
(2)使用JPA的NamedQuery查询
Spring Data JPA支持用JPA的NameQuery来定义查询方法,即一个名称映射一个查询语句。定义如下:
@Entity
@NamedQuery(name="Person.findByName, query = "select p from Person p where p.name = ?1")
public class Person{
}
使用如下语句:
public interfae PersonRepository extends JpaRepository<Person,Long>{
/**
*这时我们使用的是NamedQuery里定义的查询语句,而不是根据方法名称查询
*/
List<Person> findByName(String name);
(3)使用@Query查询
1)使用参数索引。Spring Data JPA还支持用@Query注解在接口的方法上实现查询,例如:
public interface PersonRepository extends JpaRepository<Person,Long>{
@Query("select p from Person p where p.address=?1")
List<Person> findByAddress(String address);
}
2)使用命名参数。上面的例子是使用参数索引号来查询的,在Spring Data JPA里还支持在语句里用名称来匹配查询参数,例如:
public interface PersonRepository extends JpaRepository<Person,Long>{
@Query("select p from Person P where p.address= :address")
List<Person> findByAddress(@Param("address") String address);
}
3)更新查询。Spring Data JPA支持@Modifying和@Query注解组合来事件更新查询,例如:
public interface PersonRepository extends JpaRepository<Person,Long> {
@Modifying
@Transactional
@Query("update Person p set p.name=?1)
int setName(String name);
}
(4) Specification
JPA提供了基于准则查询的方式,即Criteria查询。而Spring Data JPA提供了一个Specification(规范)接口让我们可以更方便地构造准则查询,Specification接口定义了一个toPredicate方法用来构造查询条件。
1)定义。我们的接口类必需实现JpaSpecificationExecutor接口,代码如下:
public interface PersonRepository extends JpaRepository<Person,Long>,JpaSpecificationExecutor<Person>{
}
然后需要定义Criterial查询,代码如下:
public static Specification<Person>personFromHefei(){
return new Specification<Person>(){
@Override
public Predicate toPredicate(Root<Person> root,CriteriaQuery<?>query,CriteriaBuilder cb){
return cb.equal(root.get("address"),"合肥");
}
};
}
2)使用。静态导入:
import static com.wisely.specs.CustomerSpecs.*;
注入personRepository的Bean后:
List<Person>people = personRepository.findAll(personFromHefei());
(5)排序与分布
Spring Data JPA充分考虑了在实际开发中所必需的排序和分页的场景,为我们提供了Sort类以及Page接口和Pageable接口。
1)定义:
public interface PersonRepository extends JpaRepository<Person,Long>{
List<Person> findByName(String name,Sort sort);
Page<Person>findByName(String name,Pageable pageable);
}
2)使用排序:
List<Person> people = personRepository.findByName("xx",new Sort(Direction.ASC,"age"));
3)使用分页:
Page<Person> people2 = personRepository.findByName("xx",new PageRequest(0,10));
5.自定义Repository的实现
Spring Data提供了和CrudRepository、PagingAndSortingRepository;Spring Data JPA也提供了JpaRepository。如果我们想把自己常用的数据库操作封装起来,像JpaRepository一样提供给我们领域类的Repository接口使用,应该怎么操作呢?
(1)定义自定义Repository接口:
@NoRepositoryBean //@NoRepositoryBean指明当前这个接口不是我们领域类的接口(如PersonRepository。
public interface CustomRepository<T,ID extends Serializable>extends PagingAndSortingRepository<T,ID>{ //我们自定义的Repository实现PagingAndSortingRepository接口,具备分页和排序的能力。
public void doSomething(ID id); //要定义的数据操作方法在接口中的定义。
}
(2)定义接口实现:
public class CustomRepositoryImpl<T,ID extends Serializable> extends SimpleJpaRepository<T,ID> implements CustomRepository<T,ID> { //首先要实现CustomRepository接口,继承SimpleJpaRepository类让我们可以使用其提供的方法(如findAll)。
private final EntityManager entityManager; //让数据操作方法中可以使用entityManager。
public CustomRepositoryImpl(Class<T> domainClass,EntityManager entityManager) { //CustomRepositoryImpl的构造函数,需当前处理的领域类类型和entityManager作为构造参数,在这里也给我们的entityManager赋值了。
supur(domainClass,entityManager);
this.entityManager=entityManager;
}
@Override
public void doSomething(ID id){
//在此处定义数据访问操作,如调用findAll方法并构造一些查询条件。
}
(3)自定义RepositoryFactoryBean。自定义JpaRepositoryFactoryBean替代默认RepositoryFactoryBean,我们会获得一个RepositoryFactory,RepositoryFactory将会注册我们自定义的Repository的实现。
(4)开启自定义支持使用@EnableJpaRepositoryes的RepositoryFactoryBeanClass来指定FactoryBean即可
8.2.2 Spring Boot 的支持
1.JDBC的自动配置
spring-boot-starter-data-jpa依赖于spring-boot-starter-jdbc,而Spring Boot对JDBC做了一些自动配置。源码放置在org.springframework.boot.autoconfigure.jdbc下,
从源码分析可以看出,我们通过"spring.datasoure"为前缀的属性自动配置dataSource;Spring Boot 自动开启了注解事务的支持(@EnableTransactionManagement);还配置了一个jdbcTemplate。
Spring Boot还提供了一个初始化数据的功能:放置在类路径下的schema.sql文件会自动用来初始化表结构;放置在类路径下的data.sql文件会自动用来填充表数据。
2.对JPA的自动配置
Spring Boot对JPA的自动配置放置在org.springframework.boot.autoconfigure.orm.jpa下,
从HibernateJpaAutoConfiguration可以看出,Spring Boot默认JPA的实现者是Hibernate;
HibernateJpaAutoconfiguration依赖于DataSourceAutoConfiguration。
从JpaProperties的源码可以看出,配置JPA可以使用spring.jpa 为前缀的属性在applicatonproperties中配置。
从JpaBaseConfiguration的源码中可以看出,Spring Boot为我们配置了transactionManager、jpaVendorAdapter、entityManagerFactory等Bean。JpaBaseConfiguration还有一个getPackagesToScan方法,可以自动扫描注解有@Entity的实体类。
在Web项目中我们经常会遇到在控制器或者页面访问数据的时候出现会话连接已关闭的错误,这时候我们会配置一个Open EntityManager(Session)In View这个过滤器。令人惊喜的是,Spring Boot为我们自动配置了OpenEntityManagerInViewInterceptor这个Bean,并注册到Spring MVC的拦截器中。
3.对Spring Data JPA的自动配置
而Spring Boot对Spring Data JPA的自动配置放置在org.springframework.boot.autoconfigure.data.jpa下,如图
从JpaRepositoriesAutoConfiguration和JpaRepositoriesAutoConfigureRegistrar源码可以看出,JpaRepositoriesAutoConfigration是依赖于HibernateJpaAutoConfiguration配置的,且Spring Boot 自动开启了对Spring Data JPA的支持,即我们无须在配置类显示声明@EnableJpaRepositories。
4.Spring Boot下的Spring Data JPA
通过上面的分析可知,我们在Spring Boot下使用Spring Data JPA,在项目的Maven依赖里添加spring-boot-stater-data-jpa,然后只需定义DataSource、实体类和数据访问层,并在需要使用数据访问的地方注入数据访问层的Bean即可,无须任何额外配置。
8.2.3实战
在本节的实战里,我们将演示基于方法名的查询、基于@Query的查询、分页及排序,最后 我们将结合Specification和自定义Repository实现 来完成一个通用实体查询,即对于任意类型的实体类的传值对象,只要对象有值的属性我们就进行自动构造查询(字符型用like,其它类型用等于)。这里起一个抛砖引玉的功能,感兴趣的可以继续扩展,如构造范围查询及关联表查询等。
1.安装 Oracle XE
因大部分Java程序员在实际开发中一般使用的是Oracle,所以此处选择用Oracle XE作为开发测试数据库。
Oracle XE是Oracle公司提供的免费开发测试用途的数据库,可自由使用,功能和使用与Oracle完全一致,但数据大小限制为4G。
(1)非Docker安装
不打算使用Docker安装Oracle XE的请至 http://www.oracle.com/technetwork/database/database-technologies/express-edition/downloads/index.html下载Oracle XE安装。
(2)Docker安装
我们在8.13节已经下载了Oracle XE的镜像,现在我们运行一个Oracle XE的容器。
运行命令
docker run -d -p 9090:8080 -p 1521:1521 gswteam/docker-oracle-xe-11g
(3)端口映射
我们在8.1节曾经提到,容器暴露的端口只是映射到VirtualBox虚拟机上,而本机要访问容器的话需要我们把VirtualBox虚拟机的端口映射到当前开发机器上。这确实有点麻烦,但是在生产环境我们一般都是基于Linux部署Docker的,所以不会存在这个问题。
下面我们演示将VirtualBox虚拟机的端口映射到当前开发机器。
打开VirtualBox软件。
选中boot2docker-vm,单击“设置”按钮,或者右击,在右键菜单中选择“设置”,找开虚拟机设置页面
单击“网络”,页面下方出现了“端口转发”按钮,如图
单击端口转发按钮,弹出“端口转发规则”界面,将我们刚才暴露到虚拟机的9090及1521端口映射为开发机的9090及1521端口,如图
做了如上设置后,我们即可通过本机9090及1521端口正确访问Oracle XE容器里的端口了。
(4)管理
通过上面的设置之后,我们就可以像操作普通的Oracle数据库一样操作Oracle XE了。我们可以通过访问XE的管理界面:http://localhost:9090/apex登录管理数据库;或者在开发机器安装Oracle Client,管理并安装一个数据库管理工具(如PL/SQL Developer)来管理数据库。
利用我们的管理工具创建一个用户,作为我们程序使用的数据库账号,账号密码皆为boot。
3.新建Spring Boot项目
搭建Spring Boot项目,依赖选择JPA和WEB。
项目信息
groupId:com.wisely
arctifactId:ch8_2
package:com.wisely.ch8_2
因为我们使用的是Oracle XE数据库,所以需要使用Oracle的JDBC驱动,而Maven中心库没有Oracle JDBC的驱动下载,因此我们需要通过Maven命令,自己打包Oracle的JDBC驱动到本地。
在Oracle官网下载ojdbc6.jar(http://www.oracle.com/technetwork/database/enterprise-edition/jdbc-112010-090769.html),当然一般我们都有这个jar包。
通过控制台执行下面命令,将ojdbc6.jar安装到本地库:
mvn install:install-file -DgroupId=com.oracle "-DartifactId=ojdbc6" "-Dversion=11.2.0.2.0" "-Dpackaging=jar" "-Dfile=E:\ojdbc6.jar"
说明:
-DgroupId=com.oracle :指定当前包的groupId为com.oracle。
-DartifactId=ojdbc6:指定当前包的artifactfactId为ojdbc6。
-Dversion=11.2.0.2.0:指定当前包version为11.2.0.2.0。
-Dfile=E:\ojdbc6.jar:指定要打包的jar的文件位置。
此时ojdbc6被打包的本地库,如图
这时我们只需在Spring Boot项目中的pom.xml加入下面坐标即可引入ojdbc6:
<dependency>
<groupId>com.oracle</groupId>
<artifactId>ojdbc6</artifactId>
<version>11.2.0.2.0</version>
</dependency>
添加google guava依赖,它包含大量Java常用的工具类:
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>18.0</version>
</dependency>
新建一个data.sql文件放置在src/main/resources下,内容为向表格增加一些数据,数据插入完成后请删除或对此文件改名:
insert into person(id,name,age,address) values(hibernate_sequence.nextval,'管理员',32,'深圳');
insert into person(id,name,age,address) valeus(hibernate_sequence.nextval,'xx',31,'北京');
insert into person(id,name,age,address) valeus(hibernate_sequence.nextval,'yy',30,'上海');
insert into person(id,name,age,address) valeus(hibernate_sequence.nextval,'zz',29,'南京');
insert into person(id,name,age,address) valeus(hibernate_sequence.nextval,'xx',28,'武汉');
insert into person(id,name,age,address) valeus(hibernate_sequence.nextval,'xx',27,'深圳');
4.配置基本属性
在application.properties里配置数据源和jpa的相关属性。
spring.datasource.driverClassName=oracle.jdbc.OracleDriver
spring.datasource.url=jdbc\:oracle\:thin\:@192.168.99.100\:1521\:xe
spring.datasource.username=boot
spring.datasource.password=boot
#1
spring.jpa.hibernate.ddl-auto=update
#2
spring.jpa.show-sql=true
#3
spring.jackson.serialization.indent_output=true
5.定义映射实体类
Hibernate支持自动将实体映射为数据表格:
package com.wisely.domain;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import org.hibernate.annotations.NamedQuery;
@Entity //指明这是一个和数据库表映射的实体类
@NamedQuery(name="Person.withNameAndAddressNamedQuery", query = "select p from Person p where p.name=?1 and address=?2")
public class Person {
@Id //指明这个属性映射为数据库的主键
@GeneratedValue //默认使用主键生成方式为自增,hibernate会为我们自动生成一个名为HIBERNATE_SEQUENCE的序列。
private Long id;
private String name;
private Integer age;
private String address;
public Person() {
super();
}
public Person(Long id,String name,Integer age,String address) {
super();
this.id=id;
this.name=name;
this.age=age;
this.address=address;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
public String getAddress() {
return address;
}
public void setAddress(String address) {
this.address = address;
}
}
在此例中使用的注解也许和你平时经常使用的注解实体类不大一样,比如没有使用@Table(实体类映射表名)、@Column(属性映射字段名)注解。这是因为我们是采用正向工程通过实体类生成表结构,而不是通过逆向工程从表结构生成数据库。
在这里你可能注意到,我们没有通过@Column注解来注解普通属性,@Column是用为映射属性名和字段名,不注解的时候hibernate会自动根据属性名生成数据表的字段名。如属性名name映射成字段NAME;多字母属性如testName会自动映射为TEST_NAME。表名的映射规则也如此。
定义数据访问接口
package com.wisely.web;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Sort;
import org.springframework.data.domain.Sort.Direction;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.wisely.dao.PersonRepository;
import com.wisely.domain.Person;
@RestController
public class DataController {
//1 Spring Data JPA 已自动为你注册bean,所以可自动注入
@Autowired
PersonRepository personRepository;
/**
* 保存
* sava支持批量保存:<S extends T> Iterable<S> save(Iterable<S> entities);
*
* 删除:
* 支持使用id删除对象、批量删除以及删除全部:
* void delete(ID id)
* void delete(T entity)
* void delete(Iterable<? extends T> entities);
* void deleteAll();
*/
@RequestMapping("/save")
public Person save(String name,String address,Integer age) {
Person p = personRepository.save(new Person(null,name,age,address));
return p;
}
/**
* 测试finByAddress
*/
@RequestMapping("/q1")
public List<Person> q1(String address){
List<Person> people = personRepository.findByAddress(address);
return people;
}
/**
* 测试findByNameAndAddress
*/
@RequestMapping("/q2")
public Person q2(String name,String address) {
Person people = personRepository.findByNameAndAddress(name, address);
return people;
}
/**
* 测试withNameAndAddressQuery
*/
@RequestMapping("/q3")
public Person q3(String name,String address) {
Person p = personRepository.withNameAndAddressQuery(name, address);
return p;
}
/**
* 测试withNameAndAddressNamedQuery
*/
@RequestMapping("/q4")
public Person q4(String name,String address) {
Person p = personRepository.withNameAndAddressQuery(name, address);
return p;
}
/**
* 测试排序
*/
@RequestMapping("/sort")
public List<Person> sort(){
List<Person> people = personRepository.findAll(new Sort(Direction.ASC,"age"));
return people;
}
/**
* 测试分页
*/
@RequestMapping("/page")
public Page<Person> page(){
Page<Person> pagePeople = personRepository.findAll(new PageRequest(1, 2));
return pagePeople;
}
}
下面分别访问地址测试运行效果。
访问http://localhost:8080/save?name=dd&address=上海&age=25,如图
访问http://localhost:8080/q1?address=深圳,如图
访问http://localhost:8080/q2?address=深圳&name=管理员,如图
访问http://localhost:8080/q3?address=深圳&name=管理员,如图
访问http://localhost:8080/q4?address=深圳&name=管理员,如图
访问http://localhost:8080/sort,如图
访问http://localhost:8080/page,如图