从零开始 Spring Boot 62:过滤实体和关系
图源:简书 (jianshu.com)
JPA(Hibernate)中有一些注解可以用于筛选实体和关系,本文将介绍这些注解。
@Where
有时候,我们希望对表中的数据进行“软删除”,即删除的时候只修改其标记位而不从表中物理删除。
对于存在软删除的表,在执行相应查询的时候都要考虑删除标记,即添加上相应字段的条件语句后进行查询。这样就显得很麻烦,持久层框架一般都会支持对这种情况的“自动处理”,比如 MyBatisPlus。
JPA(Hibernate) 同样支持这种做法,通过使用@Where
注解实现:
@Entity
@Table(name = "student")
@Where(clause = "del_flag = false")
public class Student {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
// ...
@NotNull
@Builder.Default
private Boolean delFlag = false;
@OneToMany(mappedBy = "student",
cascade = CascadeType.ALL,
fetch = FetchType.EAGER)
@Builder.Default
private List<Account> accounts = new ArrayList<>();
// ...
}
@Entity
@Table(name = "account")
@Where(clause = "del_flag = false")
public class Account {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
// ...
@NotNull
@Builder.Default
private Boolean delFlag = false;
@ManyToOne
@JoinColumn(name = "student_id")
private Student student;
}
这里的实体类Student
和Account
都具备一个属性delFlag
用于表示是否已经被删除(默认为否),用@Where
注解标记实体类,并指定查询条件(del_flag = false
),现在 JPA 从数据库中加载实体数据时就会用查询条件自动过滤。
比如:
var students = studentRepository.findAll();
var savedIcexmoon = students.stream().filter(s -> s.getName().equals("icexmoon")).findFirst().get();
Assertions.assertEquals(2, savedIcexmoon.getAccounts().size());
savedIcexmoon.getAccounts().forEach(a -> {
a.setDelFlag(true);
});
studentRepository.save(savedIcexmoon);
students = studentRepository.findAll();
savedIcexmoon = students.stream().filter(s -> s.getName().equals("icexmoon")).findFirst().get();
Assertions.assertEquals(0, savedIcexmoon.getAccounts().size());
可以看到,修改Student
关联的Email
的删除标识,将其“软删除”后,再查询就不会再查询到相应的数据。
SQL 日志:
select s1_0.id,s1_0.del_flag,s1_0.name from student s1_0 where (s1_0.del_flag = 0)
select a1_0.student_id,a1_0.id,a1_0.del_flag,a1_0.name,a1_0.password,a1_0.role from account a1_0 where a1_0.student_id=? and (a1_0.del_flag = 0)
binding parameter [1] as [BIGINT] - [5]
需要注意的是,@Where
注解同样会影响到 JPQL 的查询,但原生SQL(Native SQL)查询不受影响。
比如,为了让测试用例都有一致的原始数据,在每个用例执行前都需要清空相应的表,可以:
session.createNativeQuery("delete from account", Account.class).executeUpdate();
session.createNativeQuery("delete from student", Student.class).executeUpdate();
如果这里使用的是Session.createQuery(delete from Account)
,调用时实际执行的 SQL 就会是delete from account where del_flag = 0
。这样就会保留一部分被软删除的数据,甚至因为外键关联的原因导致SQL执行失败。
用于集合(关系)
@Where
除了可以用于实体,还可以用于集合(关系)。
假设Account
实体中有一个属性role
,用于记录不同的帐号类型:
public class Account {
public enum Role {
ADMIN, MEMBER
}
// ...
@NotNull
@Enumerated(EnumType.STRING)
private Role role;
// ...
}
在Student
实体中,我们可以借助@Where
为不同的“连接Account的关系”设置不同的查询条件,以按照帐号的角色对Student
关联的Account
进行分组:
public class Student {
// ...
@Where(clause = "role = 'ADMIN'")
@OneToMany(mappedBy = "student",
cascade = CascadeType.ALL,
fetch = FetchType.EAGER)
private List<Account> adminAccounts = new ArrayList<>();
@Where(clause = "role = 'MEMBER'")
@OneToMany(mappedBy = "student",
cascade = CascadeType.ALL,
fetch = FetchType.EAGER)
private List<Account> memberAccounts = new ArrayList<>();
// ...
}
现在就可以轻松获取某个Student
下不同角色的账号:
var savedStudent = studentRepository.findAll().stream().findFirst().get();
System.out.println("admin accounts;");
savedStudent.getAdminAccounts().forEach(a->{
System.out.println(a);
});
System.out.println("member accounts:");
savedStudent.getMemberAccounts().forEach(a->{
System.out.println(a);
});
从 SQL 日志中可以很清楚地看到@Where
发挥的作用:
select s1_0.id,s1_0.del_flag,s1_0.name from student2 s1_0 where (s1_0.del_flag = 0)
select m1_0.student_id,m1_0.id,m1_0.del_flag,m1_0.name,m1_0.password,m1_0.role from account2 m1_0 where m1_0.student_id=? and (( m1_0.del_flag = 0 ) and ( m1_0.role = 'MEMBER' ))
binding parameter [1] as [BIGINT] - [1]
select a1_0.student_id,a1_0.id,a1_0.del_flag,a1_0.name,a1_0.password,a1_0.role from account2 a1_0 where a1_0.student_id=? and (( a1_0.del_flag = 0 ) and ( a1_0.role = 'ADMIN' ))
binding parameter [1] as [BIGINT] - [1]
@WhereJoinTable
@WhereJoin
与@Where
类似,不过它作用于@JoinTable
和@ManyToMany
定义的多对多关系,可以添加相应的过滤条件。
看下面的示例:
@Entity(name = "Student3")
@Table(name = "student3")
public class Student {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@NotNull
@NotBlank
@Length(max = 45)
@Column(unique = true)
private String name;
@ManyToMany(fetch = FetchType.EAGER)
@JoinTable(name = "student_address3",
joinColumns = {@JoinColumn(name = "student_id")},
inverseJoinColumns = {@JoinColumn(name = "address_id")})
@Builder.Default
private List<Address> addresses = new ArrayList<>();
}
@Entity(name = "Adress3")
@Table(name = "address3")
public class Address {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@NotNull
@NotBlank
@Length(max = 100)
private String name;
@ManyToMany(mappedBy = "addresses", fetch = FetchType.EAGER)
@Builder.Default
private List<Student> students = new ArrayList<>();
}
这里有两个实体,Student
和Address
之前是用@JoinTable
和@ManyToMany
实现的多对多关系。假设现在需要在关系表上添加一个字段,用来表示这个对应关系是长期住址还是短期住址。
为了方便单独添加关系表的数据,为其创建实体:
@Entity(name = "StudentAddress3")
@Table(name = "student_address3")
public class StudentAddress {
@Embeddable
@EqualsAndHashCode
@NoArgsConstructor
@AllArgsConstructor
public static class StudentAdressId implements Serializable {
@Column(name = "student_id")
private Long studentId;
@Column(name = "address_id")
private Long addressId;
}
public enum Type {
TEMPORARY, LONG
}
@EmbeddedId
private StudentAdressId id;
@NotNull
@Enumerated(EnumType.STRING)
private Type type;
}
这个实体只是为了用于添加数据,所以简单起见没有创建任何对Student
或Adress
的关系映射。
现在我们可以在Student
中利用@WhereJoin
添加对常住地址或短期地址的筛选映射:
public class Student {
// ...
@ManyToMany(fetch = FetchType.EAGER)
@WhereJoinTable(clause = "type = 'LONG'")
@JoinTable(name = "student_address3",
joinColumns = {@JoinColumn(name = "student_id")},
inverseJoinColumns = {@JoinColumn(name = "address_id")})
@Builder.Default
private List<Address> longAddresses = new ArrayList<>();
@ManyToMany(fetch = FetchType.EAGER)
@WhereJoinTable(clause = "type = 'TEMPORARY'")
@JoinTable(name = "student_address3",
joinColumns = {@JoinColumn(name = "student_id")},
inverseJoinColumns = {@JoinColumn(name = "address_id")})
@Builder.Default
private List<Address> temporaryAddresses = new ArrayList<>();
}
测试:
studentRepository.save(student1);
addressRepository.save(address1);
addressRepository.save(address2);
studentAddressRepository.save(new StudentAddress(
new StudentAddress.StudentAdressId(student1.getId(), address1.getId()),
StudentAddress.Type.LONG));
studentAddressRepository.save(new StudentAddress(
new StudentAddress.StudentAdressId(student1.getId(), address2.getId()),
StudentAddress.Type.TEMPORARY));
var students = studentRepository.findAll();
students.forEach(s->{
System.out.println(s);
});
最后查询部分的 SQL 日志:
select s1_0.id,s1_0.name from student3 s1_0
select t1_0.student_id,t1_1.id,t1_1.name from student_address3 t1_0 join address3 t1_1 on t1_1.id=t1_0.address_id where t1_0.student_id=? and (t1_0.type = 'TEMPORARY')
binding parameter [1] as [BIGINT] - [2]
select s1_0.address_id,s1_1.id,s1_1.name from student_address3 s1_0 join student3 s1_1 on s1_1.id=s1_0.student_id where s1_0.address_id=?
binding parameter [1] as [BIGINT] - [4]
select l1_0.student_id,l1_1.id,l1_1.name from student_address3 l1_0 join address3 l1_1 on l1_1.id=l1_0.address_id where l1_0.student_id=? and (l1_0.type = 'LONG')
binding parameter [1] as [BIGINT] - [2]
select s1_0.address_id,s1_1.id,s1_1.name from student_address3 s1_0 join student3 s1_1 on s1_1.id=s1_0.student_id where s1_0.address_id=?
binding parameter [1] as [BIGINT] - [3]
select a1_0.student_id,a1_1.id,a1_1.name from student_address3 a1_0 join address3 a1_1 on a1_1.id=a1_0.address_id where a1_0.student_id=?
binding parameter [1] as [BIGINT] - [2]
可以看到,Hibernate 会使用相应的筛选条件进行join
查询。
@Filter
@Where
和@WhereJoin
都是“静态的”,无法在(程序)运行时改变其行为,但@Filter
可以实现。
假设有一个Student
实体,包含一个表示平均分的属性:
@ToString
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Entity(name = "Student4")
@Table(name = "student4")
public class Student {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@NotNull
@NotBlank
@Length(max = 45)
private String name;
@NotNull
private Integer averageScore;
}
现在需要根据需要,在查询这个实体的时候过滤大于某个平均分的学生,并且需要最低分可以设置。
可以用@Filter
实现:
@FilterDef(
name = "scoreFilter",
parameters = @ParamDef(name = "minScore", type = Integer.class)
)
@Filter(
name = "scoreFilter",
condition = "average_score > :minScore"
)
public class Student {
// ...
}
用@FilterDef
可以添加一个过滤器的定义,其parameters
属性可以定义过滤器的参数。@FilterDef
可以添加到类定义或者包定义上,所以是可以重用的。
@Filter
定义一个实体使用的过滤器,过滤条件由condition
指定,其中的:minScore
指代@FilterDef
中的参数。
注意,
condition
中的 SQL 语句是原生 SQL,而非 JPQL,所以字段使用的是表字段名(average_score
)而非实体属性名(averageScore
)。
过滤器使用的时候要在持久上下文(Session
或EntityManager
)上启用,并设置参数,之后用这个实体上下文从数据库加载相应的实体数据就会使用过滤器和指定参数过滤数据。换言之,过滤器和持久上下文是绑定的,所以只有启用了过滤器的持久上下文会受影响。
调用示例:
@Test
void test() {
var session = sessionFactory.openSession();
var transaction = session.beginTransaction();
printStudents(85, session);
printStudents(70, session);
transaction.commit();
session.close();
}
void printStudents(int minScore, Session session){
session.enableFilter("scoreFilter")
.setParameter("minScore", minScore);
var students = session.createQuery("from Student4", Student.class)
.getResultList();
students.forEach(s -> {
System.out.println(s);
});
}
SQL 日志:
select s1_0.id,s1_0.average_score,s1_0.name from student4 s1_0 where s1_0.average_score > ?
binding parameter [1] as [INTEGER] - [85]
select s1_0.id,s1_0.average_score,s1_0.name from student4 s1_0 where s1_0.average_score > ?
binding parameter [1] as [INTEGER] - [70]
用于集合(关系)
与@Where
类似,@Filter
同样可以用于筛选集合(关系)。
比如:
@FilterDef(name = "accountRoleFilter",
parameters = @ParamDef(name = "role", type = String.class),
defaultCondition = "role = :role")
@Entity(name = "Student5")
@Table(name = "student5")
public class Student {
k // ...
@Filter(name = "accountRoleFilter")
@OneToMany(mappedBy = "student",
cascade = CascadeType.ALL,
fetch = FetchType.EAGER)
private List<Account> accounts = new ArrayList<>();
// ...
}
@Entity(name = "Account5")
@Table(name = "account5")
public class Account {
public enum Role{
ADMIN, MEMBER
}
// ...
@NotNull
@Enumerated(EnumType.STRING)
private Role role;
@ManyToOne
@JoinColumn(name = "student_id")
private Student student;
}
调用示例:
session.enableFilter("accountRoleFilter")
.setParameter("role", "ADMIN");
var students = session.createQuery("from Student5", Student.class).getResultList();
students.forEach(s->{
System.out.println(s);
});
此外,还有一个类似@WhereJoinTable
的注解@FilterJoinTable
,这里不再赘述。
总结
@Where
和@WhereJoinTable
可用于静态筛选实体和关系,@Filter
和@FilterJoinTable
可以动态地设置参数并筛选实体和关系。
The End,谢谢阅读。
本文地所有示例代码可以从这里获取。
参考资料
- Deleting Objects with Hibernate | Baeldung
- Dynamic Mapping with Hibernate | Baeldung
- Hibernate @WhereJoinTable Annotation | Baeldung
- Hibernate ORM 6.2.6.Final User Guide (jboss.org)