Hibernate:关联entity:加载 / 改动 / 删除

更多
2021年10月13日 17点02分 作者:叶飞 修改 解锁

为了便于表达,我们先对entity里面要持久化的字段/属性做一个定义:


简称
定义
示例(当前entity:Studnet)
关联属性

对另一个@Entity的引用
Student taughtBy
关联集合
集合
对其他entites集合的引用
Set<Student> teachers
基本属性
属性
Java基本类型+String+时间类型等
int age、String name
ValueType

对另一个@embedable的引用
Contact contact
另:为演示方便,再引入Score和Bed类,自此:

  • 多对多:Student和Teacher
    @ManyToMany(mappedBy = "students")
    private Set<Teacher> teachers = new HashSet<>();
    
    @ManyToMany
    private Set<Student> students = new HashSet<>();
  • 一对多:Student和Score
    @OneToMany(mappedBy = "candidate")
    private List<Score> scores  = new ArrayList<>();
    
    @ManyToOne
    private Student candidate;
  • 一对一:Student和Bed
    @OneToOne(mappedBy = "sleepedBy")
    private Bed bed;
    
    @OneToOne
    private Student sleepedBy;
均设置为双向,且Student为inverse端。
Student atai = new Student("atai", true, 17, DayOfWeek.MONDAY, LocalDate.of(2011, 5, 1),
		new Contact("atai@qq.com", "178654321"), "我是阿泰,泰山的泰,^_^");
Student bo = new Student("bo", true, 16, DayOfWeek.TUESDAY, LocalDate.of(2011, 6, 1),
		new Contact("bo@qq.com", "158654322"), "我是波仔,鱼仔的仔");
Student lang = new Student("lang", false, 21, DayOfWeek.FRIDAY, LocalDate.of(2011, 5, 18),
		new Contact("lang@qq.com", "148654323"), "我是浪神,女神的神……");

Teacher fg = new Teacher("fg");
Teacher xy = new Teacher("xy");

Score aSQL = new Score("SQL",atai, 85.0f);
Score aJava = new Score("Java",atai, 90.0f);
Score bSQL = new Score("SQL",bo, 85.0f);
Score bJava = new Score("Java",bo, 78.5f);
Score bJavascript = new Score("JavaScript",bo, 82.5f);
Score lSQL = new Score("SQL",lang, 78.0f);
Score lCSharp = new Score("CSharp",lang, 86.0f);
Score lJava = new Score("Java",lang, 55.5f);		

Bed aBed = new Bed("闭包间", atai); Bed lBed = new Bed("二叉树", lang);

fg.getStudents().add(atai);
fg.getStudents().add(bo);

xy.getStudents().add(bo);
xy.getStudents().add(lang);
em.persist(fg);	
em.persist(xy); em.persist(bo);
em.persist(atai);
em.persist(lang);

em.persist(aBed);
em.persist(lBed);
em.persist(aSQL);
em.persist(aJava);
em.persist(bSQL);
em.persist(bJava);
em.persist(bJavascript);
em.persist(lJava); 
em.persist(lCSharp);
em.persist(lSQL);


加载

