Spring Boot 中 JPA 的使用

JPA简介

JPA全称Java Persistence API,JPA通过JDK 5.0注解或XML描述对象-关系表的映射关系,并将运行期的实体对象持久化到数据库中。

项目依赖

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>
<version>6.0.6</version>
</dependency>

项目配置

1
2
3
4
5
6
7
8
9
10
11
12
#通用数据源配置
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://127.0.0.1:3306/test?charset=utf8mb4&useSSL=false
spring.datasource.username=springboot
spring.datasource.password=springboot
# Hikari 数据源专用配置
spring.datasource.hikari.maximum-pool-size=20
spring.datasource.hikari.minimum-idle=5
# JPA 相关配置
spring.jpa.database-platform=org.hibernate.dialect.MySQL5InnoDBDialect
spring.jpa.show-sql=true
spring.jpa.hibernate.ddl-auto=create
  • spring.jpa.show-sql=true 配置在日志中打印出执行的 SQL 语句信息
  • spring.jpa.hibernate.ddl-auto=create 配置指明在程序启动的时候要删除并且创建实体类对应的表。这个参数很危险,因为会把对应的表删除掉然后重建。所以千万不要在生产环境中使用。只有在测试环境中,一开始初始化数据库结构的时候才能使用一次。
  • spring.jpa.database-platform=org.hibernate.dialect.MySQL5InnoDBDialect 在 Srping Boot 2.0 版本中,Hibernate创建数据表的时候,默认的数据库存储引擎选择的是MyISAM(之前好像是InnoDB,这点比较诡异)。这个参数是在建表的时候,将默认的存储引擎切换为InnoDB用的。

数据实体类

数据库实体类是一个POJO Bean对象。这里我们先建立一个UserDO的数据库实体。数据库实体的源码如下:

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

@Data
@Entity
@Table(name = "AUTH_USER")
public class UserDO {
@Id
private Long id;
@Column(length = 32)
private String name;
@Column(length = 32)
private String account;
@Column(length = 64)
private String pwd;
}

其中:

  • @Entity 是一个必选的注解,声明这个类对应了一个数据库表
  • @Table(name = "AUTH_USER") 是一个可选的注解。声明了数据库实体对应的表信息,包括表名称、索引信息等。这里声明这个实体类对应的表名是AUTH_USER。如果没有指定,则表名和实体的名称保持一致
  • @Id 注解声明了实体唯一标识对应的属性
  • @Column(length = 32) 用来声明实体属性的表字段的定义。默认的实体每个属性都对应了表的一个字段。字段的名称默认和属性名称保持一致(并不一定相等)。字段的类型根据实体属性类型自动推断。这里主要是声明了字符字段的长度。如果不这么声明,则系统会采用255作为该字段的长度

以上配置全部正确,则此时运行这个项目,我们就可以看到日志中如下的内容:

1
2
Hibernate: drop table if exists auth_user
Hibernate: create table auth_user (id bigint not null, account varchar(32), name varchar(32), pwd varchar(64), primary key (id)) engine=InnoDB

系统将自动创建表及表结构

实现持久层服务

Spring Data JPA的世界里,实现一个持久层的服务是一个非常简单的事情。以上面的UserDO实体对象为例,要实现一个增加、删除、修改、查询功能的持久层服务,那么只需要声明一个接口,这个接口继承org.springframework.data.repository.Repository<T, ID>接口或者其子接口就行。这里为了功能的完备,我们继承org.springframework.data.jpa.repository.JpaRepository<T, ID>接口。其中T数据库实体类,ID是数据库实体类的主键。

然后再简单的在这个接口上增加一个@Repository注解即可

1
2
3
4
5
6
7
8
9
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

/**
* 用户服务数据接口类
*/
@Repository
public interface UserDao extends JpaRepository<UserDO, Long> {
}

一行代码也不用写。针对UserDO这个实体类,我们已经拥有JpaRepository接口的所有功能

