Spring Jpa Data 快速入门指南

1. 初识Spring Jpa Data

1.1 环境准备

创建maven工程,项目继承spring-boot-starter-parent,只需要引入spring-boot-starter-data-jpa即可:

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
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.6.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>

<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>

<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
</dependencies>

1.2 数据库连接配置

application.yml配置文件中,配置数据库源信息和jpa基础配置:

1
2
3
4
5
6
7
8
9
10
11
spring:
datasource:
username: root
password: root
url: jdbc:mysql:///jpa-study
driver-class-name: com.mysql.cj.jdbc.Driver
jpa:
database: mysql
show-sql: true
hibernate:
ddl-auto: update

1.3 数据库连接测试

首先在数据库中创建jpa-study数据库:

1
CREATE DATABASE `jpa-study` /*!40100 COLLATE 'utf8mb4_unicode_ci' */

编写数据连接测试方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
@RunWith(SpringRunner.class)
@SpringBootTest
public class SpringJpaDataStudyApplicationTests {

@Autowired
private DataSource dataSource;

@Test
public void testDataSource() {
System.out.println("dataSource->" + dataSource);
}

}

启动运行,不报异常表示数据库连接正常。

1.4 创建表对象

编写Person类,这个类对应数据库表为:JPA_PERSONS

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.Table;
import java.io.Serializable;
import java.util.Date;

@Table(name = "JPA_PERSONS")
@Entity
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Person implements Serializable {
@Id
@GeneratedValue
private Integer id;
private String lastName;
private String email;
private Date birth;
}

再次运行测试类,此时数据库创建了JPA_PERSONS表,使用heidisql工具查看数据库:

注意:在主键属性上加了@GeneratedValue注解,则会在创建表的时候生成一张hibernate_sequence表。

1.5 编写dao接口

编写Person对象的数据库操作接口,并继承Repository<T, ID>,其中需要指定当前数据对象的类型和主键类型,并在接口中编写save()方法:

1
2
3
4
5
6
import org.springframework.data.repository.Repository;
import org.woodwhales.springjpadatastudy.entity.Person;

public interface PersonDao extends Repository<Person, Integer> {
void save(Person person);
}

此时PersonDao接口已经具备了保存数据的能力了。

注意:经过测试,这个自定义的save()方法如果写了返回类型是Long、Integer或boolean,那么在数据执行完毕之后,会抛出异常java.lang.ClassCastException,但是数据会保存到数据库中,所以要注意异常情况增加事务。

编写测试方法测试:

1
2
3
4
5
6
7
8
@Autowired
private PersonDao personDao;

@Test
public void testSave() {
Person person = new Person(1, "woodwhales", "[email protected]", new Date());
personDao.save(person);
}

从执行日志可以看出,数据已经成功保存到数据库:

1
2
Hibernate: select person0_.id as id1_0_0_, person0_.birth as birth2_0_0_, person0_.email as email3_0_0_, person0_.last_name as last_nam4_0_0_ from jpa_persons person0_ where person0_.id=?
Hibernate: insert into jpa_persons (birth, email, last_name, id) values (?, ?, ?, ?)

再到数据库中直接检查一下:

再次在接口中编写根据id查询数据方法:

1
Person getById(Integer id);

测试:

1
2
3
4
5
@Test
public void testGetById() {
Person person = personDao.getById(2);
System.out.println("person -> " + person);
}

执行日志可以看到,顺利查询出数据:

1
2
Hibernate: select person0_.id as id1_0_, person0_.birth as birth2_0_, person0_.email as email3_0_, person0_.last_name as last_nam4_0_ from jpa_persons person0_ where person0_.id=?
person -> Person(id=2, lastName=woodwhales, email=woodwhales@163.com, birth=2019-08-02 14:17:17.0)

2. 认识Repository接口

2.1 Repository是标记接口

点击Repository接口源码可以看到,这个接口就是一个空接口,从上述测试的结果可以得出:

  • Repository 接口是一个空接口,也就是一个标记接口。
  • 如果程序员自己定义的接口继承了这个 Repository 接口,那么这个自定义接口也会被加载到 spring ioc 容器中(因为使用@Autowired 拿到了这个接口,并成功执行了测试方法,所以一定被纳进了容器中),并且编写一定规则的方法,都是可以执行相应的数据库操作。
  • 实际上, 也可以通过@RepositoryDefinition注解来替代继承 Repository 接口

形如:

1
2
3
4
5
6
import org.springframework.data.repository.RepositoryDefinition;

@RepositoryDefinition(domainClass=Person.class,idClass = Integer.class)
public interface PersonDao {

}

2.2 Repository接口的实现类/子接口

