数据库的表与表之间的关系
一对多关系
一对多关系的建表原则:在多的一方创建外键指向一的一方的主键
多对多关系
多对多关系的建表原则:多对多建表原则是,需要创建一个中间表,中间表至少需要两个字段,分别作为外键指向多对多双方的主键。
一对一关系
对于一对一关系一般来说是可以作为一个表来进行存储,除非是有特殊的需求需要用到两个表来存储的时候。如果一定要创建两个表,那么一对一关系可以使用两种方式来实现:
- 就像一对多一样,可以将其中一个表当做是多的一方,这样可以在那张表中增加一个字段来存储外键,只是此时需要个该字段一个unique约束(因为在一对多中多的一方外键是可以重复的,但是一对一中外键是不可以重复的)。
- 第二种方式就是使用主键对应,也就是在一对一的关系中两张表的主键都是一样的。
hibernate一对多关系配置
注:下面所有的例子中这些表的关联在理论上是有外键进行约束的,但是在企业的实际开发中虽然表之前在逻辑上是有关联的,但是实际上并没有建立外键约束,都是通过在代码层面(事务的一致性)来保证的。所以下面的表中实际上并没有建立外键约束,不过不影响hibernate的操作。
案例
表
- category: 分类表
主键 | 名称 |
---|---|
id | cname |
- product:商品表
主键 | 名称 | 外键 |
---|---|---|
id | pname | cid |
实体类(top.twolovelypig.entity包下)
- Category类
package top.twolovelypig.entity;
import java.util.HashSet;
import java.util.Set;
public class Category {
private Integer id;
private String cname;
private Set<Product> products = new HashSet<Product>();
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getCname() {
return cname;
}
public void setCname(String cname) {
this.cname = cname;
}
public Set<Product> getProducts() {
return products;
}
public void setProducts(Set<Product> products) {
this.products = products;
}
}
- Product类
package top.twolovelypig.entity;
public class Product {
private Integer id;
private String pname;
//需要注意的是在多的一方中存放的是一的一方的对象而不是单独的一个外键
private Category category;
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public Category getCategory() {
return category;
}
public void setCategory(Category category) {
this.category = category;
}
public String getPname() {
return pname;
}
public void setPname(String pname) {
this.pname = pname;
}
}
映射配置文件
- Product.hbm.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE hibernate-mapping PUBLIC
"-//Hibernate/Hibernate Mapping DTD 3.0//EN"
"http://www.hibernate.org/dtd/hibernate-mapping-3.0.dtd">
<hibernate-mapping>
<class name="top.twolovelypig.entity.Product" table="product">
<id name="id" column="id">
<!-- 主键生成策略 -->
<generator class="native"></generator>
</id>
<!-- 一般的字段对应规则 -->
<property name="pname" column="pname"></property>
<!-- 多对一的外键对应规则,放置的是一的一方的对象
many-to-one标签:
name:一的一方的属性名称
class:一的一方类的全路径
column:在多的一方的表的外键名称
-->
<many-to-one name="category" class="top.twolovelypig.entity.Category" column="cid" />
</class>
</hibernate-mapping>
- Category.hbm.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE hibernate-mapping PUBLIC
"-//Hibernate/Hibernate Mapping DTD 3.0//EN"
"http://www.hibernate.org/dtd/hibernate-mapping-3.0.dtd">
<hibernate-mapping>
<!-- 表与实体类对应 -->
<class name="top.twolovelypig.entity.Category" table="category">
<!-- id对应的是表中的主键 -->
<id name="id" column="id">
<generator class="native"/>
</id>
<!-- 主键外的其余字段使用property标签 -->
<property name="cname" column="cname"></property>
<!-- 一对多的映射关系:放置的是多的一方的集合
set标签:
name:多的一方的对象的集合的属性的名称
-->
<set name="products">
<!-- key标签:column:放置的是多的一方的外键的名称 -->
<key column="cid"/>
<!-- class属性放置的是多的一方的类的全路径 -->
<one-to-many class="top.twolovelypig.entity.Product"/>
</set>
</class>
</hibernate-mapping>
核心配置文件
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE hibernate-configuration PUBLIC
"-//Hibernate/Hibernate Configuration DTD 3.0//EN"
"http://www.hibernate.org/dtd/hibernate-configuration-3.0.dtd">
<hibernate-configuration>
<session-factory>
<!--下面三个是基本的配置,是必须有的 -->
<!-- 配置连接数据库的基本参数 -->
<property name="hibernate.connection.driver_class">com.mysql.cj.jdbc.Driver</property>
<property name="hibernate.connection.url">jdbc:mysql:///test?serverTimezone=UTC</property>
<property name="hibernate.connection.username">root</property>
<property name="hibernate.connection.password">admin</property>
<!--配置hibernate的方言 -->
<property name="hibernate.dialect">org.hibernate.dialect.MySQL5Dialect</property>
<!-- 显示sql语句 -->
<property name="hibernate.show_sql">true</property>
<!-- 自动建表语句 -->
<property name="hibernate.hbm2ddl.auto">create</property>
<!-- 设置事务隔离级别 -->
<property name="hibernate.connection.isolation">4</property>
<!-- 配置当前线程绑定的Session -->
<property name="hibernate.current_session_context_class">thread</property>
<!-- 加载映射文件 -->
<mapping resource="top/twolovelypig/entity/Category.hbm.xml"/>
<mapping resource="top/twolovelypig/entity/Product.hbm.xml"/>
</session-factory>
</hibernate-configuration>
hibernate帮助类(top.twolovelypig.utils包)
package top.twolovelypig.utils;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.hibernate.cfg.Configuration;
public class HibernateUtils {
public static final Configuration cfg;
public static final SessionFactory sf;
static{
cfg = new Configuration().configure();
sf = cfg.buildSessionFactory();
}
public static Session openSession(){
return sf.openSession();
}
public static Session getCurrentSession(){
return sf.getCurrentSession();
}
}
log4j2.xml
<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="warn">
<Appenders>
<Console name="Console" target="SYSTEM_OUT">
<PatternLayout pattern="%m%n" />
</Console>
</Appenders>
<Loggers>
<Root level="INFO">
<AppenderRef ref="Console" />
</Root>
</Loggers>
</Configuration>
测试类两侧都保存
package top.twolovelypig.entity;
import org.hibernate.Session;
import org.hibernate.Transaction;
import org.junit.Test;
import top.twolovelypig.utils.HibernateUtils;
public class TestHibernate {
@Test
public void testSave(){
Session session = HibernateUtils.getCurrentSession();
//手动开启事务,对于hibernate5是不需要自己手动开启的,为了兼容3,需要手动开启一下
Transaction transaction = session.beginTransaction();
//创建两个分类
Category category1 = new Category();
category1.setCname("11");
Category category2 = new Category();
category2.setCname("22");
//创建三个商品
Product product1 = new Product();
product1.setPname("aa");
Product product2 = new Product();
product2.setPname("bb");
Product product3 = new Product();
product3.setPname("cc");
//设置产品与分类的关系
product1.setCategory(category1);
product2.setCategory(category1);
product3.setCategory(category2);
//设置分类与产品的关系
category1.getProducts().add(product1);
category1.getProducts().add(product2);
category2.getProducts().add(product3);
//保存操作
session.save(product1);
session.save(product2);
session.save(product3);
session.save(category1);
session.save(category2);
//提交事务
transaction.commit();
}
}
上面的测试类在一对多的关系中两张表都执行了保存操作,所以两张表中都会有数据。
只保存一侧的测试
比如上面的测试代码中如果只保存一侧:
//保存操作
/*session.save(product1);
session.save(product2);
session.save(product3);*/
session.save(category1);
session.save(category2);
也就是只保存分类而没有保存产品那么就会出现如下错误:
java.lang.IllegalStateException: org.hibernate.TransientObjectException: object references an unsaved transient instance - save the transient instance before flushing: top.twolovelypig.entity.Product
......
这是想要保存瞬时对象所导致的。
hibernate的级联操作
上面的案例中如果只是保存一侧的操作就会出现错误,实际上我们想要达到的效果是保存一侧的时候另外一侧可以自动保存,因为这两个表是有关联的,我们已经配置了双向的联系。此时可以通过hibernate的级联操作可以达到。
什么叫级联
级联指的是,操作一个对象的时候,可以同时操作器关联的对象
级联的方向性
级联具有方向。
- 操作一的一方的时候,是否操作到多的一方
- 操作多的一方的时候,是否操作到一的一方
级联保存或更新
在上面的例子当保存类别的时候可以级联保存产品,此时是操作分类对象级联操作产品对象,因此需要在分类对象的映射文件里面配置如下(Category.hbm.xml文件上面有,这里只写了部分):
<!--添加的内容只有 cascade="save-update"-->
<set name="products" cascade="save-update">
<!-- key标签:column:放置的是多的一方的外键的名称 -->
<key column="cid"/>
<!-- class属性放置的是多的一方的类的全路径 -->
<one-to-many class="top.twolovelypig.entity.Product"/>
</set>
在此配置文件中只是配置了一个cascade=“save-update”,此时当更新或者保存category对象时就会自动保存或者创建product对象。
同样的如果想要操作product对象来自动保存category对象,那么可以在category.hbm.xml文件中添加配置:
<!-- 多对一的外键对应规则,放置的是一的一方的对象
many-to-one标签:
name:一的一方的属性名称
class:一的一方类的全路径
column:在多的一方的表的外键名称
-->
<many-to-one name="category" class="top.twolovelypig.entity.Category" column="cid" cascade="save-update"/>
同样的上面也只是配置了一句cascade="save-update"那么在测试类中就可以只需要保存category就可以级联保存category:
package top.twolovelypig.entity;
import org.hibernate.Session;
import org.hibernate.Transaction;
import org.junit.Test;
import top.twolovelypig.utils.HibernateUtils;
public class TestHibernate {
@Test
public void testSave(){
Session session = HibernateUtils.getCurrentSession();
//手动开启事务,对于hibernate5是不需要自己手动开启的,为了兼容3,需要手动开启一下
Transaction transaction = session.beginTransaction();
//创建两个分类
Category category1 = new Category();
category1.setCname("112");
Category category2 = new Category();
category2.setCname("221");
//创建三个商品
Product product1 = new Product();
product1.setPname("aaa");
Product product2 = new Product();
product2.setPname("bbb");
Product product3 = new Product();
product3.setPname("ccc");
//设置产品与分类的关系
product1.setCategory(category1);
product2.setCategory(category1);
product3.setCategory(category2);
//设置分类与产品的关系
category1.getProducts().add(product1);
category1.getProducts().add(product2);
category2.getProducts().add(product3);
//保存操作
session.save(product1);
session.save(product2);
session.save(product3);
/*session.save(category1);
session.save(category2);*/
//提交事务
transaction.commit();
}
}
测试对象导航
@Test
public void testSave2(){
Session session = HibernateUtils.getCurrentSession();
//手动开启事务,对于hibernate5是不需要自己手动开启的,为了兼容3,需要手动开启
Transaction transaction = session.beginTransaction();
//创建一个分类
Category category = new Category();
category.setCname("11");
//创建三个商品
Product product1 = new Product();
product1.setPname("aa1a");
Product product2 = new Product();
product2.setPname("b1bb");
Product product3 = new Product();
product3.setPname("cc1c");
//设置产品与分类的关系
product1.setCategory(category);
//设置分类与产品的关系
category.getProducts().add(product2);
category.getProducts().add(product3);
//保存操作(两边都设置了casecade)
session.save(product1);
//提交事务
transaction.commit();
}
注:这里的案例中两侧都开启了casecade=“save-update”,也就是两侧都是级联更新的
上面的案例中产品1与分类1是互相关联的(这两条一定会被插入到数据库中,并且产品表中的外键是有值的),同时又将产品2与3关联到分类下面,由于在分类那一侧开启了级联操作,所以操作分类对象的时候绑定到分类上面的产品也是会被执行到数据库的。因此一共有四条插入语句。
现在将上面的save方法修改如下所示:
session.save(category);
此时如果打印sql到控制台会发现执行了三条insert语句,数据中也是只有三条语句,分别是category表中一条(因为这里是save(category),所以category肯定是被insert到数据库中),又由于将product2与product3绑定到categor中,并且category.hbm.xml开启了级联保存或更新操作,所以product2与product3也会被保存到product表中,需要注意的是这里虽然product1绑定了category,但是由于操作的主体是category,并且category并没有绑定product1,所以product1是不会被插入到数据库中的。
再次将插入语句修改如下所示:
session.save(product2);
此时只会执行一条插入语句,就是讲product2插入到product表中,category表不会有数据,而且插入的product2这条数据中外键也为null。
级联删除
所谓的级联删除的时候就是删除一侧的时候将绑定的对象也会删除。
在没有配置的情况下。即使是有外键约束,hibernate也是可以删除的,做法是先将外键的值设置为null,然后再去删除主单数据。
如下代码所示:
@Test
public void testDel(){
Session session = HibernateUtils.getCurrentSession();
//手动开启事务,对于hibernate5是不需要自己手动开启的,为了兼容3,需要手动开启
Transaction transaction = session.beginTransaction();
//创建一个分类
Category category = session.get(Category.class, 1);
session.delete(category);
//提交事务
transaction.commit();
}
目前这里还没有在映射文件中配置级联删除,此时直接删除主键为1的分类的话,在product表中外键为1的首先都会被设置为null,然后再去删除category。
级联删除的设置
级联删除的设置与级联更新一样,也是在映射文件中进行设置,比如这里是删除category想要级联删除product,所以主体是category,那么就可以在category.hbm.xml中进行设置即可。
category.hbm.xml:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE hibernate-mapping PUBLIC
"-//Hibernate/Hibernate Mapping DTD 3.0//EN"
"http://www.hibernate.org/dtd/hibernate-mapping-3.0.dtd">
<hibernate-mapping>
<!-- 表与实体类对应 -->
<class name="top.twolovelypig.entity.Category" table="category">
<!-- id对应的是表中的主键 -->
<id name="id" column="id">
<generator class="native"/>
</id>
<!-- 主键外的其余字段使用property标签 -->
<property name="cname" column="cname"></property>
<!-- 一对多的映射关系:放置的是多的一方的集合
set标签:
name:多的一方的对象的集合的属性的名称
-->
<set name="products" cascade="save-update, delete">
<!-- key标签:column:放置的是多的一方的外键的名称 -->
<key column="cid"/>
<!-- class属性放置的是多的一方的类的全路径 -->
<one-to-many class="top.twolovelypig.entity.Product"/>
</set>
</class>
</hibernate-mapping>
这里是配置了级联更新以及级联删除:cascade="save-update, delete"
,此时再用上面的代码删除category时就会级联删除绑定的product。
需要注意的是对于删除或者是更新一般都是要先查询出结果,然后再进行更新或者删除的操作,而且还需要判断一下查询的结果是否为空。
一对多设置了双向关联时可能会产生多余的sql语句
由于双向关联也就是操作一方的时候会操作另外一方,如果我们此时操作了两方的话就会出现多余的sql语句,这会影响到性能。此时解决方法如下:
- 单向维护(也就是级联操作加在一方上面即可)
- 使某一方放弃外键维护权
- 一般都是一的一方放弃,操作是在set上面配置inverse=“true”
<set name="products" cascade="save-update, delete" inverse="true">
<!-- key标签:column:放置的是多的一方的外键的名称 -->
<key column="cid"/>
<!-- class属性放置的是多的一方的类的全路径 -->
<one-to-many class="top.twolovelypig.entity.Product"/>
</set>
多对多关系
表的关系
对于表与表之间有多对多的关系的时候(比如学生与选课之间的关系)是需要建立中间表的,中间表至少需要两个字段,分别是关联表的外键。这里就以学生表(student)与选课表(course)来举例说明。
student表:
主键 | 姓名 | 学号 |
---|---|---|
stu_id | stu_name | stu_num |
course表:
主键 | 课程名 |
---|---|
c_id | c_name |
中间表stu_cou:
学生表外键 | 课程表外键 |
---|---|
stu_id | c_id |
在中间表中这两个字段对应的是各自表的外键,它们组合在一起是主键。
创建实体
student类:
public class Student{
private Long stu_id;
private String stu_name;
private String stu_num;
//设置多对多的关系,表示一个学生选择多个课程
//此处放置的是课程的集合
private Set<Course> courses = new HashSet<Course>();
//省略了set、get方法
}
course类:
public class Course{
private Long c_id;
private String c_name;
//同样也是需要设置多对多的关系,表示一个课程被多个学生选择
//此处需要放置的是学生的集合
private Set<Student> courses = new HashSet<Student>();
//省略了set、get方法
}
创建映射
student的映射:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE hibernate-mapping PUBLIC
"-//Hibernate/Hibernate Mapping DTD 3.0//EN"
"http://www.hibernate.org/dtd/hibernate-mapping-3.0.dtd">
<hibernate-mapping>
<class name="top.twolovelypig.entity.Student" table="student">
<!--column与name一致时,column可以省略-->
<id name="stu_id" column="stu_id">
<!-- 主键生成策略 -->
<generator class="native"></generator>
</id>
<!-- 一般的字段对应规则 -->
<property name="stu_name" column="stu_name"></property>
<property name="stu_num" column="stu_num"></property>
<!-- 创建于课程的多对多的映射关系,由于多对多放置的都是多的一方,所以都是需要使用set
name: 对方的集合属性名称
table:多对多关系需要使用中间表,放置的是中间表的名称
-->
<set name="courses" table="stu_cou">
<!--column: 当前的对象对应的中间表的外键的名字-->
<key column="stu_id"/>
<!--
class:对方的类的全路径
column:对方的对象在中间表的外键的名称
-->
<many-to-many class="top.twolovelypig.entity.Course" column="c_id"/>
</set>
</class>
</hibernate-mapping>
course的映射:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE hibernate-mapping PUBLIC
"-//Hibernate/Hibernate Mapping DTD 3.0//EN"
"http://www.hibernate.org/dtd/hibernate-mapping-3.0.dtd">
<hibernate-mapping>
<class name="top.twolovelypig.entity.Course" table="course">
<!--column与name一致时,column可以省略-->
<id name="c_id" column="c_id">
<!-- 主键生成策略 -->
<generator class="native"></generator>
</id>
<!-- 一般的字段对应规则 -->
<property name="c_name" column="c_name"></property>
<!-- 创建和课程的多对多的映射关系,由于多对多放置的都是多的一方,所以都是需要使用set
name: 对方的集合属性名称
table:多对多关系需要使用中间表,放置的是中间表的名称
-->
<set name="students" table="stu_cou">
<!--column: 当前的对象对应的中间表的外键的名字-->
<key column="c_id"/>
<!--
class:对方的类的全路径
column:对方的对象在中间表的外键的名称
-->
<many-to-many class="top.twolovelypig.entity.Student" column="stu_id"/>
</set>
</class>
</hibernate-mapping>
多对多建立双向关系直接保存
多对多建立双向关系后直接保存于一对多是不同的,因为多对多是有中间表的,他们之间的关系其实就是网中间表插入数据,所以如果双方都建立了联系,然后都执行保存的话就会报错,因为两个外键合起来就是主键,主键是不允许重复的,因此此时必须有一方放弃外键维护,一般都是被动的一方放弃外键的维护,在这个例子里面课程是被选的,所以课程是被动的一方,放弃外键维护的方式就是在set标签里面使用inverse=“true”
将course.hbm.xml中的set标签修改如下:
<set name="students" table="stu_cou" inverse="true">
<!--column: 当前的对象对应的中间表的外键的名字-->
<key column="c_id"/>
<!--
class:对方的类的全路径
column:对方的对象在中间表的外键的名称
-->
<many-to-many class="top.twolovelypig.entity.Student" column="stu_id"/>
</set>
此时就是course放弃了外键的维护权。
只保存一侧是否可以
多对多与一对多一样,如果只是保存一侧也是会提示保存瞬时对象出错,所以此时必须设置级联操作才可以。
多对多级联更新
比如如果想保存学生的时候自动级联保存课程,此时学生就是主体需要在学生的映射文件中配置级联操作。下面是学生的映射文件:
<set name="courses" table="stu_cou" casecade="save-update">
<!--column: 当前的对象对应的中间表的外键的名字-->
<key column="stu_id"/>
<!--
class:对方的类的全路径
column:对方的对象在中间表的外键的名称
-->
<many-to-many class="top.twolovelypig.entity.Course" column="c_id"/>
</set>
由于学生是主体,也就是多对多关系时由学生来维护的,此时需要课程放弃外键的维护权,也就是需要在course.hbm.xml文件的set标签中设置inverse=“true”来达到让课程放弃外键的维护权的目的。当设置了级联操作后只需要保存学生,那么相关联的课程也是会被保存到数据库中。
多对多的级联删除(基本不用)
虽然hibernate也是可以进行级联删除(和一对多一类似的设置,需要注意的是需要先查询再删除),但是一般在实际开发中是不使用级联删除的,因为这样做的是不合理,比如我删除了一门课程(取消了该课程),而选了该课程的学生也会被删除,显然是不合理的。
多对多的其他操作
给学生添加课程
如一个学成是可以选多个课程的,所以自然是可以添加课程的
public testAddCourse(){
Session session = HibernateUtils.getCurrentSession();
Transaction tx = session.beginTransaction();
//假如是想要给学号为1的学生添加课程id为2的课程(使用get查询参数需要是主键)
//查询学号为1的学生
Student student = session.get(Student.class, 1);
//查询课程id为2的课程
Course course = session.get(Course.class, 2);
if(student != null){
student.getCourses().add(course);
}
tx.commit;
}
通过上面的操作就会将课程号为2的课程添加到一号学生中。其实就是中间表里面多了一条1-2的数据。(前面的1表示学生主键,后面的2表示课程表的主键)
给学生改选课程
public testAddCourse(){
Session session = HibernateUtils.getCurrentSession();
Transaction tx = session.beginTransaction();
//假如是想要给学号为1的学生将原有课程id为2的课程改成课程id为3的课程
//查询学号为1的学生
Student student = session.get(Student.class, 1);
//查询课程id为2的课程
Course course2 = session.get(Course.class, 2);
//查询课程id为3的课程
Course course3 = session.get(Course.class, 3);
if(student != null){
student.getCourses().remove(course2);
student.getCourses().add(course3);
}
tx.commit;
}
让学生删除课程
public testAddCourse(){
Session session = HibernateUtils.getCurrentSession();
Transaction tx = session.beginTransaction();
//假如是想要给学号为1的学生删除已选课程id为2的课程
//查询学号为1的学生
Student student = session.get(Student.class, 1);
//查询课程id为2的课程
Course course = session.get(Course.class, 2);
if(student != null){
student.getCourses().remove(course);
}
tx.commit;
}