从零开始 Spring Boot 62:过滤实体和关系

news2024/11/18 9:36:01

从零开始 Spring Boot 62:过滤实体和关系

spring boot

图源:简书 (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;
}

这里的实体类StudentAccount都具备一个属性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<>();
}

这里有两个实体,StudentAddress之前是用@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;
}

这个实体只是为了用于添加数据,所以简单起见没有创建任何对StudentAdress的关系映射。

现在我们可以在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)。

过滤器使用的时候要在持久上下文(SessionEntityManager)上启用,并设置参数,之后用这个实体上下文从数据库加载相应的实体数据就会使用过滤器和指定参数过滤数据。换言之,过滤器和持久上下文是绑定的,所以只有启用了过滤器的持久上下文会受影响。

调用示例:

@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)

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/726997.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Meta为全天候AR眼镜设计了AI系统的八大指导方针

众所周知&#xff0c;Meta不仅局限在Quest这类VR头显上&#xff0c;同时还在打造更轻量化的AR眼镜&#xff0c;目标就是让产品更好的融入到人们的日常生活中去。除了硬件上轻量化以外&#xff0c;在功能和交互体验上也至关重要&#xff0c;例如自然交互方式&#xff0c;比如手势…

什么是人工智能大模型?

目录 1. 人工智能大模型的概述&#xff1a;2. 典型的人工智能大模型&#xff1a;3. 人工智能大模型的应用领域&#xff1a;4. 人工智能大模型的挑战与未来&#xff1a;5. 人工智能大模型的开发和应用&#xff1a;6. 人工智能大模型的学习资源&#xff1a; 人工智能大模型是指具…

MySQL(创建、删除、查询数据库以及依据数据类型建表)

一、 1.创建数据库&#xff0c; mysql> CREATE DATABASE IF NOT EXISTS SECOND_DB; Query OK, 1 row affected (0.01 sec)2.删除数据库&#xff0c; mysql> DROP DATABASE IF EXISTS SECOND_DB; Query OK, 0 rows affected (0.11 sec)3.查询创建数据的语句&#xff0c;…

优化模型案例

案例1 生产决策问题 &#xff08;一个简单的线性规划问题&#xff09; 某工厂在计划期内要安排I、II两种产品生产。生产单位产品所需的设备台时&#xff0c;A&#xff0c;B两种原材料的消耗&#xff0c;资源的限制以及单件产品利润如下表所示 问工厂应分别生产多少单位产品I和…

修改开发板内核启动日志输出级别

1.用超级用户权限输入命令 2.将verbosity 1改成7&#xff0c;将console(控制&#xff09; both 改成 serial&#xff08;串口控制),然后wq保存退出 3.输入命令sudo reboot 查看启动日志输出级别

华为云CodeArts IDE Online:让你随时随地畅享云端编码乐趣

软件开发是把人类智慧以代码方式表达出来的过程&#xff0c;面对不可预知且快速变化的世界&#xff0c;开发者面临着前所未有的巨大挑战。例如&#xff0c;软件交付周期和迭代速度要求更高、开发者需要快速学习各种新技术、开发时间碎片化严重、分散的交付团队协同困难、开发与…

微信小程序接入第三方后,不能及时发送客服消息

微信小程序接入第三方后&#xff0c;不能及时发送客服消息 1、要把这里关了&#xff0c;后台才能及时收到用户发来的消息

机器学习16:使用 TensorFlow 进行神经网络编程练习

在【机器学习15】中&#xff0c;笔者介绍了神经网络的基本原理。在本篇中&#xff0c;我们使用 TensorFlow 来训练、验证神经网络模型&#xff0c;并探索不同 “层数节点数” 对模型预测效果的影响&#xff0c;以便读者对神经网络模型有一个更加直观的认识。 目录 1.导入依赖…

Dubbo入门详解,API方式与SpringBoot方式

Hi I’m Shendi Dubbo入门详解&#xff0c;API方式与SpringBoot方式 在之前一直使用的自己编写的RPC框架&#xff0c;因为是自己编写的&#xff0c;功能上比不过市面上的开源框架&#xff0c;包括后面Spring Cloud系列&#xff0c;如果还用自己编写的话就需要去做整合之类的&am…

OpenResume一个功能强大的开源简历生成器,太炫了

