Spring Data JPA的使用及开启二级缓存

前言

Spring Data JPA 是基于 Hibernate 的。

Hibernate 是一个广泛使用的 Java ORM(对象关系映射)框架,它提供了对关系型数据库的映射和操作功能,使开发者能够以面向对象的方式来处理数据库操作,而不用直接编写 SQL 语句。

与 MyBatis 比较

  • Spring Data JPA:优点是代码简单、易于维护,集成 Spring 框架更方便;

    缺点是灵活性不如 MyBatis,性能也可能不如 MyBatis。

  • MyBatis:优点是灵活性强,可以执行复杂的 SQL 语句;

    缺点是需要手动编写 SQL 语句,难以维护。

环境

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
36
37
38
39
40
41
42
43
44
45
46
47
48
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.6.3</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>cn.psvmc</groupId>
<artifactId>z-api-jpa</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>z-api-jpa</name>
<description>z-api-jpa</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</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-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>

添加依赖

在项目的 pom.xml 文件中添加如下 Spring Data JPA 相关依赖:

1
2
3
4
5
6
7
8
9
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>

<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>

连接配置

application.properties配置文件中增加数据库参数,信息内容如下:

1
2
3
4
5
6
7
8
9
10
spring.datasource.url=jdbc:mysql://127.0.0.1:3306/zbi_source?useUnicode=true&characterEncoding=utf8
spring.datasource.username=root
spring.datasource.password=123456
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.hikari.minimum-idle=10
spring.datasource.hikari.maximum-pool-size=20
spring.datasource.hikari.idle-timeout=500000
spring.datasource.hikari.max-lifetime=540000
spring.datasource.hikari.connection-timeout=60000
spring.datasource.hikari.connection-test-query=SELECT 1

实体类

在项目中创建实体类,用于映射数据库表和列。

表实体

实体类需要使用@Entity注解进行标记,并且需要指定主键和自动生成策略。

例如:

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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
package cn.psvmc.zapijpa.entity;

import javax.persistence.*;

@Entity
@Table(name = "t_user")
public class UserEntity {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;

@Column(name = "name", length = 20, nullable = false)
private String name;

@Column(name = "age")
private Integer age;

@Column(name = "sex")
private Integer sex;

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 Integer getSex() {
return sex;
}

public void setSex(Integer sex) {
this.sex = sex;
}
}

主键生成

1
2
3
4
5
@Id
@GeneratedValue(generator="system-uuid")
@GenericGenerator(name="system-uuid", strategy = "uuid")
@Column(name = "id")
private String id;

自动建表

默认JPA是不会自动建表的,但是如果想自动建表,可以添加配置。

1
spring.jpa.hibernate.ddl-auto=update

设置 spring.jpa.hibernate.ddl-auto 属性为 createupdate

create 表示每次启动应用时都会删除现有表并重新创建。

update 表示每次启动应用时会根据实体类的定义,更新已存在的表结构(增加或修改列),但不会删除数据。如果表不存在也会创建。

一般来说使用 update,如果不想自动建表可以设置为none

关系映射

关系映射通常包括一对一、一对多和多对多等关系。

在 Spring Data JPA 中,可以使用 @OneToOne@OneToMany@ManyToMany 注解来标注关系映射。

这些注解通常与 @JoinColumn 注解一起使用,用于指定关联的外键列。

示例代码:

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
@Entity
@Table(name = "t_user")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

@OneToMany(mappedBy = "user", cascade = CascadeType.ALL)
private List<Address> addresses;

// 省略其他属性和方法
}

@Entity
@Table(name = "t_address")
public class Address {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

@ManyToOne
@JoinColumn(name = "user_id")
private User user;

// 省略其他属性和方法
}

在上例中,UserAddress 之间是一对多的关系,所以在 User 实体类中使用了 @OneToMany 注解,在 Address 实体类中使用了 @ManyToOne 注解。mappedBy 属性用于指定关联的属性名称,这里是 user,表示 Address 实体类中的 user 属性与 User 实体类中的 addresses 属性相对应。

cascade 属性表示级联操作,这里使用 CascadeType.ALL 表示在删除 User 实体时同时删除其关联的所有 Address 实体。

@JoinColumn 注解用于指定外键名称,这里是 user_id,表示 Address 表中的 user_id 列与 User 表中的主键相对应。

Repository

UserRepository.java

1
2
3
4
5
6
7
8
9
10
package cn.psvmc.zapijpa.repository;

import cn.psvmc.zapijpa.entity.UserEntity;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface UserRepository extends JpaRepository<UserEntity, Long> {
UserEntity findByName(String name);
}

