hibernate中双向关联在级联情况下save对象讨论

来源:互联网 发布:淘宝账户已被冻结 编辑:程序博客网 时间:2024/06/06 17:06

一般在双向关联的情况下,都要在一方设置mappedBy(name="xxx"),由对方主导映射关系。在多对一的情况下,一般在多的一方设置主导映射的关系(为了方便叙述,就这么叫它了,呵呵)。所谓主导这种映射关系,如group,user,就是由多的一方(user)里面产生一个外键参考一的一方(group)的主键,这时候user就是主导的一方,写mappedBy是被主导的一方。在多对多的情况下,随便由那方主导,在数据库表的结构上都是一样的,都会产生一个中间表,中间表有两个字段的联合主键,分别作为外键参考两个多的一方。

在一对多和多对多的双向关联的情况下,并且在cascade=CascadeType.ALL情况下,save不同方面(如主导关系一方或被主导的一方)在级联的具体表现上是不同的。分别来讨论一下。

先看一对多的双向关联关系,这里就拿group和user举例。

Group类如下:

package com.chen.hibernate.ormapping;import java.util.HashSet;import java.util.Set;import javax.persistence.CascadeType;import javax.persistence.Entity;import javax.persistence.FetchType;import javax.persistence.GeneratedValue;import javax.persistence.Id;import javax.persistence.OneToMany;import javax.persistence.Table;@Entity@Table(name = "t_group")public class Group {private int id;private String name;private Set<User> users = new HashSet<User>();@Id@GeneratedValuepublic int getId() {return id;}public void setId(int id) {this.id = id;}public String getName() {return name;}public void setName(String name) {this.name = name;}// 设置mappedBy是被主导的一方 @OneToMany(mappedBy = "group", cascade = { CascadeType.ALL }, fetch = FetchType.LAZY)public Set<User> getUsers() {return users;}public void setUsers(Set<User> users) {this.users = users;}}

User类如下:

package com.chen.hibernate.ormapping;import javax.persistence.CascadeType;import javax.persistence.Entity;import javax.persistence.FetchType;import javax.persistence.GeneratedValue;import javax.persistence.Id;import javax.persistence.JoinColumn;import javax.persistence.ManyToOne;import javax.persistence.Table;@Entity@Table(name = "t_user")public class User {private int id;private String name;private Group group;@Id@GeneratedValuepublic int getId() {return id;}public void setId(int id) {this.id = id;}public String getName() {return name;}public void setName(String name) {this.name = name;}//设置fetch为lazy,多的一方默认问eager@ManyToOne(cascade = { CascadeType.ALL }, fetch = FetchType.EAGER)@JoinColumn(name = "groupId")public Group getGroup() {return group;}public void setGroup(Group group) {this.group = group;}}

junit测试类

package com.chen.hibernate.ormapping;import org.hibernate.Session;import org.hibernate.SessionFactory;import org.hibernate.cfg.AnnotationConfiguration;import org.hibernate.tool.hbm2ddl.SchemaExport;import org.junit.AfterClass;import org.junit.BeforeClass;import org.junit.Test;public class CRUDTest {private static SessionFactory sessionFactory = null;@BeforeClasspublic static void beforeClass() {new SchemaExport(new AnnotationConfiguration().configure()).create(false, true);sessionFactory = new AnnotationConfiguration().configure().buildSessionFactory();}@Testpublic void testSaveUser() {User user = new User();user.setName("u1");Group group = new Group();group.setName("g1");// 这里仅仅把group加到user中,而不需要group.getUsers().add(user);把当前的对象加入到group的set里user.setGroup(group);Session session = sessionFactory.getCurrentSession();session.beginTransaction();session.save(user);session.getTransaction().commit();}// 再看看save被主导的一方会如何。@Testpublic void testSaveGroup() {// 先new两个user,一个group,然后把user加到group中User user = new User();user.setName("u1");User user2 = new User();user2.setName("u2");Group group = new Group();group.setName("g1");group.getUsers().add(user);group.getUsers().add(user2);// 注意,虽然两边都是级联的方式,但這裡的user也要把group设一下,不然,save(group)的时候,user表中的// groupId字段仍是nulluser.setGroup(group);user2.setGroup(group);Session session = sessionFactory.getCurrentSession();session.beginTransaction();/* * 此时在save(group)的时候,由于级联关系会先插入里面的两个user,而此时如果不写上面的 * user.setGroup(group); user2.setGroup(group); * 这两句,user对应表里面的groupId字段都将为空 */session.save(group);session.getTransaction().commit();}@AfterClasspublic static void afterClass() {sessionFactory.close();}}

先来个结论,什么时候t_user中的外键groupId才会参考t_group主键取得值?

结论:这里的关系的主导方向是User--->Group,只有当级联的方向也是从User--->Group时,才会设置这个外键的值。

这个结论可以参考这样的图:


看下实验结果,先测试运行testSaveUser方法,运行结果对于的t_group和t_user表如下,此时数据插入都是正常的。插入的过程是应该是这样的,由于save的是user,所以先插t_user表,由于t_user表和t_group表的主键id字段都是由同一个sequence产生的,所以这里由主键id字段也可以看出先是插入了t_user,t_user中有group对象且不是null(要是null的话,即使设了级联,也不会再插t_group表了,没东西插入啊!),由于User设置了级联,所以此时又插入了t_group表,因为级联的方向为User--->Group,和关系的主导方向相同,所以这里t_user表的groupId字段参考了t_group表的主键字段2。

t_user表


t_group表


在测试下testSaveGroup,若先注掉user.setGroup(group);user2.setGroup(group);这两行,也就是说就像testSaveUser中一样,只是简单的把user加到group里的集合set里,会怎样呢?测试的结果如下:

t_user表


t_group表


可以看到t_user表的groupId字段为空,为什么呢?同样按照上面的分析思路,由于save的是group,所以先插入t_group表,插入1,g1,而group对象中有一个user的集合,里面有两个user,由于级联,此时就会插入t_user表,此时级联的方向为Group--->User,这个级联方向不会设置t_user中的groupId,因为上面注释掉了user.setGroup(group);user2.setGroup(group);这两句,也就是说这两个user中的group都为null,所以在插入t_user表的时候不会产生级联再插入t_group表了,所以t_user中两条记录的groupId字段都为空。

把user.setGroup(group);user2.setGroup(group);这两句加上后,运行的结果如下,可以看到t_user表的groupId字段为1,参考了t_group的主键。这个执行的过程是这样的,先将group对象插入t_group表为1,g1,由于级联(Group--->User)会将user和user2两个对象插入t_user表,当插user对象时候,user中的group字段不是一个null(是之前的group对象),所以又会发生级联,方向为(User--->Group),此时又会将这个group对象插入到t_group表,将插入后的主键id的值设为t_user中插入user对象时的groupId的值,但是会发现这个group对象刚才已经插入过了,所以只是简单的将刚才插入group对象时的产生的主键id的值1给刚才的groupId值,最终将user插入到t_user中的记录为2,u1,1,上面的级联到重复插入那个group的时候就停止了,不会无穷的递归的进行下去。插入user2对象时,过程一样。

可以从这个过程得到两个结论:

(1)在同一个session中,同一个对象不会在数据库中插入两条不同的记录。

(2)级联停止的条件为,对象里的关联字段为null或在一个表插队的对象已经被插入过。

t_user表


t_group表


若把testSaveGroup()改为如下:

@Test                                                                    public void testSaveGroup() {                                            // 先new两个user,一个group,然后把user加到group中                                User user = new User();                                              user.setName("u1");                                                                                                                           User user2 = new User();                                             user2.setName("u2");                                                 Group group = new Group();                                           group.setName("g1");                                                 group.getUsers().add(user);                                          group.getUsers().add(user2);                                                                                                                  // 新new一个group2,把它加到user2对象里面,下面save(group)会发生什么呢?                   Group group2 = new Group();                                          group2.setName("g2");                                                user2.setGroup(group2);                                                                                                                       Session session = sessionFactory.getCurrentSession();                session.beginTransaction();                                          session.save(group);                                                 session.getTransaction().commit();                                   }                                                                        

再new一个group2,然后set到user2的group属性,这时测试会怎么样?可以设想一下,结果应该是这样的,由于save(group),所以先插入t_group表,在t_group中插入一条记录为1,g1,因为t_group中有一个集合,里面有两个user且都是非null的,所以会发生级联(方向为Group--->User),会插入这两个user,这时候到底先插入哪一个user是不一定的,因为group中是个集合,假设按照add的顺序先插user,后插user2,那么在t_user表中会先插入一条记录2,u1,?,此时插入这条记录的时候由于user中的group为null,所以不会发生级联,所以这里记录的groupId为null,所以完整的是2,u1,null。接着插入user2对象,插入的记录为3,u2,?,这里的?暂时不定,user2中的group为非null(设了group2对象,注意这个group2之前没有被插入过),所以会发生级联插入t_group表,所以在t_group表中会插入4,g2,这条记录是由插入3,u2,?级联过来的(方向为User--->Group),所以上面的?会参考这条记录的主键4,接着在t_group中插入4,g2的时候,因为这个group对象的集合set为null,所以不会发生级联,此时级联就收敛了。最终t_group和t_user表的情况应该是这样。

t_user表

2,u1,null

3,u2,4

t_group表

1,g1

4,g2

测试的结果如下,和预期是一样的。

t_user表:


t_group表:


以上分析的是一对多的双向关联,采用这种分析的思路看能不能解释多对多的双向关联的结果,做如下实验,有两个类Teacher和Student类,他们时间是多对多的关系。多对多的关系会映射成三张表,一张中间表,这里产生的三张表为teacher(id,name),student(id,name),t_s(teacher_id,student_id),teacher_id和student_id作为t_s的联合主键,其中teacher_id参考了teacher表中的主键id,student_id参考了student表的主键id字段。其中关系的主导方向为Teacher--->Student,此时中间表t_s可以看成teacher表中的一部分,只不过如果真的作为teacher表的一部分,teacher表中会多出很多冗余的字段,所以把它独立出来,只是我们可以参考一对多双向关联的情况下那样看它。所以只有级联的方向为teacher--->student方向才会插入t_s表。


Teacher类:

package com.chen.hibernate.ormapping;import java.util.HashSet;import java.util.Set;import javax.persistence.CascadeType;import javax.persistence.Entity;import javax.persistence.GeneratedValue;import javax.persistence.Id;import javax.persistence.JoinColumn;import javax.persistence.JoinTable;import javax.persistence.ManyToMany;@Entitypublic class Teacher {private int id;private String name;private Set<Student> students = new HashSet<Student>();@Id@GeneratedValuepublic int getId() {return id;}public void setId(int id) {this.id = id;}public String getName() {return name;}public void setName(String name) {this.name = name;}@ManyToMany(cascade = CascadeType.ALL)@JoinTable(name = "t_s", joinColumns = @JoinColumn(name = "teacher_id", referencedColumnName = "id"), inverseJoinColumns = @JoinColumn(name = "student_id", referencedColumnName = "id"))public Set<Student> getStudents() {return students;}public void setStudents(Set<Student> students) {this.students = students;}}
Student类:

package com.chen.hibernate.ormapping;import java.util.HashSet;import java.util.Set;import javax.persistence.CascadeType;import javax.persistence.Entity;import javax.persistence.GeneratedValue;import javax.persistence.Id;import javax.persistence.ManyToMany;@Entitypublic class Student {private int id;private String name;private Set<Teacher> teachers = new HashSet<Teacher>();@Id@GeneratedValuepublic int getId() {return id;}public void setId(int id) {this.id = id;}public String getName() {return name;}public void setName(String name) {this.name = name;}// 设置Student类为被主导的类@ManyToMany(mappedBy = "students", cascade = CascadeType.ALL)public Set<Teacher> getTeachers() {return teachers;}public void setTeachers(Set<Teacher> teachers) {this.teachers = teachers;}}


junit测试类

package com.chen.hibernate.ormapping;import org.hibernate.Session;import org.hibernate.SessionFactory;import org.hibernate.cfg.AnnotationConfiguration;import org.hibernate.tool.hbm2ddl.SchemaExport;import org.junit.AfterClass;import org.junit.BeforeClass;import org.junit.Test;public class testMany2ManyCRUD {private static SessionFactory sessionFactory = null;@BeforeClasspublic static void beforeClass() {new SchemaExport(new AnnotationConfiguration().configure()).create(false, true);sessionFactory = new AnnotationConfiguration().configure().buildSessionFactory();}@Testpublic void testSaveStudent() {//先new两个student和两个teacher,然后把两个student分别加到两个teacher里Student student = new Student();student.setName("s1");Student student2 = new Student();student2.setName("s2");Teacher teacher = new Teacher();teacher.setName("t1");Teacher teacher2 = new Teacher();teacher2.setName("t2");student.getTeachers().add(teacher);student.getTeachers().add(teacher2);student2.getTeachers().add(teacher);student2.getTeachers().add(teacher2);//把这两个teacher分别加到两个student里,这是必须的,不然中间表t_s中不会有记录for (Teacher t : student.getTeachers()) {t.getStudents().add(student);t.getStudents().add(student2);}for (Teacher t : student2.getTeachers()) {t.getStudents().add(student);t.getStudents().add(student2);}Session session = sessionFactory.getCurrentSession();session.beginTransaction();session.save(student);session.save(student2);session.getTransaction().commit();}@Testpublic void testSaveTeacher() {//new两个teacher和student,在save(teacher)时,只需把两个student分别加到两个teacher中就可以了。//中间表t_s中也有数据Teacher teacher = new Teacher();teacher.setName("t1");Teacher teacher2 = new Teacher();teacher2.setName("t2");Student student = new Student();student.setName("s1");Student student2 = new Student();student2.setName("s2");teacher.getStudents().add(student);teacher.getStudents().add(student2);teacher2.getStudents().add(student);teacher2.getStudents().add(student2);Session session = sessionFactory.getCurrentSession();session.beginTransaction();session.save(teacher);session.save(teacher2);session.getTransaction().commit();}@AfterClasspublic static void afterClass() {sessionFactory.close();}}

先运行testSaveStudent(),若先把中间的两个for循环注释掉,运行的结果如下:

student表:


teacher表:


t_s表为空,没有数据,不贴了。为什么会出现这样的结果呢?

分析一下这个执行的过程,首先save(student),所以先向student表中插入一条记录1,s1,由于student中的又一个teacher的set里有teacher和teacher2且都不为null,所以这会发生级联,方向为Student--->Teacher,这个方向主导的关系方向不同,所以不会像t_s表中试图插入数据,这时候直接把teacher对象插入到teacher表中,需要注意的是,先插入哪个对象是不定的,看上面teacher表的截图可知,先插入了teacher2,因为注释掉了那两个for循环,所以两个teacher对象里的set里面没有student,所以此时就不会发生级联了。然后以同样的过程插入了student,这样teacher表就有如图所示的两条记录,至此,save(student)就执行完了。接着还有save(student2),这时候过程和前面差不多,只不过当级联到teacher表的时候,在同一个session中,那两个teacher对象都插入过了,不会重复插入了,到此级联终止了,所以最后t_s表为空。

如果把那两个for循环打开会出现什么结果呢?

先预设一下这个结果,开始和上面一样,先save(student)时,student表中插入1,s1,由于student中有teacher和teacher2的集合,所以会级联到teacher表,假如先向teacher表中插入teacher对象,这样会插入一条2,t1的记录,因为teacher中有student和student2两个对象的集合,所以又会级联到student表,因为student对象刚才已经插入过了,所以会把student2对象插入到student表,所以此时student表中增加一条记录3,s2,又此时级联的方向是Teacher--->Student,所以会向t_s表中插入2,?,2,?,这两个?应该分别为刚才级联的两个student在student表中的主键id,所以分别为1,3,所以此时t_s表中有两条记录2,1和2,3,这时级联到teacher表的teacher对象的插入过程就完了。接着应该插入teacher2对象,这样会插入一条记录4,t2,因为teacher2中也有student和student2对象,所以会发生级联,此时这两个对象在student表中都被插入过了,不会在student表中插入新的记录,由于级联的方向是Teacher--->Student,所以会在t_s表中插入记录4,?,4,?,这两个?分别为刚才级联的两个student对象在student表中的主键id,所以分别为1,3,所以t_s表中又增加两条记录4,1,4,3。至此,save(student)就完成了,接着还有save(student2),首先会向student表中插入student2对象,但是此时student2已经被插入过了,所以这里要不要这条语句结果都是一样的。

所有最后的情况如下:

student表:

1,s1

3,s2

teacher表:

2,t1

4,t2

t_s表:

2,1

2,3

4,1

4,3
结果截图如下,和分析一致。

student表:


teacher表:


t_s表:


testSaveTeacher()过程比上面要简单,按照同样的方法分析就可以了,不再分析了。

结论:上面分析的过程虽然看上复杂但是,最后的结论是简单的,就是若想再save对象的时候能够将想关联的表和字段都设置为正确的值,而不是null,应该注意,在关系主导的一方,只需要将里面的所有字段设置上就可以了,如这里在testSaveTeacher()中,只需要将对象user加到teacher对象的集合中,而不需要反过来再将teacher加到user对象的teacher集合中。而在关系被主导的一方(Student),如在testSaveStudent()中,若想将关联的表和字段都正确的设置,而不是null,除了要将teacher对象加到student对象的teacher集合中,也要反过来将student对象加到teacher对象的student集合中。



原创粉丝点击