测试数据

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
53
54
55
56
57
58
59
60
61
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;

import java.util.Optional;

@RunWith(SpringRunner.class)
@SpringBootTest
public class UserDOTest {

@Autowired
private UserDao userDao;

@Before
public void before() {
UserDO userDO = new UserDO();
userDO.setId(1L);
userDO.setName("风清扬");
userDO.setAccount("fengqy");
userDO.setPwd("123456");
userDao.save(userDO);
userDO = new UserDO();
userDO.setId(3L);
userDO.setName("东方不败");
userDO.setAccount("bubai");
userDO.setPwd("123456");
userDao.save(userDO);
userDO.setId(5L);
userDO.setName("向问天");
userDO.setAccount("wentian");
userDO.setPwd("123456");
userDao.save(userDO);
}

@Test
public void testAdd() {
UserDO userDO = new UserDO();
userDO.setId(2L);
userDO.setName("任我行");
userDO.setAccount("renwox");
userDO.setPwd("123456");
userDao.save(userDO);
userDO = new UserDO();
userDO.setId(4L);
userDO.setName("令狐冲");
userDO.setAccount("linghuc");
userDO.setPwd("123456");
userDao.save(userDO);
}

@After
public void after() {
userDao.deleteById(1L);
userDao.deleteById(3L);
userDao.deleteById(5L);
}
}

这里采用Junit来执行测试用例。@Before注解在测试用例之前执行准备的代码。这里先插入三个用户信息。执行该测试,查看数据库即可看到数据库中有了5条记录

我们还可以通过测试用例验证通过标识查找对象功能,验证所有数据查询功能的正确性,查询功能甚至可以进行排序和分页

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Test
public void testLocate() {
Optional<UserDO> userDOOptiona = userDao.findById(1L);
if (userDOOptiona.isPresent()) {
UserDO userDO = userDOOptiona.get();
System.out.println("name = " + userDO.getName());
System.out.println("account = " + userDO.getAccount());
}
}

@Test
public void testFindAll() {
List<UserDO> userDOList = userDao.findAll(new Sort(Sort.Direction.DESC,"account"));
for (UserDO userDO : userDOList) {
System.out.println("name = " + userDO.getName());
System.out.println("account = " + userDO.getAccount());
}
}

可以看到,我们所做的全部事情仅仅是在Sping Boot工程里面增加数据库配置信息,声明一个UserDO的数据库实体对象,然后声明一个持久层的接口,改接口继承自org.springframework.data.jpa.repository.JpaRepository<T, ID>接口。然后,系统就自动拥有了丰富的增加、删除、修改、查询功能。查询功能甚至还拥有了排序和分页的功能。

这就是JPA的强大之处。除了这些接口外,用户还会有其他的一些需求,JPA也一样可以满足需求。

扩展查询

根据属性来查询

如果想要根据实体的某个属性来进行查询我们可以在UserDao接口中进行声明。例如,如果想根据实体的account这个属性来进行查询(在登录功能的时候可能会用到)。我们在UserDao中增加一个接口声明即可

1
UserDO findByAccount(String account);

然后增加一个测试用例

1
2
3
4
5
6
7
8
@Test
public void testFindByAccount() {
UserDO userDO = userDao.findByAccount("wentian");
if (userDO != null) {
System.out.println("name = " + userDO.getName());
System.out.println("account = " + userDO.getAccount());
}
}

运行之后,会在日志中打印出

1
2
name = 向问天
account = wentian

这种方式非常强大,不仅能够支持单个属性,还能支持多个属性组合。例如如果我们想查找账号和密码同时满足查询条件的接口。那么我们在UserDao接口中声明

1
UserDO findByAccountAndPwd(String account, String pwd);

再例如,我们要查询id大于某个条件的用户列表,则可以声明如下的接口

1
List<UserDO> findAllByIdGreaterThan(Long id);

这个语句结构可以用下面的表(JPA关键字)说明