默认的,当我们加载一个entity的时候,除了集合以外,它的其他

  • 基本属性、关联属性和ValueType都会自动加载(可以通过bytecode enhancement,略,但推荐了解
  • 集合会延迟/懒惰加载

@想一想@:关联属性怎么及时加载?log演示:使用JOIN

Student atai = em.find(Student.class, 1);   //SELECT ???

//基本属性
System.out.println(atai.getAge());
System.out.println(atai.getName());
System.out.println(atai.getEnroll());

//ValueType
Contact contact = atai.getContact();
System.out.println(contact.getEmail());

//关联属性
Bed bed = atai.getBed();  
System.out.println(bed.getId());   
System.out.println(bed.getName());

//集合:以下那一句语句执行时会查询数据库?
System.out.println(atai.getTeacher() == null);
Set<Teacher> teachers = atai.getTeacher();
System.out.println(teachers.size());
for (Teacher teacher : teachers) {
	System.out.println(teacher.getName());
}

但关联属性和集合的加载方式可以通过在注解中设置属性值fetch予以改变:

@ManyToMany(mappedBy = "students", fetch = FetchType.EAGER)

log演示:SELECT中主动的JOIN

但是,当出现一对一的双向链接时,如果我们是从inverse的一段设置到owner的关联lazyload

@OneToOne(mappedBy = "sleepedBy", fetch = FetchType.LAZY)
private Bed bed;
我们的自定义就会失效。

log演示:首次加载就出现两次SELECT,第一次SELECT不涉及bed,第二次就JOIN Bed……

@想一想@:为什么呢?

因为lazyload需要Id!Student表里没有Bed的Id,Hibernate无法构建proxy对象。

演示:从Bed到Student就可以lazyload。


添加

已有两个entity,我们要在其间添加关联。

注意:如之前演示,双向引用一定要在owner一端添加(实际开发中就总是在两端添加即可)

一对一

直接进行设置就可以了

Student lang = em.find(Student.class, 3);
Bed bed = em.find(Bed.class, 2);
			
//合乎双向关联规则的(可省略)
lang.setBed(bed);
//起决定作用的
bed.setSleepedBy(lang);

本质上是一条UPDATE语句,但我们却额外执行了两条SELECT语句,@想一想@:是不是不值?

这里使用session.load()或em.getReference()也没有用,因为:所有的lazyload,在使用setter的时候必然查询数据库(演示:略)

但是,如果省略掉lang.setBed(bed);可以少一个有关lang的SELECT语句。

多对多

Teacher fg = em.getReference(Teacher.class, 1);
Student lang = em.getReference(Student.class, 3);
//会导致通过JOIN一次性的加载出整个students集合
fg.getStudents().add(lang);		
//inverse端,可省略
lang.getTeachers().add(fg);

一样出现了没有必要的SELECT。

而且在加载students的时候,会依次查询Student的Bed(因为关联属性引用)

@想一想@:如果是重复的添加,会有什么样的结果?

一对多

实际开发中通常都是在生成一(端entity)的同时就建立了一对多的关联并持久化。以下仅为演示用:

先准备一个“无主的”Score:

Score lHtml = new Score();
lHtml.setName("HTML");
em.persist(lHtml);
em.flush();
再添加关联关系:
Student lang = em.getReference(Student.class, 3);
//因为setter,会SELECT...FROM Student
lHtml.setCandidate(lang);
//这里“不”会额外SELECT哟,赞!
//inverse端,可省略
lang.getScores().add(lHtml);
//生成UPDATE语句
em.flush();


性能优化考虑

如果从性能优化的角度考虑,要慎用(不是说不能用):

一对一的关联

首先考虑是不是可以用ValueType代替,因为ValueType的加载不需要外键JOIN。

然后考虑是不是可以使用一对多替换,这倒无关性能,而是为未来扩展提供可能性。比如现在一个学生一张床,但以后呢?源栈同学出栈入栈,一定会形成一(张床)对多(个学生)的关系。

实际上,必须要用一对一关联,而不能使用ValueType或一对多关系的例子是非常非常少的。

双向引用

首先,要搞清楚owner端和inverse端非常麻烦(尤其是没有annotation,而是xml配置的时候);

不想搞清楚,蒙着头写,就得两端同步,造成性能上的损耗。

尤其是一对一的双向引用:默认采用eager load模式,所以inverse的一端始终无法proxy load,就会JOIN查询,非常没有必要。

集合

虽然有lazyload,但一旦使用到getter/setter,也非常容易触发SELECT:
  • 首先是数据量大:会把所有数据不加过滤的全部加载。
  • 在某些场景下,修改集合元素还会出现“重建”(即DELETE所有之后再ADD)

所以每当你要引入集合的时候,仔细思考:有无必要?是不是entity不可或缺的一部分?比如:汽车的轮胎,就是不可或缺,缺了汽车没法跑;老师的学生就不一定是不可或缺的,没了学生的老师还可以是老师。具体到代码层面,就是没有这个集合,我这个entity就没办法实现某个功能,那这个集合就是必须的,比如老师有一个点名的功能……

你清楚的知道这样做会带来的问题,仔细权衡之后你仍然觉得这样做,那一般来说都是OK的。

拆分多对多

引入一个关系表对应的entity:

@Entity
public class Student2Teacher implements Serializable {
	@Id
	@ManyToOne
	private Student student;
	@Id
	@ManyToOne
	private Teacher teacher;
说明:
  • Hibernate要求必须有@Id(PrimaryKey),
  • 在多个列上标记@Id会生成联合主键
  • 组合@Id必须实现Serializable

Student和Teacher分别反向(inverse)引用这个关系entity:

@OneToMany(mappedBy = "student")
private Set<Student2Teacher> teachers = new HashSet<>();
@OneToMany(mappedBy = "teacher")
private Set<Student2Teacher> students = new HashSet<>();

添加关联时之前的:

fg.getStudent().add(atai);
就要变成:
em.persist(new Student2Teacher(atai, fg));
于是我们就可以独立的操作这个关系entity(比如INSERT)

log演示:仅仅在flush()时生成一条INSERT语句


更改和删除

以下不深究SELECT相关的性能,只关注:

  • 如何用面向对象的方法表示
  • 数据库上实际发生的变化

单个元素

直接更改关联属性为新的entity或者NULL(删除)即可。例如:
  • 一对一:学生lang(id=3)的床变成atai(id=1)的
    Student lang = em.find(Student.class, 3);
    //注意owner和inverse端的区别
    lang.getBed().setSleepedBy(em.find(Student.class, 1));;	
    update Bed set name=?, sleepedBy_id=? where id=?
    binding parameter [2] as [INTEGER] - 1
    
  • 一对多:把某成绩从属于atai的改为不再属于atai
    Score score = em.find(Score.class, 1);
    score.setCandidate(null);	
    update Score set name=?, candidate_id=?, point=? where id=?
    binding parameter [2] as [INTEGER] - [null]

集合

从面向对象的角度,集合也可以替换,比如:

或者,改变集合里面的单个元素?那就集合里面的改动只能是删(remove)了再加(add)。但:

  • 一对多的时候,owner始终在非集合引用的那一端,仅在inverse端集合上操作是不会影响数据库的:
    Student lang = em.find(Student.class, 3);
    lang.getScores().remove(0);
    断点演示:t1.getStudents()集合元素已经减少,但无法生成SQL语句……
    说明:以上系JPA规范,不是每一个ORM工具严格遵守
  • 一对多的时候,在owner一端remove,
    Student atai = em.find(Student.class, 1);
    Teacher fg = em.find(Teacher.class, 1);
    fg.getStudents().remove(atai);
    理解:atai不再是fg的学生,fg不再是atai的老师,不能说atai就没了……


级联cascade

在Hibernate里面,也可以通过annotation进行设置。


保存

比如给某个床位设定一个Student:

em.find(Bed.class, 1).setSleepedBy(
		new Student());

如果没有cascade:

Caused by: org.hibernate.TransientPropertyValueException: object references an unsaved transient instance - save the transient instance before flushing : Bed.sleepedBy -> Student

所以需要设置:

@OneToOne(cascade = CascadeType.PERSIST)
private Student sleepedBy;

演示:生成INSERT Student语句


删除

演示:当没有设置cascade的时候,删除一个老师

@想一想@:删除Bed和Score呢?会不会造成cascade?

删除一个学生呢?


Student lang = em.find(Student.class, 1);			
em.remove(lang);

报错:

Caused by: com.mysql.jdbc.exceptions.jdbc4.MySQLIntegrityConstraintViolationException: Cannot delete or update a parent row: a foreign key constraint fails (`17bang`.`teacher_student`, CONSTRAINT `FK_Teacher_Student_students_id_Student` FOREIGN KEY (`students_id`) REFERENCES `student` (`id`)复习:SQL中自带的cascade功能设置

理解:cascade本质上还是以外键约束为基础的。但Hibernate喜欢使用这种表述:删除父类对象的时候,如何处理子类对象……

所以需要设置:

@ManyToMany(mappedBy = "students", cascade = CascadeType.REMOVE)
private Set<Teacher> teachers = new HashSet<>();

演示:还需要继续设置所有相关联的Bed和Score

对于多对多,

如果使用CascadeType.REMOVE,会把这个学生所有的老师一起删除

必须要从owner一端来删除



PS:还有其他的CascadeType,用得不多:略


orphanRemoval

成为孤儿时自动删除,在JPA 2.0中引入。

orphanRemoval是基于Hibernate的:只要有一个entity,没有其他entity引用它,就予以删除。


演示:把Bed的SleepBy设置成NULL


当我们从运行效果上来看,它和CascadeType.REMOVE非常类似,但实际上它和cascade没有关系。

cascade是基于数据库(外键约束)的,


再次总结

事实上,这一章节的内容,是非常容易被滥用的。

稍有不慎,它就会:

  • 偷走你的性能:双向引用和集合额外的SELECT
  • 悄悄删除一些不应该删除的数据:@ManyToMany的CascadeType.REMOVE
  • 或者留下垃圾数据:orphan

同学们在使用的时候,务必小心小心再小心。


作业


关联对象 加载 删除 修改
赞: 0 踩: 0

打赏
已收到打赏的 帮帮币

你的 打赏 非常重要!
为了保证文章的质量,每一篇文章的发布,都已经消耗了作者 1 枚 帮帮币
没有“帮帮币”,作者无法发布新的文章。

全系列阅读
评论 / 0

持久化


ADO&EF

如何通过C#进行数据库的读取,包含ADO.NET和Entity Framework相关知识……

JDBC&Hibernate

Java连接数据库操作,包括JDBC、Hibernate和mybatis等

全部
关键字



帮助

反馈