内置方法

在继承 Repository 接口后,会默认提供基本的增删改查方法,无需额外的代码实现即可使用。

常用的方法如下:

方法名 描述
T save(T entity) 保存实体对象
Iterable saveAll(Iterable entities) 批量保存实体对象
Optional findById(ID id) 根据主键获取实体对象
boolean existsById(ID id) 判断是否存在特定主键的实体对象
Iterable findAll() 获取所有实体对象
Iterable findAllById(Iterable ids) 根据主键批量获取实体对象
long count() 获取实体对象的数量
void deleteById(ID id) 根据主键删除实体对象
void delete(T entity) 删除实体对象
void deleteAll(Iterable<? extends T> entities) 批量删除实体对象

方法名称查询

方法名称查询是 Spring Data JPA 中最简单的一种自定义查询方法,并且不需要额外的注解或 XML 配置。

它通过方法名来推断出查询的条件,

例如以 findBy 开头的方法表示按照某些条件查询,以 deleteBy 开头的方法表示按照某些条件删除数据。

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
public interface UserRepository extends Repository<UserEntity, Long> {
// 根据用户名查询用户
UserEntity findByName(String name);

// 根据年龄查询用户列表
List<UserEntity> findByAge(Integer age);

// 根据用户名和密码查询用户
UserEntity findByNameAndPassword(String name, String password);

// 根据主键和用户名删除用户
void deleteByIdAndUserName(Long id, String name);
}

规则

关键字 方法命名 sql where字句
And findByNameAndPwd where name= ? and pwd =?
Or findByNameOrSex where name= ? or sex=?
Is,Equals findById,findByIdEquals where id= ?
Between findByIdBetween where id between ? and ?
LessThan findByIdLessThan where id < ?
LessThanEquals findByIdLessThanEquals where id <= ?
GreaterThan findByIdGreaterThan where id > ?
GreaterThanEquals findByIdGreaterThanEquals where id > = ?
After findByIdAfter where id > ?
Before findByIdBefore where id < ?
IsNull findByNameIsNull where name is null
isNotNull,NotNull findByNameNotNull where name is not null
Like findByNameLike where name like ?
NotLike findByNameNotLike where name not like ?
StartingWith findByNameStartingWith where name like ‘?%’
EndingWith findByNameEndingWith where name like ‘%?’
Containing findByNameContaining where name like ‘%?%’
OrderBy findByIdOrderByXDesc where id=? order by x desc
Not findByNameNot where name <> ?
In findByIdIn(Collection<?> c) where id in (?)
NotIn findByIdNotIn(Collection<?> c) where id not in (?)
TRUE findByStateTue where state= true
FALSE findByStateFalse where state= false
IgnoreCase findByNameIgnoreCase where UPPER(name)=UPPER(?)

查询参数设置

除了方法名称查询外,还可以使用参数设置方式进行自定义查询。

它通过在方法上使用 @Query 注解来指定查询语句,然后使用 @Param 注解来指定方法参数与查询语句中的参数对应关系。

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
public interface UserRepository extends Repository<UserEntity, Long> {
// 根据用户名查询用户
@Query("SELECT u FROM UserEntity u WHERE u.name = :name")
UserEntity findByName(@Param("name") String name);

// 根据用户名和密码查询用户
@Query("SELECT u FROM UserEntity u WHERE u.name = :name AND u.password = :password")
UserEntity findByUserNameAndPassword(@Param("name") String name, @Param("password") String password);

@Query("SELECT u FROM UserEntity u WHERE u.name like %:name%")
List<UserEntity> findByName(@Param("name") String name);
}

使用 Native SQL 查询

在某些情况下,需要执行原生的 SQL 查询语句。

Spring Data JPA 提供了 @Query 注解来支持使用原生 SQL 查询数据。

@Query 注解中设置 nativeQuery=true 即可执行原生 SQL 语句。

以下示例代码演示了如何使用原生 SQL 查询 age 大于等于 18 的用户。

1
2
3
4
public interface UserRepository extends JpaRepository<UserEntity, Long> {
@Query(value = "SELECT * FROM t_user WHERE age >= ?1", nativeQuery = true)
List<UserEntity> findByAgeGreaterThanEqual(Integer age);
}

使用

1
userRepository.findByAgeGreaterThanEqual(18);

这两种是一样的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import cn.psvmc.zapijpa.entity.UserEntity;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;

import java.util.List;

