目录 在上一篇《SpringBoot-工程结构、配置文件以及打包》中,我们介绍了Spring Boot项目的工程结构、基本配置、使用IDEA开发的配置,以及可执行jar包的结构和打包方式,对Spring Boot的项目有了整体的认识。在本篇,我们将介绍JPA的使用。 简介百度百科对JPA的解释是这样的:
简单来说,Hibernate这种ORM的持久化框架的出现极大的简化了数据库持久层的操作,随着使用人数的增多,ORM的概念越来越深入人心。因此,JAVA专家组结合Hibernate,提出了JAVA领域的ORM规范,即JPA。 JPQL 其实同Hibernate的HQL类似,是JPA标准的面向对象的查询语言。百度百科的解释如下:
JPA与Hibernate的关系 JPA是参考Hibernate提出的Java持久化规范,而Hibernate全面兼容JPA,是JPA的一种标准实现。 JPA与Spring Data JPA Spring Boot对JPA的支持其实使用的是Spring Data JPA,它是Spring对JPA的二次封装,默认实现使用的是Hibernate,支持常用的功能,如CRUD、分页、条件查询等,同时也提供了强大的扩展能力。 HelloWorld僚机了JPA的概念过后,接下来,我们使用Spring Boot工程来实现一个最简单的CRUD操作,看看使用JPA我们需要做哪些事情。 引入依赖 直接引入如下依赖: <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-devtools</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency>1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.20.21.22.23.24.25.26.27.28.29.30.31.32.33. lombok是一款自动生成样板代码、简化源码、提高可读性的工具,功能基于注解实现,例如常见的自动生成getter、setter、equlas、hashcode代码等,Spring boot官方推荐使用,官方地址:https://www./。在使用idea开发的时候,需要安装lombok idea插件,插件库搜索即可找到。 定义Entity Gender: public enum Gender { MALE, FEMALE; }1.2.3. Employee: @Entity @Table(name = "employee") @Data @EqualsAndHashCode public class Employee { @Id @GeneratedValue(strategy = GenerationType.AUTO) private Long id; @Column(nullable = false, columnDefinition = "varchar(50) COMMENT '姓名'") private String name; @Column(nullable = false, columnDefinition = "bit COMMENT '性别'") @Enumerated(EnumType.ORDINAL) private Gender gender = Gender.MALE; private Integer age; }1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18. 其中 创建Repository接口 创建接口很简单,只需要继承Spring Data JPA提供的接口即可: public interface DefaultEmployeeDao extends JpaRepository<Employee, Long> { }1.2. 这里继承了Spring Data提供的 定义查询方法 如果默认的这些方法不能满足需要,你还可以在接口中自定义查询方法: public interface DefaultEmployeeDao extends JpaRepository<Employee, Long> { List<Employee> findByNameLike(String name); }1.2.3. 这里,定义了一个按照name属性进行模糊查询的方法。 Spring Data JPA有一套根据方法名称自动解析并生成查询实现的规则,极大的简化了开发工作,正如上边的例子,要实现按照名称模糊查询,只需要按照规则定义方法名即可,不需要做更多的事情。具体的规则和用法下边章节会详细介绍。 使用Repository接口 使用接口很简单,只需要在service中注入,就可以获得定义的全部方法: @Service public class DefaultEmployeeService { private static Logger log = LoggerFactory.getLogger(DefaultEmployeeService.class); @Autowired private DefaultEmployeeDao employeeDao; public Employee add(Employee employee) { return employeeDao.save(employee); } public Employee update(Employee employee) { return employeeDao.save(employee); } public void delete(Long id) { employeeDao.delete(id); } public List<Employee> queryAll() { return employeeDao.findAll(); } public Employee getById(Long id) { return employeeDao.findOne(id); } public List<Employee> queryByName(String name) { name = "%" + name + "%"; return employeeDao.findByNameLike(name); } }1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.20.21.22.23.24.25.26.27.28.29.30.31.32. 上边的Service定义了常见的CRUD的和模糊查询方法。 到这里,最简单的CRUD业务逻辑就完成了,是不是很简单呢? 正如你所见的,我们不需要实现Repository接口,只需要继承已有接口,接下来的事情就交给Spring Data JPA来处理,它会为我们自动生成代理实现。 下面,我们来详细了解下到底有哪些接口我们可以继承,他们都提供了什么功能。 核心接口和类Spring data JPA Repository接口体系如下: Repository 顶层标记接口,通过泛型定义了其管理的Entity和Entity对应的主键类型。同时,也会从此接口开始扫描继承它的Repository接口,并创建代理Bean: public interface Repository<T, ID extends Serializable> { }1.2. CrudRepository 定义了通用的CRUD方法,一般而言,增删改查业务逻辑直接继承该接口即可。 public interface CrudRepository<T, ID extends Serializable> extends Repository<T, ID> { // 保存实体,返回保存后的实体 <S extends T> S save(S entity); // 保存所有给定的实体,如果参数为null,则抛出IllegalArgumentException。返回保存后的全部实体 <S extends T> Iterable<S> save(Iterable<S> entities); // 按照给定id查询实体,如果id为null,则抛出IllegalArgumentException,找到则返回带id的实体,否则返回null T findOne(ID id); // 判断给定id的实体是否存在,id不能为null,否则抛出IllegalArgumentException,如果存在则返回true,否则返回false boolean exists(ID id); // 查询所有实体对象 Iterable<T> findAll(); // 返回给定id列表的所有实体 Iterable<T> findAll(Iterable<ID> ids); // 返回可用实体的数量 long count(); // 删除给定id的实体,id不能为null void delete(ID id); // 删除给定的实体,实体不能为null void delete(T entity); // 删除给定的所有实体,参数不能为null void delete(Iterable<? extends T> entities); // 删除所有实体 void deleteAll(); }1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.20.21.22.23.24.25.26.27.28.29.30.31.32.33.34. PagingAndSortingRepository 在CurdRepository的基础上增加了分页查询和排序方法: public interface PagingAndSortingRepository<T, ID extends Serializable> extends CrudRepository<T, ID> { // 查询所有实体,并按照给定规则排序 Iterable<T> findAll(Sort sort); // 分页查询,返回分页对象 Page<T> findAll(Pageable pageable); }1.2.3.4.5.6.7. 关于分页查询后边再细说。 JpaRepository 继承了PagingAndSortingRepository和QueryByExampleExecutor接口,扩展和重写了部分方法,同时支持QBE查询。 public interface JpaRepository<T, ID extends Serializable> extends PagingAndSortingRepository<T, ID>, QueryByExampleExecutor<T> { // 查询所有对象列表 List<T> findAll(); // 查询所有对象列表并进行排序 List<T> findAll(Sort sort); // 查询给定一批id的实体 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(); // 获取单个实体,如果实体不存在,抛出EntityNotFoundException异常 T getOne(ID id); // 查询匹配给定example的所有实体 <S extends T> List<S> findAll(Example<S> example); // 查询匹配给定example的所有实体并按给定规则排序 <S extends T> List<S> findAll(Example<S> example, Sort sort); }1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.20.21.22.23.24.25.26.27.28.29.30.31.32.33.34.35. 更多关于QBE查询的信息可以看这里。 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); }1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16. 可以看到,每个方法都需要传递一个 public interface Specification<T> { // 使用给定的Root和CriteriaQuery对象构建一个Predicate,用于拼接where语句 Predicate toPredicate(Root<T> root, CriteriaQuery<?> query, CriteriaBuilder cb); }1.2.3.4.
核心的几个接口介绍完了,接下来我们看看一些用法。 查询创建前边提到,Spring Data JPA能够根据定义的方法名称自动生成查询实现,同时,我们也可以使用 根据方法名自动查询通过方法名命名规则,Spring Data JPA能够自动解析方法名称并生成实现,其查询构建机制如下: 将前缀 下边是一些方法名称定义的例子: interface PersonRepository extends Repository<User, Long> { // 根据Email地址和Lastname查询Person列表 List<Person> findByEmailAddressAndLastname(EmailAddress emailAddress, String lastname); // 根据Lastname或者Firstname查询Person列表,并对结果去重 List<Person> findDistinctPeopleByLastnameOrFirstname(String lastname, String firstname); List<Person> findPeopleDistinctByLastnameOrFirstname(String lastname, String firstname); // 根据Lastname查询Person,不区分大小写 List<Person> findByLastnameIgnoreCase(String lastname); // 根据Lastname和Firstname查询Person列表,都忽略大小写 List<Person> findByLastnameAndFirstnameAllIgnoreCase(String lastname, String firstname); // 根据Lastname查询Person列表,结果按照Firstname排序 List<Person> findByLastnameOrderByFirstnameAsc(String lastname); List<Person> findByLastnameOrderByFirstnameDesc(String lastname); }1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17. 方法名定义说明如下:
使用@Query定义查询自动查询虽然为我们带来的极大的便利,但是某些业务场景下仍不能满足需求,例如:连表查询。此时,我们可以手动定义查询。Spring Data JPA提供了 @Query("select d from Employee e, Department d where e.departmentId = d.id and e.id = :employeeId") Department findDepartmentById(@Param("employeeId") Long employeeId);1.2. 查询参数绑定 上边的例子使用了 @Query("select d from Employee e, Department d where e.departmentId = d.id and e.id = ?1") Department findDepartmentById(Long employeeId);1.2. 原生SQL
@Query(value = "SELECT * FROM USERS WHERE EMAIL_ADDRESS = ?1", nativeQuery = true) User findByEmailAddress(String emailAddress);1.2. 需要注意的时,使用 使用已命名的查询除了使用 定义命名的Query: @Entity @Table(name = "employee") @Data @EqualsAndHashCode @NamedQueries({ @NamedQuery(name = "Employee.findByDeptId", query = "select e from Employee e where e.departmentId = ?1"), @NamedQuery(name = "Employee.findByGender1", query = "select e from Employee e where e.gender = ?1"), }) public class Employee { …… }1.2.3.4.5.6.7.8.9.10.11. 通过 定义对应的方法: List<Employee> findByDeptId(Long deptId); List<Employee> findByGender1(Gender gender);1.2. Query查找策略现在,我们有了三种方法来定义Query了:通过方法名自动创建Query,通过 通过配置
一般情况下使用默认配置即可,如果确定项目Query的具体定义方式,可以更改上述配置,例如全部使用 简单查询接下来,我们看看Spring Data JPA所支持的一些简单查询用法。 特定参数和返回除了前边介绍的返回List,还可以返回单个实体,使用 public interface EmployeeDao extends BaseDao<Employee> { List<Employee> findByNameLike(String name); // 按照名称模糊查询,返回第一个员工,结果按ID升序排列 Employee findTopByNameLikeOrderByIdAsc(String name); // 按名称进行分页查询,结果按ID升序排列 Page<Employee> findByNameLikeOrderByIdAsc(String name, Pageable pageable); // 分页查询大于等于某一年龄的员工,结果ID升序排列 Slice<Employee> findByAgeGreaterThanEqualOrderByIdAsc(int age, Pageable pageable); }1.2.3.4.5.6.7.8.9.10.11.12. 分页查询后边再细说。 查询结果数量限制可以使用first、top关键字来限定查询结果的数量,如果不设定,则返回一条数据,否则返回给定数量的条数,例如: List<Employee> findTop3ByNameLikeOrderByIdAsc(String name); Employee findTopByNameLikeOrderByIdAsc(String name); Employee findFirstByNameLikeOrderByIdAsc(String name);1.2.3. 同样支持使用 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);1.2.3.4. 使用Stream来处理结果集支持使用Java8提供的Stream类来接收查询结果集: @Query("select e from Employee e where e.name like ?1") Stream<Employee> findByCustomQueryAndStream(String name);1.2. 前边的Pageable、Sort参数同样试适用,要注意的是,Stream用完后必须关闭流,可以调用close或使用try-with-resources语句块: @Transactional public List<Employee> queryByNameFilterWithAge(String name, int minAge) { try (Stream<Employee> employeeStream = employeeDao.findByCustomQueryAndStream(name);) { return employeeStream.filter(employee -> employee.getAge() >= minAge).collect(Collectors.toList()); } }1.2.3.4.5.6. 测试过程中发现,使用Stream时,必须保证处于事务控制范围,否则会出现
意思是为了保持Stream连接打开,必须保证消费Stream的代码处于事务控制。 异步查询通过使用Spring的异步方法执行功能,可以异步运行查询。这意味着方法在调用时立即返回,而实际的查询执行发生在已提交给Spring TaskExecutor的任务中。 // 使用java.util.concurrent.Future作为返回类型 @Async Future<User> findByFirstname(String firstname); // 使用java8的java.util.concurrent.CompletableFuture作为返回类型 @Async CompletableFuture<User> findOneByFirstname(String firstname); // 使用org.springframework.util.concurrent.ListenableFuture作为返回类型 @Async ListenableFuture<User> findOneByLastname(String lastname); 1.2.3.4.5.6.7.8.9. 通过 用于修改的查询当需要使用sql来进行数据库update和delete操作时,Spring Data JPA也支持: @Modifying @Query("update Employee e set e.gender = com.belonk.entity.Gender.MALE where e.gender = com.belonk.entity.Gender.FEMALE") int reverseGenderOfFemale(); @Modifying @Query("delete from Employee e where e.departmentId = ?1") void deleteInBulkByDeptId(Long deptId);1.2.3.4.5.6.7. 使用 条件分页查询前边已经介绍了分页查询的几个对象 查询定义如下: Page<Employee> findByNameLikeOrderByIdAsc(String name, Pageable pageable); Slice<Employee> findByAgeGreaterThanEqualOrderByIdAsc(int age, Pageable pageable);1.2.3. 条件分页查询的方法使用的是 Page<T> findAll(Specification<T> spec, Pageable pageable);1. 具体实现: public Page<Employee> pageQueryByName(int pageIndex, int pageSize, String name) { // pageIndex从0开始 Pageable pageable = new PageRequest(pageIndex, pageSize); return employeeDao.findByNameLikeOrderByIdAsc(name, pageable); } public Slice<Employee> pageQueryByAge(int pageIndex, int pageSize, int minAge) { Pageable pageable = new PageRequest(pageIndex, pageSize); return employeeDao.findByAgeGreaterThanEqualOrderByIdAsc(minAge, pageable); } public Page<Employee> pageQueryByNameAndAage(int pageIndex, int pageSize, String name, int minAge) { // 创建查询条件规则 Specification<Employee> specification = (root, cq, cb) -> { List<Predicate> predicates = new ArrayList<>(); if (StringUtils.hasLength(name)) { predicates.add(cb.and(cb.like(root.get("name"), name))); } if (minAge > 0) { predicates.add(cb.and(cb.greaterThanOrEqualTo(root.get("age"), minAge))); } if (predicates.size() > 0) { cq.where(predicates.toArray(new Predicate[predicates.size()])); } return cq.getRestriction(); }; // 创建排序规则 Sort sort = new Sort(Sort.Direction.DESC, "id"); // 创建分页对象 Pageable pageable = new PageRequest(pageIndex, pageSize, sort); return employeeDao.findAll(specification, pageable); }1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.20.21.22.23.24.25.26.27.28.29.30.31.32. 前边说过, Page和Slice的区别 这里先说一下
查询投影很多时候,我们并不需要实体的全部字段,而是其中一部分,投影就是用来解决这个问题。Spring Data JPA有三种投影方式。 基于接口的投影将需要查询的字段定义到接口中,接口方法与属性名称必须对应: public interface MyEmployee { // 属性必须与entity对应 Long getId(); String getName(); Integer getAge(); Long getDepartmentId(); String getDepartmentName(); }1.2.3.4.5.6.7.8.9.10.11.12.13. 查询定义: @Query("select e.id as id, e.name as name, e.age as age, d.id as departmentId, d.name as departmentName from Employee e, Department d where e.departmentId = d.id and e.id = ?1") MyEmployee findByIdWithDepartment(Long id);1.2. 接口可以进行嵌套投影: interface PersonSummary { String getFirstname(); String getLastname(); AddressSummary getAddress(); interface AddressSummary { String getCity(); } }1.2.3.4.5.6.7.8.9. Spring Data JPA能够对基于接口的投影进行查询优化。 自定义投影类也可以自定义数据传输对象(DTOS),这些DTO类型可以以使用投影接口的完全相同的方式使用,但是不会生成代理,也不能应用嵌套投影。
@Data @EqualsAndHashCode @ToString public class UserConstructWithField { private Long id; private String name; public UserConstructWithField(Long id, String name) { this.id = id; this.name = name; }1.2.3.4.5.6.7.8.9.10.11.
@Data @EqualsAndHashCode public class MyEmployeeDTO { private Long id; private String name; private Long deptId; private String deptName; public MyEmployeeDTO(Long id, String name, Long deptId, String deptName) { this.id = id; this.name = name; this.deptId = deptId; this.deptName = deptName; } }1.2.3.4.5.6.7.8.9.10.11.12.13.14.15. 查询定义: // 直接返回投影对象 UserConstructWithField findById(Long id); // 直接返回投影对象列表 List<UserConstructWithField> findByNameIsLike(String name); // 使用自定义查询,不能直接转换DTO,JPQL需要new一个对象 @Query("select new com.belonk.domain.MyEmployeeDTO(e.id, e.name, d.id, d.name) from Employee e, Department d where e.departmentId = d.id and e.id = ?1") MyEmployeeDTO findByIdWithDepartment2(Long id);1.2.3.4.5.6.7.8.9. 在实际的测试过程中,使用方法名自东创建查询,可以直接返回投影对象,但是使用 推荐使用DTO属性来进行构造,这样Spring Data JPA能够对SQL进行优化,只查询构造器对应的字段。 动态投影动态投影用于动态定义投影结果DTO对象,在进行自动创建查询投影时非常方便。 再定义一个DTO: @Data @EqualsAndHashCode @ToString public class UserConstructWithField1 { private String name; private Integer age; public UserConstructWithField1(String name, Integer age) { this.name = name; this.age = age; } }1.2.3.4.5.6.7.8.9.10.11.12. 查询接口定义: <T> List<T> findByAgeGreaterThan(int age, Class<T> tClass);1. 动态传递DTO的 public List<UserConstructWithField> queryByAgeGreaterThan(int minAage) { return employeeDao.findByAgeGreaterThan(minAage, UserConstructWithField.class); } public List<UserConstructWithField1> queryByAgeGreaterThan1(int minAage) { return employeeDao.findByAgeGreaterThan(minAage, UserConstructWithField1.class); };1.2.3.4.5.6.7. 附录下表列出了Spring Data存储库方法名支持的关键字,数据库不同,关键字的支持也会不同:
|
|