关键字 方法命名 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 findByAaaTue where aaa = true
False findByAaaFalse where aaa = false
IgnoreCase findByNameIgnoreCase where UPPER(name)=UPPER(?)

自定义查询

如果上述的情况还无法满足需要。那么我们就可以通过通过import org.springframework.data.jpa.repository.Query注解来解决这个问题。例如我们想查询名称等于某两个名字的所有用户列表,则声明如下的接口即可

1
2
@Query("SELECT O FROM UserDO O WHERE O.name = :name1 OR O.name = :name2")
List<UserDO> findTwoName(@Param("name1") String name1, @Param("name2") String name2);

这里是用PQL的语法来定义一个查询。其中两个参数名字由语句中的:后面的字符来决定

如果你习惯编写SQL语句来完成查询,还可以再用下面的方式实现

1
2
@Query(nativeQuery = true, value = "SELECT * FROM AUTH_USER WHERE name = :name1 OR name = :name2")
List<UserDO> findSQL(@Param("name1") String name1, @Param("name2") String name2);

这里在@Query注解中增加一个nativeQuery = true的属性,就可以采用原生SQL语句的方式来编写查询。

联合主键

org.springframework.data.jpa.repository.JpaRepository<T, ID>接口定义来看,数据实体的主键是一个单独的对象,那么如果一个数据库的表的主键是两个或者两个以上字段联合组成的怎么解决呢。

我们扩充一下前面的场景。假如我们有一个角色Role对象,有两个属性:一个id,一个name,对应auth_role数据表,同时有一个角色用户关系对象RoleUser,说明角色和用户对应关系,有两个属性roleIduserId,对应auth_role_user表。那么我们需要声明一个RoleDO对象如下

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

/**
* 角色实体类
*/
@Data
@Entity
@Table(name = "AUTH_ROLE")
public class RoleDO {
@Id
private Long id;
@Column(length = 32)
private String name;
@Column(length = 64)
private String note;
}

对于有多个属性作为联合主键的情况,我们一般要新建一个单独的主键类,它的属性和数据库实体主键的字段一样,要实现java.io.Serializable接口,类声明如下

1
2
3
4
5
6
7
8
9
import java.io.Serializable;

/**
* 联合主键对象
*/
public class RoleUserId implements Serializable {
private Long roleId;
private Long userId;
}

同样的,我们声明一个 RoleUserDO 对象如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import javax.persistence.*;
import java.io.Serializable;

/**
* 角色用户关系实体类
*/
@Entity
@IdClass(RoleUserId.class)
@Table(name = "AUTH_ROLE_USER")
public class RoleUserDO {
@Id
private Long roleId;
@Id
private Long userId;
}

这里因为数据实体类和数据实体主键类的属性一样,所以我们可以删除掉这个数据实体主键类,然后将数据实体类的主键类声明为自己即可。当然,自己也要实现java.io.Serializable接口。

这样,我们如果要查询某个角色下的所有用户列表,就可以声明如下的接口

1
2
@Query("SELECT U FROM UserDO U, RoleUserDO RU WHERE U.id = RU.userId AND RU.roleId = :roleId")
List<UserDO> findUsersByRole(@Param("roleId") Long roleId);

这种情况下,我们同样会看到系统自动建立AUTH_ROLEAUTH_ROLE_USER

注意这里auth_role_user表中,属性名userId转换为了user_idroleId转换为了role_id

如果我们要用SQL语句的方式实现上面的功能,那么我们就把这个接口声明修改为下面的形式:

1
2
@Query(nativeQuery = true, "SELECT U.* FROM AUTH_USER U, AUTH_ROLE_USER RU WHERE U.id = RU.user_id AND RU.role_id = :roleId")
List<UserDO> findUsersByRole(@Param("roleId") Long roleId);

Powered by AppBlog.CN     浙ICP备14037229号

Copyright © 2012 - 2021 APP开发技术博客 All Rights Reserved.

访客数 : | 访问量 :