@Repository
public interface UserRepository extends JpaRepository<UserEntity, Long> {
@Query("SELECT u FROM UserEntity u WHERE u.name like %:name%")
List<UserEntity> findByName(@Param("name") String name);

@Query(value = "SELECT * FROM t_user WHERE name like %?1%", nativeQuery = true)
List<UserEntity> findByName2(@Param("name") String name);
}

让人难受的是下面的写法是对的,但是IDEA工具会提示错误。

排序和分页

在查询数据时,经常需要对结果进行排序和分页操作。

Spring Data JPA 提供了 SortPageable 两个类来实现排序和分页功能。

Sort 类表示排序规则,可以使用 Sort.by() 静态方法创建实例,并指定排序属性和排序方向。

常用方法如下:

方法名 描述
static Sort by(Sort.Order… orders) 根据排序规则创建 Sort 实例
static Sort.Order by(String property) 根据属性升序排序
static Sort.Order by(String property, Sort.Direction direction) 根据属性排序

示例代码:

1
2
3
4
public interface UserRepository extends Repository<UserEntity, Long> {
// 根据年龄升序查询用户列表
List<UserEntity> findByOrderByAgeAsc();
}

Pageable 类表示分页信息,可以使用 PageRequest.of() 静态方法创建实例,并指定页码、每页数据量和排序规则。

常用方法如下:

方法名 描述
static PageRequest of(int page, int size, Sort sort) 创建分页信息实例
static PageRequest of(int page, int size, Sort.Direction direction, String… properties) 创建分页信息实例

示例代码:

1
2
3
4
public interface UserRepository extends Repository<UserEntity, Long> {
// 根据年龄降序分页查询用户列表
Page<UserEntity> findBy(Pageable pageable);
}

使用

1
2
3
Pageable pageable = PageRequest.of(0, 10, Sort.by("age").descending());
Page<UserEntity> page = userRepository.findBy(pageable);
List<UserEntity> userList = page.getContent();

更新和删除

在 Spring Data JPA 中,使用 updatedelete 语句需要使用 @Modifying 注解标注,并且需要添加 @Transactional 注解开启事务。

需要注意的是,@Modifying 注解只支持 DML 语句。

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
public interface UserRepository extends Repository<UserEntity, Long> {
// 更新用户密码
@Modifying
@Transactional
@Query("UPDATE UserEntity u SET u.password = :password WHERE u.id = :id")
void updatePasswordById(@Param("id") Long id, @Param("password") String password);

// 删除年龄大于等于 age 的用户
@Modifying
@Transactional
@Query("DELETE FROM UserEntity u WHERE u.age >= :age")
void deleteByAgeGreaterThanEqual(@Param("age") Integer age);
}

Service

UserService.java

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
package cn.psvmc.zapijpa.service;

import cn.psvmc.zapijpa.entity.UserEntity;
import cn.psvmc.zapijpa.repository.UserRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import javax.transaction.Transactional;
import java.util.List;

@Service
@Transactional
public class UserService {
@Autowired
private UserRepository userRepository;

public List<UserEntity> userList() {
return userRepository.findAll();
}

// 添加用户
public UserEntity addUser(UserEntity user) {
return userRepository.save(user);
}

// 更新用户
public void updateUser(UserEntity user) {
userRepository.save(user);
}

// 删除用户
public void deleteUser(Long id) {
userRepository.deleteById(id);
}
}

Controller

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package cn.psvmc.zapijpa.controller;

import cn.psvmc.zapijpa.entity.UserEntity;
import cn.psvmc.zapijpa.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;

@RestController
@RequestMapping("/user")
public class UserController {
@Autowired
private UserService userService;
@RequestMapping("/list")
public List<UserEntity> getUsers() {
return userService.userList();
}
}

访问

http://localhost:8080/user/list

多数据源

在实际应用中,有时需要使用多个数据源。

Spring Boot 提供了 @ConfigurationProperties@Primary@Qualifier 等注解来支持多数据源配置。

以下示例代码演示了如何在 Spring Boot 应用程序中配置多数据源。

application.properties 文件中配置两个数据源的连接信息

1
2
3
4
5
6
7
8
# 数据源一
spring.datasource.one.url=jdbc:mysql://localhost:3306/test1
spring.datasource.one.username=root
spring.datasource.one.password=123456
# 数据源二
spring.datasource.two.url=jdbc:mysql://localhost:3306/test2
spring.datasource.two.username=root
spring.datasource.two.password=123456

创建两个数据源的配置类