在 IDEA 编辑器中,按快捷键ctrl+H查看 Repository 接口的实现类:

从继承关系图中可以看出:

基础的 Repository 提供了最基本的数据访问功能,其几个子接口则扩展了一些功能。它们的继承关系如下:

  • Repository: 仅仅是一个标识,表明任何继承它的均为仓库接口类

  • CrudRepository: 继承 Repository,实现了一组 CRUD 相关的方法

  • PagingAndSortingRepository: 继承 CrudRepository,实现了一组分页排序相关的方法

  • JpaRepository: 继承 PagingAndSortingRepository,实现一组 JPA 规范相关的方法

  • 自定义的 XxxxRepository 需要继承 JpaRepository,这样的 XxxxRepository 接口就具备了通用的数据访问控制层的能力。

  • JpaSpecificationExecutor: 不属于Repository体系,实现一组 JPA Criteria 查询相关的方法,用于条件查询。

2.3 方法定义规则

在 Repository 子接口中声明方法,不是随便声明的,而需要符合一定的规范:

  • 查询方法以 find | read | get 开头。

  • 涉及条件查询时,条件的属性用条件关键字连接。

  • 要注意的是:条件属性以首字母大写。

  • 支持属性的级联查询,如果当前类有符合条件的属性,则优先使用这个属性,而不使用级联属性。

    如果想要使用级联查询,需要在属性之间使用下划线分割

直接在接口中定义查询方法,如果是符合规范的,可以不用写实现,目前支持的关键字写法如下:

Keyword Sample JPQL snippet
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.firstname like ?1(parameter bound with appended %)
EndingWith findByFirstnameEndingWith … where x.firstname like ?1(parameter bound with prepended %)
Containing findByFirstnameContaining … where x.firstname like ?1(parameter bound wrapped in %)
OrderBy findByAgeOrderByLastnameDesc … where x.age = ?1 order by x.lastname desc
Not findByLastnameNot … where x.lastname <> ?1
In findByAgeIn(Collection ages) … where x.age in ?1
NotIn findByAgeNotIn(Collection ages) … where x.age not in ?1
True findByActiveTrue() … where x.active = true
False findByActiveFalse() … where x.active = false
IgnoreCase findByFirstnameIgnoreCase … where UPPER(x.firstame) = UPPER(?1)

官方文档地址:https://docs.spring.io/spring-data/jpa/docs/current/reference/html/#jpa.query-methods.query-creation

定义规则举例

查询 id 小于 xx 并且 lastName 以 xx 开头的所有数据:

使用 JPQL 表达式表示就是:

1
WHERE lastName LIKE ?% AND id < ?

方法命名规则:

1
List<Person> getByLastNameStartingWithAndIdLessThan(String lastName, Integer id);

依次类推,查询 id 小于 xx 并且 lastName 以 xx 结尾的所有数据:

1
2
//WHERE lastName LIKE %? AND id < ?
List<Person> getByLastNameEndingWithAndIdLessThan(String lastName, Integer id);

类似的写法:

1
2
//WHERE email IN (?, ?, ?) OR birth < ?
List<Person> getByEmailInOrBirthLessThan(List<String> emails, Date birth);

级联属性规则说明

比如 Person 对象属性中有个 Address 对象属性,一个 Person 对应多个 Address 对象,如果想要级联查询,查询地址的 id 大于某某的数据,则可以写成:

1
2
//WHERE address.id > ?
List<Person> getByAddressIdGreaterThan(Integer id);

但是这里有个潜在的问题,当前 Person 对象有个叫 address 属性名的字段,而没有属性叫 addressId 的属性字段,那么查询的时候,就是先查 address 再级联到 address 的 id 的数据。

如果当前 Person 对象有个叫 addressId 的属性字段,那么在执行查询的时候,就是直接查了 addressId,而不是级联查询了。

因此为了防止歧义,对于级联的属性条件,中间需要加下划线区别:

1
List<Person> getByAddress_IdGreaterThan(Integer id);

说明:其实级联查询即使使用了下划线,也很难以阅读理解,所以建议复杂的查询全部使用原生的sql。

3. @Query注解

通过@Query注解,注解在方法上,编写自定义原生的sql或者JPQL语句,这里还是建议写原生的sql语句,因为在数据库中查询再拷贝到注解上,不需要改动很大,调试起来也很方便。

以下是JPQL语句的查询:

1
2
@Query("SELECT p FROM Person p WHERE p.id = (SELECT max(p2.id) FROM Person p2)")
Person getMaxIdPerson();

3.1 占位符

索引参数如下所示,索引值从1开始,查询中 ”?X” 个数需要与方法定义的参数个数相一致,并且顺序也要一致:

1
2
@Query("SELECT p FROM Person p WHERE p.lastName = ?1 AND p.email = ?2")
List<Person> testQueryAnnotationParams1(String lastName, String email);

命名参数(推荐使用这种方式):可以定义好参数名,赋值时采用@Param注解,而不用管顺序:

1
2
@Query("SELECT p FROM Person p WHERE p.lastName = :lastName AND p.email = :email")
List<Person> testQueryAnnotationParams2(@Param("email") String email, @Param("lastName") String lastName);

SpringData 允许在占位符上添加 %%

1
2
3
4
5
6
7
//SpringData 允许在占位符上添加 %%. 
@Query("SELECT p FROM Person p WHERE p.lastName LIKE %?1% OR p.email LIKE %?2%")
List<Person> testQueryAnnotationLikeParam(String lastName, String email);

//SpringData 允许在占位符上添加 %%.
@Query("SELECT p FROM Person p WHERE p.lastName LIKE %:lastName% OR p.email LIKE %:email%")
List<Person> testQueryAnnotationLikeParam2(@Param("email") String email, @Param("lastName") String lastName);

3.2 支持原生SQL

设置nativeQuery=true即可以使用原生的 SQL 查询:

1
2
@Query(value="SELECT count(id) FROM jpa_persons", nativeQuery=true)
long getTotalCount();

4. @Modifying注解

@Query@Modifying这两个注解一起声明,可定义个性化更新操作,例如只涉及某些字段更新时最为常用,示例如下:

1
2
3
@Modifying
@Query("UPDATE Person p SET p.email = :email WHERE id = :id")
void updatePersonEmail(@Param("id") Integer id, @Param("email") String email);

注意:

  • 方法的返回值应该是 int,表示更新语句所影响的行数

  • 在调用的地方必须加事务,没有事务不能正常执行

Spring Data 提供了默认的事务处理方式,即所有的查询均声明为只读事务。对于自定义的方法,如需改变
Spring Data 提供的事务默认方式,可以在方法上注解@Transactional声明。

因此,对于update和delete操作,需要添加@Modifying注解,service 调用这些方法的时候需要申明事务。

5. CrudRepository接口

CrudRepository 接口提供了最基本的对实体类的添删改查操作,IDEA 编辑器中按快捷键alt+7显示 CrudRepository 接口的所有方法:

6. PagingAndSortingRepository接口

PagingAndSortingRepository 接口该接口提供了分页与排序功能,开发中常用的是第二个方法:

1
2
3
4
5
public interface PagingAndSortingRepository<T, ID> extends CrudRepository<T, ID> {
Iterable<T> findAll(Sort sort);

Page<T> findAll(Pageable pageable);
}

Pageable 接口的实现类为 PageRequest,在构造函数中传入页码和页数,注意的是默认页码是从 0 页开始。一般在开发中,前端传参过来的页码是从第 1 页开始的,所以创建 PageRequest 对象的时候注意减1。

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
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.data.domain.Sort.Order;

@Test
public void testPagingAndSortingRespository(){
//pageNo 从 0 开始.
int pageNo = 6;
int pageSize = 5;
//Pageable 接口通常使用的其 PageRequest 实现类. 其中封装了需要分页的信息
//排序相关的. Sort 封装了排序的信息
//Order 是具体针对于某一个属性进行升序还是降序.
Order order1 = new Order(Direction.DESC, "id");
Order order2 = new Order(Direction.ASC, "email");
Sort sort = new Sort(order1, order2);

PageRequest pageable = new PageRequest(pageNo - 1, pageSize, sort);
Page<Person> page = personRepsotory.findAll(pageable);

System.out.println("总记录数: " + page.getTotalElements());
System.out.println("当前第几页: " + (page.getNumber() + 1));
System.out.println("总页数: " + page.getTotalPages());
System.out.println("当前页面的 List: " + page.getContent());
System.out.println("当前页面的记录数: " + page.getNumberOfElements());
}

Order 排序对象,里面传参是数据库表中的列名。

7. JpaRepository接口

在实际开发中,自定义的接口一般继承的就是这个,而不是 Repository 这个空接口。

其中saveAndFlush()表示强制执行持久化操作,返回的对象和传参对象不是同一个对象。

1
2
3
4
5
6
7
@Test
public void testJpaRepository(){
Person person = new Person(3, "woodwhales", "[email protected]", new Date());
Person person2 = personRepsotory.saveAndFlush(person);

System.out.println(person == person2);
}

返回结果是false,表示执行之后,创建了新的对象。

8. JpaSpecificationExecutor接口

