Hibernate关联映射
一、ORM,全称是(Object Relational Mapping),即对象关系映射。ORM的实现思想就是将关系数据库中表的数据映射成对象,以对象的形式展现,这样开发人员就可以把对数据库的操作转化为对这些对象的操作。Hibernate正是实现了这种思想,达到了方便开发人员以面向对象的思想来实现对数据库的操作。
hibernate在实现ORM功能的时候主要用到的文件有:映射类(*.Java)、映射文件(*.hbm.xml)和数据库配置文件(*.properties/*.cfg.xml),它们各自的作用如下。
映射类(*.java):它是描述数据库表的结构,表中的字段在类中被描述成属性,将来就可以实现把表中的记录映射成为该类的对象了。
映射文件(*.hbm.xml):它是指定数据库表和映射类之间的关系,包括映射类和数据库表的对应关系、表字段和类属性类型的对应关系以及表字段和类属性名称的对应关系等。
数据库配置文件(*.properties/*.cfg.xml):它是指定与数据库连接时需要的连接信息,比如连接哪种数据库、登录数据库的用户名、登录密码以及连接字符串等。当然还可以把映射类的地址映射信息放在这里。
二、创建工具类,用于调试程序:
public class SessionFactoryUtils {
private SessionFactory sessionFactory;
private static SessionFactoryUtils factoryUtils;
private SessionFactoryUtils(){ //单例模式----私有化构造方法
}
public static SessionFactoryUtils getInstance()
{
if(factoryUtils == null)
{
factoryUtils = new SessionFactoryUtils ();
}
return factoryUtils;
}
public SessionFactory openSessionFactory()
{
if(sessionFactory == null){
//加载主配置文件
Configuration config = new Configuration().configure()
//建立工厂
sessionFactory = config.buildSessionFactory();
}
return sessionFactory;
}
三、 Hibernate的七种映射关系:
1、 单向一对一关联映射(one-to-one): 两个对象之间一对一的关系,例如:Person(人)-IdCard(身份证),有两种策略可以实现一对一的关联映射:
*主键关联:即让两个对象具有相同的主键值,以表明它们之间的一一对应的关系;数据库表不会有额外的字段来维护它们之间的关系,仅通过表的主键来关联。如下图:
*唯一外键关联:外键关联,本来是用于多对一的配置,但是加上唯一的限制之后(采用<many-to-one>标签来映射,指定多的一端unique为true,这样就限制了多的一端的多重性为一),也可以用来表示一对一关联关系,其实它就是多对一的特殊情况。如下图:
注意:因为一对一的主键关联映射扩展性不好,当我们的需要发生改变想要将其变为一对多的时候变无法操作了,所以我们遇到一对一关联的时候经常会采用唯一外键关联来解决问题,而很少使用一对一主键关联。
示例:
类图:
数据库关系图 :
Person类:
public class Person {
private int id;
private String name;
private IdCard idCard;
}
Person.hbm.xml:
<hibernate-mapping>
<class name="com.cn.demo.Person" table="TB_PERSON">
<id name="id" type="int">
<column name="ID" />
<!-- 重点:主键生成策略 因为主键跟外键是同一个,所以直接在这里申明该主键就是外键,并且指向了idCard这个类 -->
<generator class="foreign">
<param name="property">idCard</param>
</generator>
</id>
<property name="name" type="java.lang.String">
<column name="NAME" />
</property>
<!--constrained属性:就是表明我们的主键当外键使用了。 这个属性两个作用,一是通知这种对应关系在上面已经写过了,所以这里才不需要写column,二是表明这种关系是什么,也就是主键当外键。
-->
<one-to-one name="idCard" constrained="true"></one-to-one>
</class>
</hibernate-mapping>
IdCard类:
public class IdCard {
private int id;
private String cardNo;
}
IdCard.hbm.xml:
<hibernate-mapping>
<class name="com.cn.demo.IdCard" table="TB_IDCARD">
<id name="id" type="int">
<column name="ID" />
<generator class="native" />
</id>
<property name="cardNo" type="java.lang.String">
<column name="CARDNO" />
</property>
</class>
</hibernate-mapping>
测试代码:
public class PeronToCard {
private SessionFactory sessionFactory;
@Before
public void init()
{
sessionFactory = SessionFactoryUtils.getInstance().getSessionFactory();
}
@Test
public void test() {
Session session = sessionFactory.openSession();
Transaction tx = session.beginTransaction();
IdCard idCard = new IdCard();
idCard.setCardNo("111111");
Person person = new Person();
person.setName("qqq");
person.setIdCard(idCard);
try {
session.save(idCard);
session.save(person);
tx.commit();
} catch (Exception e) {
e.printStackTrace();
tx.rollback();
}
}
}
2、 单向多对一关联映射(many-to-one):
1)单向n-1关联只需从n的一端可以访问1的一端
2)域模型:从Order到Customer的多对一单向关联需要在Order类中定义一个Customer属性,而在Customer中无需定义存放Order对象的集合属性
3)在关系数据模型Orders表中的Customer_id参照Customer表中的主键:
public class Customer {
private Integer custormerId;
private String custormerName;
//省略set/get方法
}
public class Order {
private Integer orderId;
private String orderName;
private Customer customer;
//省略set/get方法
}
Customer.hbm.xml
<class name="Customer" table="CUSTOMERS">
<id name="custormerId" type="java.lang.Integer">
<column name="CUSTORMER_ID" />
<generator class="native" />
</id>
<property name="custormerName" type="java.lang.String">
<column name="CUSTORMER_NAME" />
</property>
</class>
Order.hbm.xml
<class name="Order" table="ORDERS">
<id name="orderId" type="java.lang.Integer">
<column name="ORDER_ID" />
<generator class="native" />
</id>
<property name="orderName" type="java.lang.String">
<column name="ORDER_NAME" />
</property>
<!--
映射多对一的关联关系:使用many-to-one来映射
name : 多的一端关联的一的一端的属性的名字
class:一那一端的属性对应的类名
column:一那一端在多那一端对应的数据表中的外键的名称
-->
<many-to-one name="customer" class="Customer">
<column name="CUSTOMER_ID" />
</many-to-one>
</class>
多对一映射原理:在多的一端加入一个外键指向一的一端,它维护的关系多指向一。
一的一方,不需要维护关系,所以和普通的配置一样既可以了。
多的一方,需要维护双方关系,所以里面配置有一的一方的引用
测试:
1)插入操作:
//执行save操作:先插入customer,后插入order,3条insert语句
//先插入1的一端,后插入n的一端,只有insert语句
session.save(customer);
session.save(order1);
session.save(order2);
//先插入order,后插入customer,3条insert语句,2条update语句
//先插入n的一端,后插入1的一端,会多出update语句
session.save(order1);
session.save(order2);
session.save(customer);
2)查询操作:
//1.在查询多的一端的一个对象,则默认情况下,只查询了多的一端的对象,而没有查询关联的1的那一端的对象
Order order = (Order)session.get(Order.class,3);
System.out.println(order.getOrderName());
//2.在需要使用到关联的对象时,才发送对应的SQL语句
Customer customer = order.getCustomer();
System.out.println(customer.getCustormerName());
3)更新操作:
Order order = (Order)session.get(Order.class,1);
order.getCustomer().setCustormerName("DDD");
4)删除操作:注意级联关系,若先删除1的一端,因为在n的一端引用了1的一端,所以无法删除
Order order = (Order) session.get(Order.class,1);
session.delete(order);
3、单向一对多关联映射(ont-to-many): 一对多关联映射和多对一关联映射原理是一致的,都是在多的一端加入一个外键,指向一的一端。
两个实体类,一个部门类( Department),一个职工类(Employee):一个部门有多个员工
Department实体类:
public class Department {
private Integer did;
private String departname;
private Set<Employee> employeeSet = new HashSet<Employee>();
//get和set方法省略
}
Employee实体类
public class Employee {
private Integer eid;
private String ename;
private String esex;
//get和set方法省略
}
注意: 一个部门会对应多个员工对象,因此设置了一个员工对象的集合。这个集合为什么要用Set类型而不用List类型的呢?因为List集合是可存储重复的元素的,而Set中元素具有唯一性,不会去存储重复的元素,因此这个地方我们采用Set集合
Employee.hbm.xml映射文件:
<hibernate-mapping>
<class name="com.cn.pojo.Employee" table="EMPLOYEE">
<id name="eid" type="java.lang.Integer">
<column name="EID" />
<!-- 设置为native之后,主键会按照本来的顺序进行增长(比如之前3但是删除了,现在就从4开始) -->
<generator class="native" />
</id>
<property name="ename" type="java.lang.String">
<column name="ENAME" />
</property>
<property name="esex" type="java.lang.String">
<column name="ESEX" />
</property>
</class>
</hibernate-mapping>
Department.hbm.xml映射文件
<hibernate-mapping>
<class name="com.cn.pojo.Department" table="DEPARTMENT">
<id name="did" type="java.lang.Integer">
<column name="DID" />
<!-- 设置成increment之后,会从数据库中查找主键id最大的值,然后+1进行存储 -->
<generator class="increment" />
</id>
<property name="departname" type="java.lang.String">
<column name="DEPARTNAME" />
</property>
<!--
inverse:默认为false, 指关联关系的控制权由自己来维护
inverse设置为true的时候,就说明关联在多的那一方被维护 (一般设置在多的一方去维护,效率会更高一点)
-->
<set name="employeeSet" table="EMPLOYEE" cascade="all" inverse="false" lazy="false">
<key>
<column name="DID" />
</key>
<one-to-many class="com.cn.pojo.Employee" />
</set>
</class>
</hibernate-mapping>
强调:cascade:只有"关系标记"才有cascade属性;
一个操作因级联cascade可能触发多个关联操作。前一个操作叫“主控操作”,后一个操作叫“关联操作”。
(1)cascade属性的可能值有
all: 所有情况下均进行关联操作,即save-update和delete。
none: 所有情况下均不进行关联操作。这是默认值。
save-update: 在执行save/update/saveOrUpdate时进行关联操作。
delete: 在执行delete 时进行关联操作。
all-delete-orphan: 当一个节点在对象图中成为孤儿节点时,删除该节点。比如在一个一对多的关系中,Student包含多个book,当在对象关系中删除一个book时,此book即成为孤儿节点。
(2)级联(cascade)属性的作用:级联指的是当主控方执行操作时,关联对象(被动方)是否同步执行同一操作。
pojo和它的关系属性的关系就是“主控方 -- 被动方”的关系,如果关系属性是一个set,那么被动方就是set中的一个元素。
测试代码:
public class OneToMany {
private SessionFactory factory;
@Before
public void init()
{
factory = SessionFactoryUtils.getInstance().openSessionFactory();
}
@Test
public void add() {
Session session = factory.openSession();
Transaction ts = session.beginTransaction();
Department d = new Department();
d.setDepartname("财务部");
Employee e1 = new Employee();
e1.setEname("杨洋");
e1.setEsex("男");
Employee e2 = new Employee();
e2.setEname("美美");
e2.setEsex("女");
d.getEmployeeSet().add(e1);
d.getEmployeeSet().add(e2);
try {
session.save(d);
ts.commit();
} catch (Exception e) {
ts.rollback();
}
}
}
单向一对多:实体之间关系由”一”的一端加载”多”的一端,关系由”一”的一端来维护。也就是说在”一”的一端持有”多”的一端的集合。
我们在加载”一”的时候把”多”也加载进来了,也就是在部门中维护职工的集合。我们可以根据部门找到属于这些部门的所有职工。但是不能根据职工找到它所属的部门。
为什么会有5条sql语句,进行insert之后为什么还要update呢?
因为单向一对多关系是在”一”的一方维护数据,多的一方Employee并不知道有Department的存在。所以在保存Employee的时候,关系字段did是为null的。如果把did字段设置为非空,则无法保存数据。
因为Employee不维护关系,而是由Department来维护关系,则在对Department进行操作的时候,就会发出多余的update语句,去维持Department与Employee的关系,这样加载Department的时候才会把该Department对应的职工加载进来。所以会多两条update语句。这样的效率是非常低的。
为了提高效率,我们一般会采用双向一对多关联的方式。
4、单向多对多: 多对多关联映射需要新增加一张表才完成基本映射。 实例如: 用户与他的角色(一个用户拥有多个角色,一个角色还可以属于多个用户)
多对多关联映射,需要一个中间表,两个表中的主键放到第三张做一个关联,用第三张表来解决可能会造成数据冗余的问题,多对多的关联映射,在实体类中,跟一对多一样,也是用集合来表示的。
User类:
public class User {
private Integer id;
private String uname;
private Set<Role> roles = new HashSet<Role>();
}
User.hbm.xml:
<hibernate-mapping>
<class name="com.cn.beans.User" table="TB_USER">
<id name="id" type="java.lang.Integer">
<column name="ID" />
<generator class="native" />
</id>
<property name="uname" type="java.lang.String">
<column name="U_NAME" />
</property>
<set name="roles" table="USER_ROLE">
<key>
<column name="USER_ID" />
</key>
<many-to-many class="com.cn.beans.Role" column="ROLE_ID" />
</set>
</class>
</hibernate-mapping>
Role类:
public class Role {
private Integer id;
private String rname;
}
Role.hbm.xml:
<hibernate-mapping>
<class name="com.cn.beans.Role" table="TB_ROLE">
<id name="id" type="java.lang.Integer">
<column name="ID" />
<generator class="native" />
</id>
<property name="rname" type="java.lang.String">
<column name="RNAME" />
</property>
</class>
</hibernate-mapping>
测试类:
public class ManyToMany {
private SessionFactory sessionFactory;
@Before
public void init()
{
sessionFactory = SessionFactoryUtils.getInstance().getSessionFactory();
}
@Test
public void test() {
Session session = sessionFactory.openSession();
Role r1 = new Role();
r1.setRname("程序员");
Role r2 = new Role();
r2.setRname("产品经理");
Role r3 = new Role();
r3.setRname("CEO");
User u1 = new User();
u1.setUname("小明");
User u2 = new User();
u2.setUname("小江");
u1.getRoles().add(r1);
u1.getRoles().add(r2);
u2.getRoles().add(r3);
Transaction ts = session.beginTransaction();
try {
session.save(r1);
session.save(r2);
session.save(r3);
session.save(u1);
session.save(u2);
ts.commit();
} catch (Exception e) {
e.printStackTrace();
ts.rollback();
}
}
}
注意: 单向关联映射则是只能由A端去操作B端,B端不能操作A端的数据。而双向关联映射则是A,B两端都可以操作另一端的数据。
5、双向一对一关联: 双向的主键关联其实是单向一对一主键关联的一种特殊情况,只不过要在关联对象的两端的映射文件中都要进行<one-to-one>的配置,另外还要在主映射的主键一端采用foreign外键关联属性。
用Person和IdCard来讨论,一个人对应着一个唯一的身份证,而且一个身份证也唯一映射着一个人,所以这就产生了双向的关联关系,Person的主键同样也是IdCard的主键,分别是主键的同时也是外键,这种关联关系成为双向一对一映射,表现到关系模型中可如下图:
Person类:
public class Person {
private int id;
private String name;
private IdCard idCard;
}
Person.hbm.xml:
<hibernate-mapping>
<class name="com.cn.beans.Person" table="TB_PERSON">
<id name="id" type="int">
<column name="ID" />
<!-- 重点:主键生成策略 因为主键跟外键是同一个,所以直接在这里申明该主键就是外键,并且指向了idCard这个类 -->
<generator class="foreign">
<param name="property">idCard</param>
</generator>
</id>
<!--由于在申明主键的时候已经将关系写清楚了,所以在这里没有column这个属性。
按平常的习惯,在这里写上column="数据库中外键字段属性名称。"
-->
<property name="name" type="java.lang.String"></property>
<!-- constrained属性:就是表明我们的主键当外键使用了。 这个属性两个作用,
一是通知这种对应关系在上面已经写过了,所以这里才不需要写column,
二是表明这种关系是什么,也就是主键当外键。
-->
<one-to-one name="idCard" class="com.cn.beans.IdCard" ></one-to-one>
</class>
</hibernate-mapping>
IdCard类:
public class IdCard {
private int id;
private String cardNo;
private Person person;
}
IdCard.hbm.xml:
<hibernate-mapping>
<class name="com.cn.beans.IdCard" table="IDCARD">
<id name="id" type="int">
<column name="ID" />
<generator class="native" />
</id>
<property name="cardNo" type="java.lang.String">
<column name=""></column>
</property>
<!-- 这里只需要写这些就足够了,因为one-to-one默认使用的就是用主键跟关联类的主键进行比较,本来就是主键关系,
通过主键跟主键比较,就能达到目的,所以这个中没有column这个属性,
但是可以配置一些别的属性,不需要写column
-->
<one-to-one name="person" class="com.cn.beans.Person" constrained="true"></one-to-one>
</class>
</hibernate-mapping>
测试代码:
public class OneToOne {
private SessionFactory sessionFactory;
@Before
public void init()
{
sessionFactory = SessionFactoryUtils.getInstance().getSessionFactory();
}
@Test
public void test() {
Session session = sessionFactory.openSession();
Transaction ts = session.beginTransaction();
IdCard id = new IdCard();
id.setCardNo("1001003");
Person p = new Person();
p.setName("王五1");
// id.setPerson(p);
// p.setIdCard(id);
try {
IdCard idCard = (IdCard)session.get(IdCard.class, 62);
System.out.println(idCard.getPerson().getName());
// session.save(id);
// session.save(p);
ts.commit();
} catch (Exception e) {
e.printStackTrace();
ts.rollback();
}
}
}
6、双向一对多/多对一: 所谓的双向一对多关联,同时配置单向多对一和单向一对多就可以构成双向一对多关联关系。它在解决单向一对多关系存在的缺陷起到了一定的修补作用。
双向:更新一张表,另一张表中与之相关联的数据都进行更新,两张表都可以相互查询。
单向:主表更新,从表更新。从表更新,主表不管。主表可以查询到从表内容,从表不能查询主表内容。
ClassInfo类:
public class ClassInfo implements Serializable{
private Integer claId;
private String claName;
private Set<Student> stus = new HashSet<>();
}
ClassInfo.hbm.xml:
<hibernate-mapping>
<class name="com.cn.beans.ClassInfo" table="CLASS_INFO">
<id name="claId" type="java.lang.Integer" access="field">
<column name="CLA_ID" />
<generator class="assigned" />
</id>
<property name="claName" type="java.lang.String" access="field">
<column name="CLA_NAME" />
</property>
<!-- inverse为true 关系由对方维持 -->
<set name="stus" table="STUDENT" inverse="true" lazy="true" access="field"
cascade="save-update">
<key>
<!-- 多的一方的外键名 -->
<column name="CLA_ID" />
</key>
<one-to-many class="com.cn.beans.Student" />
</set>
</class>
</hibernate-mapping>
Student类:
public class Student implements Serializable {
private Integer stuId;
private String stuName;
private ClassInfo classInfo;
}
student.hbm.xml:
<hibernate-mapping>
<class name="com.cn.beans.Student" table="STUDENT">
<id name="stuId" type="java.lang.Integer" access="field">
<column name="STU_ID" />
<generator class="assigned" />
</id>
<property name="stuName" type="java.lang.String" access="field">
<column name="STU_NAME" />
</property>
<!-- many-to-one 无inverse属性 默认为false -->
<many-to-one name="classInfo" class="com.cn.beans.ClassInfo"
access="field" fetch="join" cascade="save-update" >
<column name="CLA_ID" />
</many-to-one>
</class>
</hibernate-mapping>
测试类:
public class HibernateUtilsTest {
private SessionFactory sessionFactory;
@Before
public void init()
{
sessionFactory = SessionFactoryUtils.getInstance().getSessionFactory();
}
public void testGet(){
Session session = sessionFactory.openSession();
Transaction tran = session.beginTransaction();
// ClassInfo classInfo = (ClassInfo) session.get(ClassInfo.class, 10);
// System.out.println(classInfo.toString());
Student stu = (Student) session.get(Student.class, 2);
System.out.println(stu.toString());
}
/**
* 通过班级添加学生
*/
@Test
public void testSave1(){
Session session = sessionFactory.openSession();
Transaction tran = session.beginTransaction();
Student stu1 = new Student();
stu1.setStuId(1);
stu1.setStuName("学生1");
Student stu2 = new Student();
stu2.setStuId(2);
stu2.setStuName("学生2");
ClassInfo classInfo = new ClassInfo();
classInfo.setClaId(10);
classInfo.setClaName("十班");
classInfo.getStus().add(stu1);
classInfo.getStus().add(stu2);
//当inverse=true的时候,这里应该添加!不然数据库中外键没有数据
stu2.setClassInfo(classInfo);
session.save(classInfo);
}
/**
* 通过学生添加班级
*/
public void testSave2(){
Session session = sessionFactory.openSession();
Transaction tran = session.beginTransaction();
Student stu1 = new Student();
stu1.setStuId(1);
stu1.setStuName("学生1");
ClassInfo classInfo = new ClassInfo();
classInfo.setClaId(10);
classInfo.setClaName("十班");
stu1.setClassInfo(classInfo);;
session.save(stu1);
}
@After
public void colseSession(){
if(tran!=null){
tran.commit();
}
if(session!=null&&session.isOpen()){
session.close();
}
}
}
结论:无论是从哪一方获取数据,都能获取到关联对方的数据
1.在映射一对多的双向关联关系时,应该在one方把inverse属性设为true,这可以提高性能
2.当删除双向关联的关系时,也应该修改关联两端的对象的相应属性:
3.只有集合标记(set/map/list/array)才有inverse属性。
4.维护关联关系是指设置外键列的值(设置有效值或是null值).
5.因为是双向操作,所以保存任何一方都能成功,但是推荐保存”一”的一方,并在”一”的一方交出控制权,这样会提高效率
7、 双向多对多:B(Teacher)有多个A(Student),A也有多个B。 注意要在一方加上inverse属性,否则一起维护关联关系可能会造成主键冲突:
Teacher类:
public class Teacher {
private Integer Id;
private String Name;
private String PhoneNum;
private Set<Student> Students = new HashSet<>();
}
Teacher.hbm.xml:
<hibernate-mapping>
<class name="Teacher" table="TEACHER">
<id name="Id" type="java.lang.Integer">
<column name="ID" />
<generator class="native">
</generator>
</id>
<property name="Name" type="java.lang.String">
<column name="NAME" />
</property>
<property name="PhoneNum" type="java.lang.String">
<column name="PhoneNum" />
</property>
<set name="Students" table="teacher_student" inverse="true">
<key column="teacherId"></key>
<many-to-many column="studentId" class="com.cn.beans.Student"></many-to-many>
</set>
</class>
</hibernate-mapping>
Student类
public class Student {
private Integer Id;
private String sName;
private String Age;
private Set<Teacher> Teachers = new HashSet<>();
}
Student.hbm.xml:
<hibernate-mapping>
<class name="Student" table="STUDENT">
<id name="Id" type="java.lang.Integer">
<column name="ID" />
<generator class="native">
</generator>
</id>
<property name="sName" type="java.lang.String">
<column name="SNAME" />
</property>
<property name="Age" type="java.lang.String">
<column name="AGE" />
</property>
<set name="Teachers" table="teacher_student">
<key column="studentId"></key>
<many-to-many column="teacherId" class="com.cn.beans.Teacher"></many-to-many>
</set>
</class>
</hibernate-mapping>
测试代码:
Teacher teacher = new Teacher();
Student student = new Student();
teacher.setName("chenT");
teacher.setPhoneNum("18788837117");
student.setsName("ma");
student.setAge("11");
student.getTeachers().add(teacher);
teacher.getStudents().add(student);
session.save(student);
session.save(teacher);