DataSourceOneConfig

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Configuration
@ConfigurationProperties(prefix = "spring.datasource.one")
public class DataSourceOneConfig {
private String url;
private String username;
private String password;

// 省略 getter 和 setter 方法

@Bean
public DataSource dataSourceOne() {
return DataSourceBuilder.create()
.url(url)
.username(username)
.password(password)
.build();
}
}

DataSourceTwoConfig

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Configuration
@ConfigurationProperties(prefix = "spring.datasource.two")
public class DataSourceTwoConfig {
private String url;
private String username;
private String password;

// 省略 getter 和 setter 方法

@Bean
public DataSource dataSourceTwo() {
return DataSourceBuilder.create()
.url(url)
.username(username)
.password(password)
.build();
}
}

在 Service 或 Repository 中指定要使用的数据源

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
@Service
public class UserService {
@Autowired
@Qualifier("dataSourceOne")
private DataSource dataSourceOne;

@Autowired
@Qualifier("dataSourceTwo")
private DataSource dataSourceTwo;

public void addUser(User user) {
try (Connection connection = dataSourceOne.getConnection();
PreparedStatement statement = connection.prepareStatement(
"INSERT INTO user (name, age) VALUES (?, ?)"
)
) {
statement.setString(1, user.getName());
statement.setInt(2, user.getAge());
statement.executeUpdate();
} catch (SQLException e) {
e.printStackTrace();
throw new RuntimeException(e);
}
}
}

在上述示例代码中,使用 @ConfigurationProperties 注解将数据源的连接信息和配置类绑定。

使用 @Qualifier@Autowired 注解指定要使用的数据源。

在 Service 或 Repository 中通过 DataSource.getConnection() 获取连接,手动执行 SQL 语句。

开启批量操作

Mysql的话,开启批量操作需要在jdbc的url后面加上参数rewriteBatchedStatements=true,Oracle无需此操作。

默认批量操作是关闭的,要想开启设置如下参数

1
2
3
4
5
spring.jpa.properties.hibernate.jdbc.batch_size=2
#开启批量插入
spring.jpa.properties.hibernate.order_inserts=true
#开启批量更新
spring.jpa.properties.hibernate.order_updates=true

batch_size设置值等于1的时候也是不生效的,必须大于1。

这里是为了测试才设置为2。实际使用可以大一点

1
spring.jpa.properties.hibernate.jdbc.batch_size=100

为了方便验证我们可以添加打印配置

1
spring.jpa.properties.hibernate.generate_statistics=true

这样当没有批量处理的时候会看到

spent executing 0 JDBC batches;

有批量的时候值是大于0的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import cn.psvmc.zapijpa.entity.UserEntity;
import cn.psvmc.zapijpa.repository.UserRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import javax.transaction.Transactional;
import java.util.List;

@Service
@Transactional
public class UserService {
@Autowired
private UserRepository userRepository;
// 批量添加用户
@Transactional
public List<UserEntity> addAll(List<UserEntity> users) {
return userRepository.saveAll(users);
}
}

注意

批处理必须开启事务。

Controller中

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
import cn.psvmc.zapijpa.entity.UserEntity;
import cn.psvmc.zapijpa.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.Arrays;
import java.util.List;

@RestController
@RequestMapping("/user")
public class UserController {
@Autowired
private UserService userService;
@RequestMapping("/list")
public List<UserEntity> getUsers() {
return userService.userList();
}

@RequestMapping("/add_users")
public List<UserEntity> addUsers() {
UserEntity u1 = new UserEntity("张三",10);
UserEntity u2 = new UserEntity("李四",18);
UserEntity u3 = new UserEntity("王五",22);
UserEntity u4 = new UserEntity("赵六",16);
List<UserEntity> userList = Arrays.asList(u1, u2, u3, u4);
return userService.addAll(userList);
}
}

当我们调用以下地址的时候会插入4条数据

http://localhost:8080/user/add_users

开启批量设置为2的时候,没两条就进行一次批处理,就会看到显示2次批处理了。

spent executing 2 JDBC batches;

有人说是通过打印SQL查看

1
2
3
4
spring.jpa.properties.hibernate.show_sql=true
spring.jpa.properties.hibernate.format_sql=false
logging.level.org.hibernate.SQL=DEBUG
logging.level.org.hibernate.type.descriptor.sql.BasicBinder=TRACE

但是实测发现就算是批处理,SQL也是一次一条,只不过是多条后批量提交。

打印执行的SQL

1
2
3
4
spring.jpa.properties.hibernate.show_sql=true
spring.jpa.properties.hibernate.format_sql=false
logging.level.org.hibernate.SQL=DEBUG
logging.level.org.hibernate.type.descriptor.sql.BasicBinder=TRACE