俗话说,自己写的代码,6个月后也是别人的代码……复习!复习!复习!涉及的知识点总结如下:
-
One to Many 映射关系
- 多对一单向外键关联(XML/Annotation)
- 一对多单向外键关联(XML/Annotation)
- 懒加载和积极加载
- 一对多双向外键关联(XML/Annotation)
-
Many to Many 映射关系
- 多对多单向外键关联(XML/Annotation)
- 多对多双向外键关联(XML/Annotation)
- set的inverse元素详解
- 问题小结
- 关联关系的优缺点
多对一单向外键关联关系
注意多对一关联是多方持有一方的引用。看一个例子,去淘宝购物,那么一个淘宝用户可以对应多个购物订单,如图所示:
多的一方是Orders,持有一方的引用,也就是Users,而在Users中无需作任何定义,从订单到用户的关系是单向多对一关联。对应数据库就是:
还有比如说学生和班级的关系,多个学生可以属于同一个班级,这就是从学生到班级也是典型的单向多对一关系,看代码实现:
基于注解的多对一单向外键关联:
单向多对一关联中,多方需要持有一方的引用,那么多方(学生类)需要额外配置,需要对持有的一方引用使用注解@ManyToOne (cascade={CascadeType.ALL}, fetch=FetchType.EAGER),设置为级联操作和饥渴的抓取策略,@JoinColumn(name="cid"),而一方(教室类)无需做任何多方的定义。
注意;多方必须保留一个不带参数的构造器!
import javax.persistence.Entity; import javax.persistence.GeneratedValue; import javax.persistence.Id; //班级类,在多对一关系中属于一的方,不持有其他多余的配置,反而是被多方持有 @Entity public class ClassRoom { private int cid;//班级编号 private String cname;//班级名称 // 自动增长的主键 @Id @GeneratedValue public int getCid() { return cid; } public void setCid(int cid) { this.cid = cid; } public String getCname() { return cname; } public void setCname(String cname) { this.cname = cname; } }
一方——班级类无需做多余的定义,下面是多方——学生实体和配置:
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; //学生实体类,属于多对一的多方,持有班级(一方)的引用 @Entity public class Students { private int sid; //编号 private String sname; //姓名 private ClassRoom classroom;//学生班级 //注意:多方一定要显式的定义不带参数的构造方法 public Students() { } public Students(String sname) { this.sname = sname; } // 多方使用注解:@ManyToOne // fetch=FetchType.EAGER,急加载,加载一个实体时,定义急加载的属性会立即从数据库中加载。 // 全部级联操作,referencedColumnName显式设置数据库字段名cid,不写默认就是和name一样的。 @ManyToOne (cascade={CascadeType.ALL}, fetch=FetchType.EAGER) @JoinColumn(name="cid",referencedColumnName="cid") public ClassRoom getClassroom() { return classroom; } public void setClassroom(ClassRoom classroom) { this.classroom = classroom; } // 自动增长主键 @Id @GeneratedValue public int getSid() { return sid; } public void setSid(int sid) { this.sid = sid; } public String getSname() { return sname; } public void setSname(String sname) { this.sname = sname; } }
下面测试:先生成数据库脚本,再进行学生对象的插入
public class TestStudentsByAnno { private static SessionFactory sessionFactory; @Before public void setUp() throws Exception { System.out.println("setUp()..."); sessionFactory = new AnnotationConfiguration().configure().buildSessionFactory(); } @After public void tearDown() throws Exception { System.out.println("tearDown()..."); sessionFactory.close(); } @Test public void testSave() { Session session = sessionFactory.getCurrentSession(); Transaction tx = session.beginTransaction(); try { ClassRoom c = new ClassRoom(); c.setCname("computer001"); Students s = new Students("zhangsan"); s.setClassroom(c); session.save(s); tx.commit(); } catch(Exception ex) { ex.printStackTrace(); tx.rollback(); } } @Test @Ignore public void testSchemaExport() { SchemaExport se = new SchemaExport(new AnnotationConfiguration().configure()); se.create(true, true); } }
反向创建表的数据库脚本如下:
create table ClassRoom (cid integer not null auto_increment, cname varchar(255), primary key (cid))
create table Students (sid integer not null auto_increment, sname varchar(255), cid integer, primary key (sid))
插入一个学生对象,会自动生成如下语句:
ClassRoom c = new ClassRoom(); c.setCname("computer001"); Students s = new Students("zhangsan"); s.setClassroom(c); session.save(s); tx.commit();
Hibernate: insert into ClassRoom (cname) values (?)
Hibernate: insert into Students (cid, sname) values (?, ?)
插入成功:
基于xml配置实现多对一单向外键关联
<class name="net.nw.vo.fk.mto.ClassRoom" table="classroom"> class="native"/> class>
一方(教室类)无需做任何多方的定义。只需要维护好自己的属性配置即可。而多方只需要加上
<class name="net.nw.vo.fk.mto.Students" table="students"> class="native"/> class>
hibernate.cfg.xml里加上
注意:如果没有设置级联ALL,那么需要在保存的时候先保存班级,在保存学生,否则出错: object references an unsaved transient instance - save the transient instance before flushing:
ClassRoom classRoom = new ClassRoom(); classRoom.setCname("CS"); Students students = new Students("111"); students.setClassroom(classRoom); session.save(classRoom); session.save(students); tx.commit();
小结:使用
一对多单向外键关联
当类与类建立了关联,程序能很方便的从一个对象导航到另一个或一组与之关联的对象,有了student对象,就可以通过student对象得到这个学生所属的班级的信息——students.getClassroom();,对于班级对象,如果想要得到某个学生的信息,怎么办呢?这时候可以反过来控制,一方控制多方,下面进行一对多单向外键关联。
简单说就是和之前多对一相反,之前是多方持有一方的引用,而一对多关联关系是一方持有多方的集合的引用,注意区别:这里是持有多方的集合。
基于注解的配置:
@OneToMany(cascade={CascadeType.ALL},fetch=FetchType.LAZY),@JoinColumn(name=""),除了级联之外,还要设置一方为懒加载模式。且外键还是加在了多方学生表里,只不过控制权变了,之前多对一关联是多方学生持有班级外键,控制班级,现在一对多关联,表里还是多方学生持有班级一方的外键,只不过控制权交给了班级,让班级控制学生。不要混淆。
import javax.persistence.*; import java.util.Set; //班级类是一方,一方持有多方的引用 @Entity public class ClassRoom { private int cid;//班级编号 private String cname;//班级名称 private Setstus ;//班级的学生集合是多方 // 现在是一方维护多方了,主控权交给了一方,设置级联,一方要设置懒加载,推荐! @OneToMany(cascade={CascadeType.ALL},fetch=FetchType.LAZY) @JoinColumn(name="cid") // 设置一方的外键,这里是cid,因为实际上这个外键还是加在多方,只不过控制权变了。 public Set getStus() { return stus; } public void setStus(Set stus) { this.stus = stus; } @Id @GeneratedValue public int getCid() { return cid; } public void setCid(int cid) { this.cid = cid; } public String getCname() { return cname; } public void setCname(String cname) { this.cname = cname; } }
注意,不论多对一还是一对多,多方都要显式保留无参构造器。
import javax.persistence.Entity; import javax.persistence.GeneratedValue; import javax.persistence.Id; //学生实体类 @Entity public class Students { private int sid; //编号 private String sname; //姓名 //注意:一定要保留这个默认不带参数的构造方法 public Students(){ } public Students(String sname) { this.sname = sname; } @Id @GeneratedValue public int getSid() { return sid; } public void setSid(int sid) { this.sid = sid; } public String getSname() { return sname; } public void setSname(String sname) { this.sname = sname; } }
执行数据库脚本,发现一方(主控方)还是和之前多对一的表结构一样,多方也是如此。
create table ClassRoom (cid integer not null auto_increment, cname varchar(255), primary key (cid))
create table Students (sid integer not null auto_increment, sname varchar(255), cid integer, primary key (sid))
执行测试,保存学生,因为现在关系是一方维护,控制多方。肯定保存主控方——班级(和之前相反,之前多对一保存的是多方学生对象),但是本质上还是先保存的学生班级,再自动保存学生,这点和多对一本质一样。
Setstus = new HashSet<>(); stus.add(new Students("zhangsan")); stus.add(new Students("lisi")); stus.add(new Students("wangwu")); stus.add(new Students("zhaoliu")); stus.add(new Students("sunqi")); ClassRoom c = new ClassRoom(); c.setCname("cs001"); c.setStus(stus); session.save(c); tx.commit();
生成的脚本如下:先插入外键的班级对象,在执行五个学生的插入操作,最后执行五个更新,为sid=1。。。5的学生,更新cid为2
Hibernate: insert into ClassRoom (cname) values (?) Hibernate: insert into Students (sname) values (?) Hibernate: insert into Students (sname) values (?) Hibernate: insert into Students (sname) values (?) Hibernate: insert into Students (sname) values (?) Hibernate: insert into Students (sname) values (?) Hibernate: update Students set cid=? where sid=? Hibernate: update Students set cid=? where sid=? Hibernate: update Students set cid=? where sid=? Hibernate: update Students set cid=? where sid=? Hibernate: update Students set cid=? where sid=?
总结:多对一时候,多方设置EAGER,一方设置LAZY,也就是说,如果是多对一,多方控制一方,那么多方设置积极加载,一方无需多余配置,反过来,如果是一对多关系,一方控制多方,那么一方设置懒加载,多方无需多余配置,但是不论哪种,多方都显式加上一个不带参数的构造器。
一对多里的懒加载
记得之前总结,get和load的查询方式源码的时候,就总结了一下懒加载load里的应用,之前说Hibernate中,当访问的数据量过大时,用缓存也不太合适, 因为内存容量有限 ,为了减少并发量,减少系统资源的消耗,Hibernate用懒加载机制来弥补这种缺陷,但是这只是弥补而不是用了懒加载总体性能就提高了。懒加载也被称为延迟加载,它在查询的时候不会立刻访问数据库,而是返回代理对象,比如之前总结的load方式查询,当真正去使用对象的时候才会访问数据库。除了load查询默认使用懒加载,现在我们的一对多关联映射也使用了lazy加载,下面进行实际验证测试:
public void testQuery() { Session session = sessionFactory.getCurrentSession(); Transaction tx = session.beginTransaction(); try { // 首先查询班级,cid=1的班级 ClassRoom c =(ClassRoom) session.get(ClassRoom.class, 1); // 通过班级导航到学生,遍历学生得到名字 for(Students s : c.getStus()) { System.out.println("姓名 :" + s.getSname()); } tx.commit(); } catch(Exception ex) { ex.printStackTrace(); tx.rollback(); } }
执行之后,debug发现:在没有使用班级对象的时候,只有这样一条SQL语句:从classroom表查询,cid=1的班级,使用使用AS赋给列一个别名。
Hibernate: select classroom0_.cid as cid0_0_, classroom0_.cname as cname0_0_ from ClassRoom classroom0_ where classroom0_.cid=?
等执行到for了,才打印这一语句:
Hibernate: select stus0_.cid as cid0_1_, stus0_.sid as sid1_, stus0_.sid as sid1_0_, stus0_.sname as sname1_0_ from Students stus0_ where stus0_.cid=?
充分说明这是执行的懒加载模式。一方控制多方,一方设置懒加载,如果什么都不设置,会是什么情况?经过验证,发现和显式设置懒加载效果一样,也就是说,one-to-many(元素)的懒加载是默认的,这是必须的,是常用的策略。一对多的时候,查询主对象时默认是懒加载。即:查询主对象的时候不会把从对象查询出来,使用从对象的时候才加载从对象。
如果人为设置为积极加载,则直接全部查询,@OneToMany(cascade={CascadeType.ALL},fetch=FetchType.EAGER),SQL语句为如下,进行了外连接的查询。一次性查了主对象和从对象出来。
Hibernate: select classroom0_.cid as cid0_1_, classroom0_.cname as cname0_1_, stus1_.cid as cid0_3_, stus1_.sid as sid3_, stus1_.sid as sid1_0_, stus1_.sname as sname1_0_ from ClassRoom classroom0_ left outer join Students stus1_ on classroom0_.cid=stus1_.cid where classroom0_.cid=?
基于xml文件配置:
一方作为主控方:
class= "" />
一方是班级,持有多方的集合,如下配置:
<class name="net.nw.vo.fk.otm.ClassRoom" table="classroom"> class="native"/> class> class="net.nw.vo.fk.otm.Students" />
多方是学生,作为从对象,别忘了,保留无参构造器,如下配置:
<class name="net.nw.vo.fk.otm.Students" table="students"> class="native"/> class>
一对多双向外键关联
其实类似之前的一对一双向外键关联,也是互相持有对方的引用,故也叫双向一对多自身关联。多方持有一方的引用,@ManyToOne(cascade={CascadeType.ALL}),@JoinColumn(name="")。反过来,一方也持有多方的集合,@OneToMany(cascade={CascadeType.ALL}),@JoinColumn(name="")。代码如下:
基于注解的配置:
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; //学生实体类,属于多方,持有一方的引用 @Entity public class Students { private int sid; //编号 private String sname; //姓名 private ClassRoom classroom;//学生班级属于一方 //注意:一定要在多方保留这个默认不带参数的构造方法 public Students() { } public Students(String sname) { this.sname = sname; } // 多方是设置积极加载,全部级联 @ManyToOne (cascade={CascadeType.ALL}, fetch=FetchType.EAGER) @JoinColumn(name="cid",referencedColumnName="cid") // 外键设置为班级id,cid public ClassRoom getClassroom() { return classroom; } public void setClassroom(ClassRoom classroom) { this.classroom = classroom; } @Id @GeneratedValue public int getSid() { return sid; } public void setSid(int sid) { this.sid = sid; } public String getSname() { return sname; } public void setSname(String sname) { this.sname = sname; } }
关键是一方,也必须持有多方的集合,形成你中有我,我中有你的局面,互相控制。但是还是注意,本质上,数据库表里外键cid还是加在了学生表——多方的表里。
import javax.persistence.*; import java.util.Set; //班级类 @Entity public class ClassRoom { private int cid;//班级编号 private String cname;//班级名称 private Setstus; // 一方也持有了多方:学生的集合引用 // 一方也要控制多方,一方设置懒加载,外键还是cid,也就是外键还是加在多方——学生表。 @OneToMany(cascade={CascadeType.ALL},fetch=FetchType.LAZY) @JoinColumn(name="cid") public Set getStus() { return stus; } public void setStus(Set stus) { this.stus = stus; } @Id @GeneratedValue public int getCid() { return cid; } public void setCid(int cid) { this.cid = cid; } public String getCname() { return cname; } public void setCname(String cname) { this.cname = cname; } }
测试脚本生成。和之前表一样,只不过控制权双方都有了:
alter table Students drop foreign key FK73AC29B8559B6D03 drop table if exists ClassRoom drop table if exists Students create table ClassRoom (cid integer not null auto_increment, cname varchar(255), primary key (cid)) create table Students (sid integer not null auto_increment, sname varchar(255), cid integer, primary key (sid)) alter table Students add index FK73AC29B8559B6D03 (cid), add constraint FK73AC29B8559B6D03 foreign key (cid) references ClassRoom (cid)
此时先保存谁都可以!控制权是双方都有。
基于xml配置:
class=""/>
本例代码如下:
<class name="net.nw.vo.bfk.mto.Students" table="students"> ----------------------------------------------------------------------------------class="native"/> class> <class name="net.nw.vo.bfk.mto.ClassRoom" table="classroom"> class="native"/> class> class="net.nw.vo.bfk.mto.Students" />
小结:在关系模型中,只存在外键参照关系,而且是many方参照one方。
多对多的关联关系映射
现在有一个角色类,和一个特权类,前者保存了都有哪些人(角色)拥有哪些特权,后者保存的是一些特权,比如可以做什么,不可以做什么等让哪些人拥有。如图类关系:
这就是一个多对多的例子,他们之间在数据库如何实现的关联呢?显然不能互相持有对方主键做外键,那么就需要用到一个新的表——映射表作为中间表:
再举一个最熟悉的学生的例子,现实中,学生和教师就构成了多对多的关联关系。一个教师可以教很多学生,同时一个学生可以师从很多老师,拿这个例子说明。
多对多单向外键关联
import javax.persistence.*; import java.util.Set; //学生实体类 @Entity public class Students { private int sid; //编号 private String sname; //姓名 private Setteachers ; // 我设置学生这个多方去持有老师这个多方的集合,去控制老师 //注意:一定要保留这个默认不带参数的构造方法 public Students() { } public Students(String sname) { this.sname = sname; } // 先设置多对多的关联,之后必须生成一个中间表,使用JoinTable注解 @ManyToMany(cascade=CascadeType.ALL) @JoinTable( // 设置中间表名 name="teachers_students", // 指定当前对象的外键,本表在中间表的外键名称 joinColumns={@JoinColumn(name="sid")}, // 指定关联对象的外键,另一个表在中间表的外键名称。 inverseJoinColumns={@JoinColumn(name="tid")} ) public Set getTeachers() { return teachers; } public void setTeachers(Set teachers) { this.teachers = teachers; } @Id @GeneratedValue public int getSid() { return sid; } public void setSid(int sid) { this.sid = sid; } public String getSname() { return sname; } public void setSname(String sname) { this.sname = sname; } }
另一个多方,老师不做多余配置:
import javax.persistence.Entity; import javax.persistence.GeneratedValue; import javax.persistence.Id; @Entity public class Teachers { private int tid;//教师的编号 private String tname;//教师姓名 public Teachers() { } public Teachers(String tname) { this.tname = tname; } @Id @GeneratedValue public int getTid() { return tid; } public void setTid(int tid) { this.tid = tid; } public String getTname() { return tname; } public void setTname(String tname) { this.tname = tname; } }
生成的数据库脚本如下:
create table Students (sid integer not null auto_increment, sname varchar(255), primary key (sid))
create table Teachers (tid integer not null auto_increment, tname varchar(255), primary key (tid))
create table teachers_students (sid integer not null, tid integer not null, primary key (sid, tid))
主要关注中间表,sid和tid都是作为了中间表的联合主键,他们同时也是外键:
// 因为学生持有教师的集合,先设置教师 Setteachers = new HashSet<>(); teachers.add(new Teachers("Wang")); teachers.add(new Teachers("Li")); teachers.add(new Teachers("Song")); teachers.add(new Teachers("Zhang")); Students s = new Students(); s.setSname("zhangsan"); s.setTeachers(teachers); session.save(s); tx.commit();
基于xml的多对多单向外键关系配置:
学生这个多方持有老师的集合,那么持有对方集合的学生映射文件配置如下:
<class name="net.nw.vo.fk.mtm.Students" table="students"> class="native"/> class> class= "net.nw.vo.fk.mtm.Teachers" column="tid"/>
老师表配置就简单了:
<class name="net.nw.vo.fk.mtm.Teachers" table="teachers"> class="native"/> class>
进行测试(删除之前的表,先删除中间表,在删除老师表,最后删除学生表):
Setteachers = new HashSet<>(); teachers.add(new Teachers("Teacher Wang")); teachers.add(new Teachers("Teacher Li")); teachers.add(new Teachers("Teacher Song")); teachers.add(new Teachers("Teacher Zhang")); Students s = new Students(); s.setSname("zhangsan"); s.setTeachers(teachers); session.save(s); tx.commit();
多对多双向外键关联
和之前的类似,是互相持有对方的集合,双方持有对方的集合对象,其中一方设置@ManyToMany(mappedBy=""),另一方:
@ManyToMany @JoinTable( name="", joinColumns={@JoinColumn(name="")}, inverseJoinColumns={@JoinColumn(name="")} )
基于注解的配置,看具体代码:
import javax.persistence.*; import java.util.Set; //学生实体类 @Entity public class Students { private int sid; //编号 private String sname; //姓名 private Setteachers ; //注意:一定要保留这个默认不带参数的构造方法 public Students() { } public Students(String sname) { this.sname = sname; } @ManyToMany(cascade=CascadeType.ALL) @JoinTable( name="teachers_students", joinColumns={@JoinColumn(name="sid")}, inverseJoinColumns={@JoinColumn(name="tid")} ) public Set getTeachers() { return teachers; } public void setTeachers(Set teachers) { this.teachers = teachers; } @Id @GeneratedValue public int getSid() { return sid; } public void setSid(int sid) { this.sid = sid; } public String getSname() { return sname; } public void setSname(String sname) { this.sname = sname; } }
关键是另一方的配置,前面总结了,双向关联不会真的是互相维持,只能交给一方去维护:
import javax.persistence.Entity; import javax.persistence.GeneratedValue; import javax.persistence.Id; import javax.persistence.ManyToMany; import java.util.Set; @Entity public class Teachers { private int tid;//教师的编号 private String tname;//教师姓名 private Setstus ; public Teachers() { } public Teachers(String tname) { this.tname = tname; } // 把控制权交给student类——teachers集合引用 @ManyToMany(mappedBy="teachers") public Set getStus() { return stus; } public void setStus(Set stus) { this.stus = stus; } @Id @GeneratedValue public int getTid() { return tid; } public void setTid(int tid) { this.tid = tid; } public String getTname() { return tname; } public void setTname(String tname) { this.tname = tname; } }
生成数据库脚本:(和之前的单向多对多一样的表结构,关键看实体中控制权的变化)
create table Students (sid integer not null auto_increment, sname varchar(255), primary key (sid))
create table Teachers (tid integer not null auto_increment, tname varchar(255), primary key (tid))
create table teachers_students (sid integer not null, tid integer not null, primary key (sid, tid))
基于xml的配置:
class="net.nw.vo.Teachers" column="tid"/>
另一方:
class="net.nw.vo.Students" column="sid"/>
具体代码:
<class name="net.nw.vo.bfk.mtm.Students" table="students"> ---------------------------------------------------------------------class="native"/> class> class= "net.nw.vo.bfk.mtm.Teachers" column="tid"/> <class name="net.nw.vo.bfk.mtm.Teachers" table="teachers"> class="native"/> class> class= "net.nw.vo.bfk.mtm.Students" column="sid"/>
注意:set元素配置;
属性name 指定类的属性名,table指定多对多关联关系中间表,cascade 级联操作属性:save-update、delete、all、none,一般all就ok,lazy属性可以指定是否是懒加载。set的子元素key元素——设定本表在中间表的外键名称。
inverse属性设置:
关联关系的优缺点
问题小结
- 注意在多对一/一对多关系里:多方必须保留一个不带参数的构造器!
- 如果没有设置级联ALL,那么需要在保存的时候先保存班级,在保存学生,否则出错: object references an unsaved transient instance - save the transient instance before flushing:
- 多对一时候,多方设置EAGER加载,一对多的时候,一方设置LAZY加载
- 多对多关联,多方需要保留一个无参构造器。
欢迎关注
dashuai的博客是终身学习践行者,大厂程序员,且专注于工作经验、学习笔记的分享和日常吐槽,包括但不限于互联网行业,附带分享一些PDF电子书,资料,帮忙内推,欢迎拍砖!