Spring Data JPA 投影使用教程

相信不少后端码农在进阶为 “高级增删改查程序员” 的道路上一般都会遇到类似这样的问题,我的数据库里面有一个 user 表,表中有 id, username, password 三个字段,前端用户在查询个人信息时,并不需要把 password 字段返回给前端,这个时候应该要使用怎样的方式进行优雅的处理呢?

数据库表格:

id username passowrd
01 zhangsan password01
02 lisi password02
03 wangwu password02

返回给前端的数据:

1
2
3
4
{
"id": "01",
"username": "zhangsan"
}

面对这样的问题,最优雅的方式一定是使用 Spring Data JPA,如果你还没有用过,那么赶紧点开本教程吧!

Tips: 本教程对应的源代码地址:配合使用,学习更加迅速

实体类

定义两个实体类,User 和 Role,User 和 Role 的关系是多对多的关系,当我们添加上 @Table 注解时,表示这两个实体类需要生成对应的具体的 Java 表格。

实体类中的 @Data, @NoArgsConstructor 等注解属于 lombok 框架。这个框架可以让我们在写 Java Bean 的时候省略 Getter 和 Setter 方法和构造器方法。详细用法,可以自行百度

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
package me.liluyang.jpa.entity;

import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import javax.persistence.*;
import java.util.List;

/**
* 实体类Demo
*/
@Data
@Table
@Entity
@NoArgsConstructor
@AllArgsConstructor
public class User {

@Id
private String id;

private String username;

private String password;

private String gender;

@OneToOne
private Address address;

@JsonIgnoreProperties("users")
@ManyToMany(mappedBy = "users")
private List<Role> roles;

public User(String id, String username) {
this.id = id;
this.username = username;
}
}
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
package me.liluyang.jpa.entity;

import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.ManyToMany;
import javax.persistence.Table;
import java.util.List;

/**
* 实体类Demo
*/
@Data
@Table
@Entity
@NoArgsConstructor
@AllArgsConstructor
public class Role {

@Id
private String id;

private String name;

@JsonIgnoreProperties(value = {"address", "roles"})
@ManyToMany
private List<User> users;

public Role(String id, String name) {
this.id = id;
this.name = name;
}
}

投影的使用

在上面呢,我们定义了两个最正常的实体User 和 Role,他们分别和数据库中的 user,role 表相对应。Spring Data JPA 支持的映射当中有两种方法,第一种是接口 interface 的方式,第二种是实体类 class 的方式

使用实体类(Java Bean)的方式接收 JPA 的返回值

1. 返回包含基础数据属性的实体类

这里的返回基础数据指 Integer,String 等基础的属性,而不包含 class 等嵌套实体类。

我们顶一个一个 UserDto 的实体类来接受数据库的返回值,注意实体类的定义方式,属性名称,Getter,Setter方法和 JPA entity 对象保持一致,另外只能有一个包含所有参数的构造方法,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
package me.liluyang.jpa.dto;

import lombok.AllArgsConstructor;
import lombok.Data;

/**
* 使用 dto class 效率很高,要什么数据,查询什么数据
*
* 基础数据查询效率非常高
*
*/
@Data
@AllArgsConstructor
public class UserDto {

private String id;

private String username;

private String gender;

// 如果取消注释,可以正常的关联查询到实体类。正常,但是没有意义
// private Address address;

// 报错
// private AddressDto address;
}
1
2
3
4
5
public interface UserRepository extends JpaRepository<User, String> {

UserDto findClassById(String id);

}

2. 返回包含基础数据属性的嵌套实体类

JPA 本地并不支持使用 class 接受数据库中的嵌套数据。

使用接口(Interface)的方式接收 JPA 的返回值

如果要使用接口接收 JPA 的返回结果,接口中需要实现 JPA entity 中属性同名的 Get 方法,例如属性中如果有 name 字段,则需要在接口中实现 getName() 方法

1
2
3
4
5
6
7
8
package me.liluyang.jpa.dto;

public interface UserView {

String getId();

String getUsername();
}
1
2
3
4
5
public interface UserRepository extends JpaRepository<User, String> {

UserView findViewById(String id);

}

除此之外,接口还支持返回嵌套实体类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// interface 接受数据定义,address 是我定义的另外一个实体类
public interface UserView2 {

String getId();

String getUsername();

AddressView getAddress();

Set<RoleView> getRoles();
}

// UserRepository 代码定义
UserView2 findView2ById(String id);

// test 接口。使用 jackson 中的 new ObjectMapper() 将对象序列化
@Test
public void findView2() throws Exception {
UserView2 view = userRepository.findView2ById("1");
System.out.println(new ObjectMapper().writeValueAsString(view));
}

// 输出结果
// {"address":{"location":"深圳"},"id":"1","roles":[{"name":"管理员"},{"name":"用户"}],"username":"赵敏"}

使用 spel 表达式计算返回值

接口中的代码定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package me.liluyang.jpa.dto;

import org.springframework.beans.factory.annotation.Value;

public interface UserView3 {

/**
*
* 开放式投影,开放式投影。开放式投影可以使用不完全和实体类相同的 getter 和 setter 方法,并且在运行时,动态的返回计算结果。
*
* 开放式预测有一个缺点:Spring Data无法优化查询执行,因为它事先不知道将使用哪些属性。
*
* 因此,当封闭投影无法满足我们的要求时,我们应该只使用开放投影。
*
* 使用 @value 注解查询复合结果,@Value 注解内部的表达式是 spel 表达式。类似前端 Vue 等工具的计算属性
*
* target 指真实的实体类对象
* @return
*/
@Value("#{target.username + '&' + target.gender}")
String getUsernameAndGender();
}

动态查询

除了将以上介绍的集中普通的查询方式, JPA 还支持动态的设置返回数据结果集,在查询中将返回数据对象传入,获取合适的返回值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

// UserRepository 代码定义
<T> T findByUsername(String username, Class<T> type);

// junit 中的测试代码
@Test
public void findByDynamic() throws Exception {
User user = userRepository.findByUsername("张无忌", User.class);
UserDto dto = userRepository.findByUsername("张无忌", UserDto.class);
UserView view = userRepository.findByUsername("张无忌", UserView.class);

System.out.println(new ObjectMapper().writeValueAsString(user));
System.out.println(new ObjectMapper().writeValueAsString(dto));
System.out.println(new ObjectMapper().writeValueAsString(view));
}

// 控制台输出结果如下
// {"id":"2","username":"张无忌","password":null,"gender":null,"address":null,"roles":[{"id":"1","name":"管理员"},{"id":"2","name":"用户"}]}
// {"id":"2","username":"张无忌","gender":null}
// {"id":"2","username":"张无忌"}

如何在序列化时解决对象循环引用的问题

使用 @jsonIgnore 注解和 @jsonIgnoreProperties

使用这种方式将当前对象中子对象中引用的所有 object 给切断,这样就不会产生循环

但是如果数据库中的关联关系足够复杂,依然会产生很多多余的查询。

学校 > 院系 > 学生 > 课程 之类的关联

那么如果在合适的地方进行切断呢,这个目前比较合适的方式还是自己使用上面结果集映射的方式。根据不同场景下的 api 返回不同的查询结果

参考链接

~


面条先生 wechat
欢迎关注我的 “知乎日报” 小程序