红茶的个人站点

  • 首页
  • 专栏
  • 开发工具
  • 其它
  • 隐私政策
Awalon
Talk is cheap,show me the code.
  1. 首页
  2. 专栏
  3. Spring Boot 学习笔记
  4. 正文

JPA 学习笔记 5:Spring Data JPA

2025年10月8日 28点热度 0人点赞 0条评论

Spring Data JPA 是 Spring 框架对 JPA 的整合,可以在 Spring 中使用 JPA 操作数据。

快速开始

创建一个 Spring Boot 项目并整合 Spring Data JPA,具体可以参考这里。

创建实体类:

@Entity
@Table(name = "tb_person")
@Data
@ToString
public class Person {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    @Column(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接口,还可以使用注解:

@RepositoryDefinition(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);
}

这些接口提供更丰富的功能。继承关系如下:

image-20251007191022727

只要定义在存储库(接口)中的方法名满足一定规范,就会被 Spring 正确解析和处理,比如:

// where lastName like ?'%' and birth<= ?
Person findByLastNameStartsWithAndBirthBefore(String lastName, LocalDate birth);

在处理简单查询时,这样做很方便,但缺点在于对于复杂查询,方法名会很长很复杂,可读性差,且修改方法名会导致一系列问题,所以并不推荐广泛使用这种方式。

@Query

使用@Query注解可以实现自定义 JPQL 实现的查询:

@Query("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 可以正确关闭,防止内存泄露。

本文的完整示例代码可以从这里获取。

参考资料

  • 仓库查询关键字 :: Spring Data JPA --- Repository query keywords :: Spring Data JPA

  • 尚硅谷SpringData教程

本作品采用 知识共享署名 4.0 国际许可协议 进行许可
标签: jpa spring
最后更新:2025年10月8日

魔芋红茶

加一点PHP,加一点Go,加一点Python......

点赞
< 上一篇
下一篇 >

文章评论

razz evil exclaim smile redface biggrin eek confused idea lol mad twisted rolleyes wink cool arrow neutral cry mrgreen drooling persevering
取消回复

COPYRIGHT © 2021 icexmoon.cn. ALL RIGHTS RESERVED.
本网站由提供CDN加速/云存储服务

Theme Kratos Made By Seaton Jiang

宁ICP备2021001508号

宁公网安备64040202000141号