JpaSpecificationExecutor 接口不属于Repository体系,实现一组 JPA Criteria 查询相关的方法:

Specification:封装 JPA Criteria 查询条件。通常使用匿名内部类的方式来创建该接口的对象。

举例:实现带查询条件的分页,并且查询 id > 5 的数据,使用 JpaSpecificationExecutor 接口的JpaSpecificationExecutor 的 Page<T> findAll(Specification<T> spec, Pageable 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
@Test
public void testJpaSpecificationExecutor(){
int pageNo = 3 - 1;
int pageSize = 5;
PageRequest pageable = new PageRequest(pageNo, pageSize);

//通常使用 Specification 的匿名内部类
Specification<Person> specification = new Specification<Person>() {
/**
*
* @param root 根对象,也就是要把条件封装到哪个对象中
* @param criteriaQuery 封装的都是查询的关键字,比如group by order by等
* @param criteriaBuilder 用了封装条件对象的,如果直接返回null,表示不需要任何条件
* @return
*/
@Override
public Predicate toPredicate(Root<Person> root, CriteriaQuery<?> query, CriteriaBuilder criteriaBuilder) {
Predicate predicate = criteriaBuilder.gt(root.get("id"), 5);
return predicate;
}
};

Page<Person> page = personRepsotory.findAll(specification, pageable);

System.out.println("总记录数: " + page.getTotalElements());
System.out.println("当前第几页: " + (page.getNumber() + 1));
System.out.println("总页数: " + page.getTotalPages());
System.out.println("当前页面的 List: " + page.getContent());
System.out.println("当前页面的记录数: " + page.getNumberOfElements());
}

Jpa条件查询最佳实践

当有多个条件查询时,需要创建多个 Predicate 对象,并将这些 Predicate 对象并联起来,因此可以定义一个createSpecification(Map searchMap)方法,用来创建最终的 Specification 对象,以下是伪代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/**
* 动态条件构建
* @param searchMap
* @return
*/
private Specification<Article> createSpecification(Map searchMap) {
return new Specification<Article>() {
@Override
public Predicate toPredicate(Root<Article> root, CriteriaQuery<?> query, CriteriaBuilder cb) {
List<Predicate> predicateList = new ArrayList<Predicate>();
// 获取 xxx 查询条件,依次做非空判断,之后条件对象创建并放入list中
if (searchMap.get("xxx")!=null && !"".equals(searchMap.get("xxx"))) {
predicateList.add(cb.like(root.get("xxxx").as(String.class), "%"+(String)searchMap.get("xxx")+"%"));
}
……

// 将list转成 Predicate[] 数组,进行拼接
return cb.and(predicateList.toArray(new Predicate[predicateList.size()]));
}
};

}

代码解释:比如传参 searchMap 中存了{“name”:”woodwhales”}的键值对,那么将该对象的所有属性当作key对这个map依次获取一遍,能获取到的key 就表示当前属性需要条件查询,创建条件对象并放入集合中。最后将所有条件对象并联返回。

简单概述就是,将传过来的map中的键值对,依次和当前对象属性比对,有值的就是要条件查询的。

9. 自定义Repository接口

步骤1:定义一个接口:声明要添加的,并自实现的方法

步骤2:提供该接口的实现类:类名需在要声明的 Repository 后添加 Impl,并实现方法

步骤3:声明 Repository 接口,并继承步骤1 声明的接口。

三步完成之后,就可以使用自定义的接口了。

例如:

步骤1:自定义Repository接口名称为:PersonRepsotory,里面有个test()方法:

1
2
3
4
5
public interface PersonRepsotory {

void test();

}

步骤2:实现自定义接口,注意实现类必须是接口名+Impl后缀的类名:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;

public class PersonRepsotoryImpl implements PersonRepsotory {

@PersistenceContext
private EntityManager entityManager;

@Override
public void test() {
Person person = entityManager.find(Person.class, 11);
System.out.println("PersonRepsotoryImpl -> " + person);
}

}

注意:默认情况下,Spring Data 会在 base-package 中查找 “接口名Impl” 作为实现类。也可以通过repository-impl-postfix声明后缀。

步骤3:在真正的业务接口中继承自定义的接口,此时当前 PersonDao 接口就具有了自定义接口里面的方法了:

1
2
3
public interface PersonDao extends JpaRepository<Person, Integer>, JpaSpecificationExecutor<Person>, PersonRepsotory {

}

参考资料:

官方文档

updated updated 2024-01-01 2024-01-01
本文结束感谢阅读

本文标题:Spring Jpa Data 快速入门指南

本文作者:woodwhales

原始链接:https://woodwhales.cn/2019/08/02/046/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

0%