Spring Data JPA 是 Spring 框架对 JPA 的整合,可以在 Spring 中使用 JPA 操作数据。
快速开始
。
创建实体类:
name = "tb_person")
(
public class Person {
strategy = GenerationType.IDENTITY)
( private Long id;
nullable = false, unique = true, length = 10)
( private String lastName;
private String email;
private LocalDate birth;
}
创建存储库:
public interface PersonRepository extends Repository<Person, Long> {
Person findByLastName(String lastName);
}
测试用例:
Person person = personRepository.findByLastName("张三丰");
System.out.println(person);
Repository
public interface PersonRepository extends Repository<Person, Long> {
Person findByLastName(String lastName);
}
Repository
实际上是一个空接口,继承了该接口的接口会被认为是 JPA 的存储库定义,Spring 会自动扫描并为这些接口创建代理对象添加到 IOC 容器中,这样我们就可以直接在 Spring 中通过这些对象操作数据库。
除了直接继承Repository
接口,还可以使用注解:
domainClass = Person.class, idClass = Long.class)
(public interface PersonRepository{
Person findByLastName(String lastName);
}
效果是相同的。
除了直接继承Repository
,还可以继承它的子接口:
public interface PersonRepository extends JpaRepository<Person, Long> {
Person findByLastName(String lastName);
}
这些接口提供更丰富的功能。继承关系如下:
只要定义在存储库(接口)中的方法名满足一定,就会被 Spring 正确解析和处理,比如:
// where lastName like ?'%' and birth<= ?
Person findByLastNameStartsWithAndBirthBefore(String lastName, LocalDate birth);
在处理简单查询时,这样做很方便,但缺点在于对于复杂查询,方法名会很长很复杂,可读性差,且修改方法名会导致一系列问题,所以并不推荐广泛使用这种方式。
@Query
使用@Query
注解可以实现自定义 JPQL 实现的查询:
"select p from Person p where p.id=(select max(p2.id) from Person p2)")
(Person getMaxIdPerson();
上面这种使用自语句的嵌套查询是无法用方法名命名规则的方式实现的。
如果 JPQL 需要使用参数,可以使用占位符的方式:
@Query("select p from Person p where p.lastName=?1 and p.email=?2")
List<Person> queryTest(String lastName, String email);
也可以使用具名参数:
@Query("select p from Person p where p.lastName=:lastName and p.email=:email")
List<Person> queryTest2(@Param("email") String email, @Param("lastName") String lastName);
和 MyBatis 类似,这里需要使用@Param
注解标记参数名,这是为了避免低版本 JDK 编译后形参名称被擦除,如果高版本的 JDK 并开启-params
编译参数,@Param
注解就不是必须的。
JPQL 中可以使用比较通配符:
@Query("select p from Person p where p.lastName like %:lastName% and p.email like %:email%")
List<Person> queryTest3(@Param("email") String email, @Param("lastName") String lastName);
使用 @Query 注解也可以实现原生 SQL 的查询:
@Query(value="select count(*) from tb_person", nativeQuery = true)
int getTotalCount();
@Modifying
一般 @Query 只能用于 SELECT 的 JPQL,如果是更新或删除,需要使用@Modifying
注解:
@Modifying
@Query("update Person p set p.email=:email where p.id=:id")
void updateEmailById(String email, Long id);
如果直接用测试用例调用,会产生异常:
Assertions.assertThrowsExactly(InvalidDataAccessApiUsageException.class, () -> {
personRepository.updateEmailById("zsf@163.com", 1L);
});
因为更新和删除的操作需要在事务中执行。
SELECT 的 JPQL 默认有一个只读事务。
JPQL 不能执行添加操作。
定义一个 Service,使用存储库方法并包含在事务中:
@Service
public class PersonService {
@Autowired
private PersonRepository personRepository;
@Transactional
public void UpdateEmailById(String email, Long id) {
personRepository.updateEmailById(email, id);
}
}
测试:
personService.UpdateEmailById("zsf@tom.com", 1L);
用 JPQL 实现删除逻辑:
@Modifying
@Query("delete from Person p where p.id=:id")
void deleteById(@NonNull @Param("id") Long id);
测试:
personRepository.deleteById(1L);
CrudRepository
可以让存储库接口继承CrudRepository
以提供一些基本的增删改查操作:
public interface PersonRepository extends CrudRepository<Person, Long> {
比如实现批量添加:
List<Person> persons = new ArrayList<>();
for (int i = 0; i < 10; i++) {
Person person = new Person();
person.setLastName("张三丰" + i);
person.setEmail("zsf" + i + "@qq.com");
person.setBirth(LocalDate.of(1995, 10, 7));
persons.add(person);
}
personRepository.saveAll(persons);
PagingAndSortingRepository
PagingAndSortingRepository
接口提供分页和排序相关的方法:
public interface PersonRepository extends CrudRepository<Person, Long>, PagingAndSortingRepository<Person, Long> {
示例:
final int CURRENT_PAGE = 3;
PageRequest pageable = PageRequest.of(CURRENT_PAGE - 1, 5);
Page<Person> page = personRepository.findAll(pageable);
System.out.println("总行数:" + page.getTotalElements());
System.out.println("总页数:" + page.getTotalPages());
System.out.println("当前页数据:" + page.getContent());
System.out.println("当前页:" + (page.getNumber() + 1));
System.out.println("每页大小:" + page.getSize());
System.out.println("是否有下一页:" + page.hasNext());
System.out.println("是否有上一页:" + page.hasPrevious());
System.out.println("是否第一页:" + page.isFirst());
需要注意的是,与一般的分页 API 有所不同,这个分页接口提供的方法中页码是从 0 开始的,所以需要在与一般从 1 开始的页码之间进行转换,在必要的地方 +1 或 -1。
分页时可以指定排序规则:
final int CURRENT_PAGE = 2;
final int PAGE_SIZE = 5;
Sort sort = Sort.by(Sort.Direction.DESC, "id");
PageRequest pageable = PageRequest.of(CURRENT_PAGE - 1, PAGE_SIZE, sort);
如果有多个字段参与排序:
final int CURRENT_PAGE = 2;
final int PAGE_SIZE = 5;
Sort.Order order1 = Sort.Order.desc("id");
Sort.Order order2 = Sort.Order.asc("lastName");
Sort sort = Sort.by(List.of(order1, order2));
PageRequest pageable = PageRequest.of(CURRENT_PAGE - 1, PAGE_SIZE, sort);
这里指定先按 ID 降序排列,如果 ID 相同,按照lastName
升序排列。
JpaRepository
JpaRepository
同时继承了PagingAndSortingRepository
和CrudRepository
:
public interface PersonRepository extends JpaRepository<Person, Long> {
示例:
Person person = new Person();
person.setLastName("Tom");
person.setEmail("tom@qq.com");
person.setBirth(LocalDate.of(1995, 10, 7));
personRepository.saveAndFlush(person);
如果是临时实体对象(没有 ID),saveAndFlush
会执行 INSERT 语句保存。
Person person = new Person();
person.setId(13L);
person.setLastName("Jack");
person.setEmail("tom@qq.com");
person.setBirth(LocalDate.of(1995, 10, 7));
Person personNew = personRepository.saveAndFlush(person);
Assertions.assertNotSame(person, personNew);
如果是游离实体对象,saveAndFlush
会先执行 SELECT 从数据库获取数据更新 EntityManager 缓存对象,再用游离对象的属性赋值给缓存对象,最后用缓存对象执行 INSERT 语句更新数据库。
saveAndFlush
方法的行为类似于EntityManager.merge
方法。
JpaSpecificationExecutor
更常见的是带条件的分页查询,使用JpaSpecificationExecutor
接口可以实现这一点:
public interface PersonRepository extends JpaRepository<Person, Long>, JpaSpecificationExecutor<Person> {
示例:
final int PAGE_NUM = 1;
final int PAGE_SIZE = 5;
final String lastName = "张";
Specification<Person> specification = new Specification<Person>() {
@Override
public Predicate toPredicate(Root<Person> root, CriteriaQuery<?> query, CriteriaBuilder criteriaBuilder) {
return criteriaBuilder.like(root.get("lastName"), "%"+lastName+"%");
}
};
Page<Person> page = personRepository.findAll(specification
, PageRequest.of(PAGE_NUM - 1, PAGE_SIZE));
自定义存储库方法实现
县创建一个自定义接口:
public interface PersonRepositoryExtend {
Page<Person> pageAndSearch(int pageNum, int pageSize, String lastName);
}
让存储库接口继承这个接口:
public interface PersonRepository
extends JpaRepository<Person, Long>, JpaSpecificationExecutor<Person>, PersonRepositoryExtend {
实现自定义接口:
public class PersonRepositoryExtendImpl implements PersonRepositoryExtend {
@PersistenceContext
private EntityManager entityManager;
@Override
@Transactional
public Page<Person> pageAndSearch(int pageNum, int pageSize, String lastName) {
TypedQuery<Person> query = entityManager.createQuery("select p from Person p where p.lastName like :lastName", Person.class);
query.setParameter("lastName", "%"+lastName+"%");
query.setFirstResult((pageNum - 1) * pageSize);
query.setMaxResults(pageSize);
return new PageImpl<>(query.getResultList(), PageRequest.of(pageNum - 1, pageSize), searchTotal(lastName));
}
private long searchTotal(String lastName) {
TypedQuery<Long> query = entityManager.createQuery("select count(p) from Person p where p.lastName like :lastName", Long.class);
query.setParameter("lastName", "%"+lastName+"%");
return query.getSingleResult();
}
}
这里使用 EntityManager
执行 JPQL 的方式实现具体的数据查询并将结果返回。这里使用@PersistenceContex
注解注入EntityManager
依赖,虽然也可以使用@Autowired
注解注入,但前者是 JPA 的标准注解,从规范上确保框架和容器要提供安全注入以及确保 EntityManager
可以正确关闭,防止内存泄露。
本文的完整示例代码可以从获取。
文章评论