OpenResume 是一个功能强大的开源简历生成器和简历解析器。目标是为每个人提供免费的现代专业简历设计&#xff0c;让任何人都能充满信心地申请工作。 核心优势 「实时UI更新」:当输入简历信息时&#xff0c;简历 PDF 会实时更新&#xff0c;因此可以轻松查看最终输出。 「现…

LeetCode刷题 | 647. 回文子串、516. 最长回文子序列

647. 回文子串 给你一个字符串 s &#xff0c;请你统计并返回这个字符串中 回文子串 的数目。 回文字符串 是正着读和倒过来读一样的字符串。 子字符串 是字符串中的由连续字符组成的一个序列。 具有不同开始位置或结束位置的子串&#xff0c;即使是由相同的字符组成&#…

ModaHub魔搭社区:清华开源ChatGLM语言模型一键部署教程

目录 ChatGLM是什么 傻瓜式安装部署 一.下载 二、解压 ChatGLM懒人安装包 ChatGLM是什么 ChatGLM和ChatGPT类似&#xff0c;是由清华大学开发的开源大型语言模型。由于它是开源的&#xff0c;所以带来了很多的可能性&#xff0c;比如可以像Ai绘画一样自己微调模型。 目前…

老板说,给我把这个 JS React 项目迁移到 TypeScript

在我们日益发展的网络开发领域中&#xff0c;JavaScript 长期以来一直是首选的语言。它的多功能性和普及性推动了许多应用和网站取得成功。然而&#xff0c;随着项目规模和复杂性的增长&#xff0c;维护 JavaScript 代码库可能变得具有挑战性、容易出错且难以扩展。 走出来的第…

5-Spring cloud之Feign的使用——服务器上实操

5-Spring cloud之Feign的使用——服务器上实操 1. 前言2. 搭建Feign2.1 添加子模块——dog-api2.1.1 子模块结构2.1.2 pom文件2.1.3 核心接口DogClientApi 2.2 添加子模块——dog-consumer-feign-802.2.1 子模块结构2.2.2 pom文件2.2.3 yml文件2.2.4 主启动类2.2.5 controller …

Linux里git的使用

git的使用 一.前置要求1.git的安装2.注册Gitee并创建仓库 二.git三板斧 一.前置要求 1.git的安装 2.注册Gitee并创建仓库 然后记住下面的网址。 之后将仓库克隆到云服务器里。记得输入gitee的账号和密码。 查看目录&#xff0c;可以发现仓库已经在目录里了。 进入目录&#xf…

python毕设课设大作业《火车票分析助手》程序

在PyCharm中运行《火车票分析助手》即可进入如图1所示的系统主界面。 图1 系统主界面 具体的操作步骤如下&#xff1a; &#xff08;1&#xff09;在主界面“车票查询”选项卡中依次输入&#xff0c;出发地、目的地以及出发时间&#xff0c;然后单击“查询”按钮&#xff0c;…

十九、Jenkins版本构建完成,触发自动化测试

十九、Jenkins版本构建完成&#xff0c;触发自动化测试 1.构建后操作-Build other projects 2.关联自动化测试工程 这样版本构建完成&#xff0c;就会执行自动化测试

金九银十跳槽涨薪Java面试题!568页真题+答案解析,大厂都在考

2023年一半又过去了&#xff0c;各大企业的招聘也又开始大量放岗了&#xff0c;各位苟着的小伙伴们要抓住机会了&#xff01; 但很多小伙伴对面试不够了解&#xff0c;不知道如何准备&#xff0c;对面试环节的设置以及目的不了解&#xff0c;尤其是面试题还很难&#xff0c;有些…

RocketMQ5.0--事务消息

RocketMQ5.0–事务消息 一、事务消息概览 RocketMQ事务消息的实现原理基于两阶段提交和定时事务状态回查来决定消息最终是提交还是回滚&#xff0c;消费者可以消费事务提交的消息&#xff0c;如下图所示。事务消息的作用&#xff1a;确保本地业务与消息在一个事务内&#xff0…

成功解决:java file outside of source root

前言 我复制一个很小项目的代码&#xff0c;然后重新命名后。用IDEA打开&#xff0c;发现.java文件的左下方有个橘色的标志。 1、问题文件 这里显示 Java file outside of source root。 查阅资料发现&#xff1a;这个问题是指Java文件不在源代码根目录之内。这可能会导致…