(转)Hibernate关联映射——一对多(多对一)

时间:2022-02-14 21:25:22

http://blog.csdn.net/yerenyuan_pku/article/details/70152173

Hibernate关联映射——一对多(多对一)

我们以客户(Customer)与订单(Order)为例来讲解Hibernate关联映射中的一对多关联关系。 
首先肯定是搭建好Hibernate的开发环境,我在此也不过多赘述,读者自行实践。接着在src目录下创建一个cn.itheima.oneToMany包,并在该包下创建两个实体类,如下:

  • 客户类

    // 客户 ---- 一的一方
    public class Customer { private Integer id; // 主键
    private String name; // 姓名 // 描述客户可以有多个订单
    private Set<Order> orders = new HashSet<Order>(); public Set<Order> getOrders() {
    return orders;
    }
    public void setOrders(Set<Order> orders) {
    this.orders = orders;
    }
    public Integer getId() {
    return id;
    }
    public void setId(Integer id) {
    this.id = id;
    }
    public String getName() {
    return name;
    }
    public void setName(String name) {
    this.name = name;
    } }
    • 1
  • 订单类

    // 订单 ---- 多的一方
    public class Order { private Integer id;
    private Double money;
    private String receiverInfo; // 收货地址 // 订单与客户关联
    private Customer c; // 描述订单属于某一个客户 public Customer getC() {
    return c;
    }
    public void setC(Customer c) {
    this.c = c;
    }
    public Integer getId() {
    return id;
    }
    public void setId(Integer id) {
    this.id = id;
    }
    public Double getMoney() {
    return money;
    }
    public void setMoney(Double money) {
    this.money = money;
    }
    public String getReceiverInfo() {
    return receiverInfo;
    }
    public void setReceiverInfo(String receiverInfo) {
    this.receiverInfo = receiverInfo;
    } }
    • 1

完成之后,再在cn.itheima.oneToMany包下分别编写这两个类的映射配置文件。

  • Customer.hbm.xml

    <?xml version="1.0"?>
    <!DOCTYPE hibernate-mapping PUBLIC "-//Hibernate/Hibernate Mapping DTD 3.0//EN"
    "http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd">
    <hibernate-mapping>
    <class name="cn.itheima.oneToMany.Customer" table="t_customer">
    <id name="id" column="c_id">
    <generator class="identity" />
    </id>
    <property name="name" column="c_name" length="20" /> <!-- 一个客户关联多个订单 -->
    <!--
    使用set来描述在一的一方中关联的多,也即Set<Order>
    它的name属性就是Set集合的名称:orders
    key:它主要描述关联的多的一方产生的外键名称,注意要与多的一方定义的外键名称相同
    one-to-many:主要描述集合中的类型
    -->
    <set name="orders">
    <key column="c_customer_id" />
    <one-to-many class="cn.itheima.oneToMany.Order" />
    </set>
    </class>
    </hibernate-mapping>
    • 1
  • Order.hbm.xml

    <?xml version="1.0"?>
    <!DOCTYPE hibernate-mapping PUBLIC "-//Hibernate/Hibernate Mapping DTD 3.0//EN"
    "http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd">
    <hibernate-mapping>
    <class name="cn.itheima.oneToMany.Order" table="t_order">
    <id name="id" column="c_id">
    <generator class="identity" />
    </id>
    <property name="money" column="c_money" />
    <property name="receiverInfo" column="c_receiverInfo" length="50" />
    <!-- 多对一 -->
    <!--
    name属性它描述的是Order类中的一的一方的属性名称:Customer c;
    class属性代表的是一的一方的类型
    column属性描述的是一对多,在多的一方产生的外键的名称:c_customer_id
    -->
    <many-to-one name="c" class="cn.itheima.oneToMany.Customer" column="c_customer_id">
    </many-to-one>
    </class>
    </hibernate-mapping>
    • 1

测试双向关联保存

现在我们来测试保存的操作,在src目录下创建一个cn.itheima.test包,并在该包下编写一个OneToManyTest单元测试类,然后在该类中编写一个用于测试保存的操作,如下:

public class OneToManyTest {

    // 测试保存,如果我做的是双向的关联,没有用cascade去做级联,那么就存在一个浪费的环节。
@Test
public void test1() { // 1.得到Session
Session session = HibernateUtils.openSession();
session.beginTransaction(); // 2.操作
// 2.1创建客户
Customer c = new Customer();
c.setName("张三"); // 2.2创建两个订单
Order o1 = new Order();
o1.setMoney(1000d);
o1.setReceiverInfo("武汉"); Order o2 = new Order();
o2.setMoney(2000d);
o2.setReceiverInfo("上海"); // 2.3建立关系
// 2.3.1订单关联客户
o1.setC(c);
o2.setC(c);
// 2.3.2客户关联订单
c.getOrders().add(o1);
c.getOrders().add(o2); session.save(o1);
session.save(o2);
session.save(c); // 3.事务提交,并关闭session
session.getTransaction().commit();
session.close();
} }

测试test1方法,运行正常。其实上面测试保存的操作就是一种双向关联关系,如果做的是双向的关联,而没有用cascade去做级联,那么就存在一个浪费的环节,后面会讲。 
顺其自然地,我们就会想可不可以只保存订单或只保存客户就能完成保存的操作呢?答案是不言而喻的。下面我就来简单地讲讲。

测试单向关联保存

现在我们只想保存订单,然后顺便保存客户,可以预想到的是我们可能会这样写代码:

public class OneToManyTest {

    // 测试保存 --- 单向操作(只演示保存订单,然后顺便保存客户)
@Test
public void test2() { // 1.得到Session
Session session = HibernateUtils.openSession();
session.beginTransaction(); // 2.操作
// 2.1创建客户
Customer c = new Customer();
c.setName("张三"); // 2.2创建两个订单
Order o1 = new Order();
o1.setMoney(1000d);
o1.setReceiverInfo("武汉"); Order o2 = new Order();
o2.setMoney(2000d);
o2.setReceiverInfo("上海"); // 2.3建立关系
// 2.3.1订单关联客户
o1.setC(c);
o2.setC(c); session.save(o1); // 这时o1是一个持久化对象
session.save(o2); // 这时o2也是一个持久化对象,而我们的o1和o2也关联了Customer对象,又Customer对象是一个瞬时对象,所以
// 在这时操作时,会报异常。 // 3.事务提交,并关闭session
session.getTransaction().commit();
session.close();
} }

这时测试以上方法,会发现报如下异常:

org.hibernate.TransientObjectException: object references an unsaved transient instance - save the transient instance before flushing: cn.itheima.oneToMany.Customer
at org.hibernate.engine.internal.ForeignKeys.getEntityIdentifierIfNotUnsaved(ForeignKeys.java:279)
at org.hibernate.type.EntityType.getIdentifier(EntityType.java:455)
at org.hibernate.type.ManyToOneType.isDirty(ManyToOneType.java:281)
at org.hibernate.type.ManyToOneType.isDirty(ManyToOneType.java:291)
at org.hibernate.type.TypeHelper.findDirty(TypeHelper.java:296)
at org.hibernate.persister.entity.AbstractEntityPersister.findDirty(AbstractEntityPersister.java:4081)
at org.hibernate.event.internal.DefaultFlushEntityEventListener.dirtyCheck(DefaultFlushEntityEventListener.java:532)
at org.hibernate.event.internal.DefaultFlushEntityEventListener.isUpdateNecessary(DefaultFlushEntityEventListener.java:215)
at org.hibernate.event.internal.DefaultFlushEntityEventListener.onFlushEntity(DefaultFlushEntityEventListener.java:142)
at org.hibernate.event.internal.AbstractFlushingEventListener.flushEntities(AbstractFlushingEventListener.java:216)
at org.hibernate.event.internal.AbstractFlushingEventListener.flushEverythingToExecutions(AbstractFlushingEventListener.java:85)
at org.hibernate.event.internal.DefaultFlushEventListener.onFlush(DefaultFlushEventListener.java:38)
at org.hibernate.internal.SessionImpl.flush(SessionImpl.java:1282)
at org.hibernate.internal.SessionImpl.managedFlush(SessionImpl.java:465)
at org.hibernate.internal.SessionImpl.flushBeforeTransactionCompletion(SessionImpl.java:2963)
at org.hibernate.internal.SessionImpl.beforeTransactionCompletion(SessionImpl.java:2339)
at org.hibernate.engine.jdbc.internal.JdbcCoordinatorImpl.beforeTransactionCompletion(JdbcCoordinatorImpl.java:485)
at org.hibernate.resource.transaction.backend.jdbc.internal.JdbcResourceLocalTransactionCoordinatorImpl.beforeCompletionCallback(JdbcResourceLocalTransactionCoordinatorImpl.java:147)
at org.hibernate.resource.transaction.backend.jdbc.internal.JdbcResourceLocalTransactionCoordinatorImpl.access$100(JdbcResourceLocalTransactionCoordinatorImpl.java:38)
at org.hibernate.resource.transaction.backend.jdbc.internal.JdbcResourceLocalTransactionCoordinatorImpl$TransactionDriverControlImpl.commit(JdbcResourceLocalTransactionCoordinatorImpl.java:231)
at org.hibernate.engine.transaction.internal.TransactionImpl.commit(TransactionImpl.java:65)
at cn.itheima.test.OneToManyTest.test2(OneToManyTest.java:43)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(Unknown Source)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(Unknown Source)
at java.lang.reflect.Method.invoke(Unknown Source)
at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:50)
at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:47)
at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:325)
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:78)
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:57)
at org.junit.runners.ParentRunner$3.run(ParentRunner.java:290)
at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:71)
at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288)
at org.junit.runners.ParentRunner.access$000(ParentRunner.java:58)
at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:268)
at org.junit.runners.ParentRunner.run(ParentRunner.java:363)
at org.eclipse.jdt.internal.junit4.runner.JUnit4TestReference.run(JUnit4TestReference.java:86)
at org.eclipse.jdt.internal.junit.runner.TestExecution.run(TestExecution.java:38)
at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:459)
at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:675)
at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.run(RemoteTestRunner.java:382)
at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.main(RemoteTestRunner.java:192)
  • 1

这个异常代表一个持久化对象关联了一个瞬时对象。出现问题,就要着手解决,那又怎么解决呢?我们可以使用级联操作来解决上述的问题,我们现在要做的是保存订单时保存客户,那么需要在订单类的映射配置文件中进行修改:

<many-to-one name="c" class="cn.itheima.oneToMany.Customer" column="c_customer_id" cascade="save-update"></many-to-one>

设置了cascade=save-update,那么在保存订单时就可以自动将客户保存了。这时再次执行test2方法,发现一切正常。 
现在我们又想要在完成保存客户时,保存订单,那又该怎么做呢?答案是不言而喻的,用屁股想都能知道,在客户类的映射配置文件中进行修改:

<set name="orders" cascade="save-update">
<key column="c_customer_id" />
<one-to-many class="cn.itheima.oneToMany.Order" />
</set>

设置了cascade=save-update,那么在保存客户时就可以自动将订单保存了。这时我们在OneToManyTest单元测试类再编写一个方法进行测试:

public class OneToManyTest {

    // 测试保存 --- 单向操作(保存客户并自动保存订单)
@Test
public void test3() { // 1.得到Session
Session session = HibernateUtils.openSession();
session.beginTransaction(); // 2.操作
// 2.1创建客户
Customer c = new Customer();
c.setName("张三"); // 2.2创建两个订单
Order o1 = new Order();
o1.setMoney(1000d);
o1.setReceiverInfo("武汉"); Order o2 = new Order();
o2.setMoney(2000d);
o2.setReceiverInfo("上海"); // 2.3建立关系
// 2.3.1客户与订单的关联
c.getOrders().add(o1);
c.getOrders().add(o2); session.save(c); // 保存客户 // 3.事务提交,并关闭session
session.getTransaction().commit();
session.close();
} }

双向关联维护

我们在开发中要配置双向关联配置,这样就可以通过任意一方来操作对方。但在写操作代码时,尽量要进行单向关联,这样可以尽量减少资源浪费。在前面测试双向关联保存那一小节中,我讲到存在一个资源浪费的问题,在这一节我就会细讲。 
回到最初测试双向关联保存的位置,如果像那样测试test1方法,那么Eclipse控制台会打印如下sql语句:

Hibernate:
insert
into
t_order
(c_money, c_receiverInfo, c_customer_id)
values
(?, ?, ?)
Hibernate:
insert
into
t_order
(c_money, c_receiverInfo, c_customer_id)
values
(?, ?, ?)
Hibernate:
insert
into
t_customer
(c_name)
values
(?)
-------------------------自己加的分割线-------------------------------
Hibernate:
update
t_order
set
c_money=?,
c_receiverInfo=?,
c_customer_id=?
where
c_id=?
Hibernate:
update
t_order
set
c_money=?,
c_receiverInfo=?,
c_customer_id=?
where
c_id=?
Hibernate:
update
t_order
set
c_customer_id=?
where
c_id=?
Hibernate:
update
t_order
set
c_customer_id=?
where
c_id=?

发现有4条update语句,为什么会这样昵?原因非常简单,我们现在这个操作,是做了一个双向关联,那么订单和客户都会去维护c_customer_id这个 
外键,也就是说当我们插订单的时候,c_customer_id是没值的,因为还没有Customer,所以它插入的是一个null值。当我们插完订单以后,再去插入客户, 
客户有了,我们就需要对订单里面的c_customer_id去修改,所以就会出现这样一种情况。 
在双向关联中,会存在多余的update语句,这个存在虽然不影响我们的程序运行,但是会影响性能,因为它在浪费资源。所以在写操作代码时,尽量要进行单向关联,但如果我们非得进行双向关联呢?那又该怎么减少资源浪费呢?这时我们可以使用inverse属性来设置,双向关联时由哪一方来维护表与表之间的关系。通常我们都会在多的一方维护关联关系,所以最好由订单类来维护双向关联关系,那么客户类的映射配置文件应修改为:

<set name="orders" inverse="true">
<key column="c_customer_id" />
<one-to-many class="cn.itheima.oneToMany.Order" />
</set>
  • inverse的值如果为true,代表由对方来维护外键。
  • inverse的值如果为false,代表由本方来维护外键。

关于inverse的取值有这样一个原则:外键在哪一个表中,我们就让哪一方来维护外键。 
就这样简简单单修改之后,再次测试test1方法,Eclipse控制台会打印如下sql语句:

Hibernate:
insert
into
t_order
(c_money, c_receiverInfo, c_customer_id)
values
(?, ?, ?)
Hibernate:
insert
into
t_order
(c_money, c_receiverInfo, c_customer_id)
values
(?, ?, ?)
Hibernate:
insert
into
t_customer
(c_name)
values
(?)
-------------------------自己加的分割线-------------------------------
Hibernate:
update
t_order
set
c_money=?,
c_receiverInfo=?,
c_customer_id=?
where
c_id=?
Hibernate:
update
t_order
set
c_money=?,
c_receiverInfo=?,
c_customer_id=?
where
c_id=?
  • 1

可发现update语句是少了两条的。

一对多的对象导航

下面来看看一对多的对象导航问题,如图: 
(转)Hibernate关联映射——一对多(多对一)
下面我们编写代码来进行演示,在OneToManyTest单元测试类编写如下测试方法:

public class OneToManyTest {

    // 测试一对多导航问题
@Test
public void test4() {
// 1.得到Session
Session session = HibernateUtils.openSession();
session.beginTransaction(); // 操作
// 2.1创建客户
Customer c = new Customer();
c.setName("张三"); // 2.2创建两个订单
Order o1 = new Order();
o1.setMoney(1000d);
o1.setReceiverInfo("武汉"); Order o2 = new Order();
o2.setMoney(2000d);
o2.setReceiverInfo("上海"); Order o3 = new Order();
o3.setMoney(3000d);
o3.setReceiverInfo("天门"); // 描述关系
// o1要关联上c
o1.setC(c); // c要关联上o2、o3
c.getOrders().add(o2);
c.getOrders().add(o3); session.save(o1); // 3.事务提交,并关闭session
session.getTransaction().commit();
session.close();
} }

运行以上方法,可发现Eclipse控制台打印出如下sql语句:

Hibernate:
insert
into
t_customer
(c_name)
values
(?)
Hibernate:
insert
into
t_order
(c_money, c_receiverInfo, c_customer_id)
values
(?, ?, ?)
Hibernate:
insert
into
t_order
(c_money, c_receiverInfo, c_customer_id)
values
(?, ?, ?)
Hibernate:
insert
into
t_order
(c_money, c_receiverInfo, c_customer_id)
values
(?, ?, ?)

所以共插入4条记录。其他情况请读者自行测试。

级联删除

现在我们有这样一个需求:当我们删除一个客户时,应该将客户对应的订单也删除(实际开发中删除订单时,是不需要删除客户的)。首先查看一下客户类和订单类的映射配置文件:

  • Customer.hbm.xml

    <?xml version="1.0"?>
    <!DOCTYPE hibernate-mapping PUBLIC "-//Hibernate/Hibernate Mapping DTD 3.0//EN"
    "http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd">
    <hibernate-mapping>
    <class name="cn.itheima.oneToMany.Customer" table="t_customer">
    <id name="id" column="c_id">
    <generator class="identity" />
    </id>
    <property name="name" column="c_name" length="20" /> <!-- 一个客户关联多个订单 -->
    <!--
    使用set来描述在一的一方中关联的多,也即Set<Order>
    它的name属性就是Set集合的名称:orders
    key:它主要描述关联的多的一方产生的外键名称,注意要与多的一方定义的外键名称相同
    one-to-many:主要描述集合中的类型
    -->
    <set name="orders" inverse="true" cascade="save-update">
    <key column="c_customer_id" />
    <one-to-many class="cn.itheima.oneToMany.Order" />
    </set>
    </class>
    </hibernate-mapping>
  • Order.hbm.xml

    <?xml version="1.0"?>
    <!DOCTYPE hibernate-mapping PUBLIC "-//Hibernate/Hibernate Mapping DTD 3.0//EN"
    "http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd">
    <hibernate-mapping>
    <class name="cn.itheima.oneToMany.Order" table="t_order">
    <id name="id" column="c_id">
    <generator class="identity" />
    </id>
    <property name="money" column="c_money" />
    <property name="receiverInfo" column="c_receiverInfo" length="50" />
    <!-- 多对一 -->
    <!--
    name属性它描述的是Order类中的一的一方的属性名称:Customer c;
    class属性代表的是一的一方的类型
    column属性描述的是一对多,在多的一方产生的外键的名称:c_customer_id
    -->
    <many-to-one name="c" class="cn.itheima.oneToMany.Customer" column="c_customer_id" cascade="save-update">
    </many-to-one>
    </class>
    </hibernate-mapping>
    • 1

大家的映射配置文件是不是上面这样的,如果是的话,可以在OneToManyTest单元测试类编写一个方法进行测试:

public class OneToManyTest {

    @Test
public void test5() {
// 1.得到Session
Session session = HibernateUtils.openSession();
session.beginTransaction(); // 操作 → 删除订单时,不需要删除客户;当我们删除一个客户时,应该将客户对应的订单也删除
Customer c = session.get(Customer.class, 1); session.delete(c); // 删除客户,订单是否会删除昵? // 3.事务提交,并关闭session
session.getTransaction().commit();
session.close();
} }

我们在删除客户时,也要删除订单,如果没有做级联删除的话,那么这个操作是不被允许的,原因是为了维护数据完整性。如果我们想要完成操作,可以在客户类的映射文件中添加cascade="delete",如下:

<set name="orders" inverse="true" cascade="delete">
<key column="c_customer_id" />
<one-to-many class="cn.itheima.oneToMany.Order" />
</set>

delete-orphan用法

delete-orphan(删除孤儿) :删除与当前对象解除关系的对象。 
现举例来演示delete-orphan的用法,在OneToManyTest单元测试类编写如下一个方法:

public class OneToManyTest {

    @Test
public void test6() {
// 1.得到Session
Session session = HibernateUtils.openSession();
session.beginTransaction(); // 2.操作
// 得到客户
Customer c = session.get(Customer.class, 1);
// 得到客户的一个订单
Order o = session.get(Order.class, 1); c.getOrders().remove(o); // 从集合里面移除掉一个订单,能实现这个效果吗?不能 // 3.事务提交,并关闭session
session.getTransaction().commit();
session.close();
} }
  • 1

这时运行以上方法,发现id为1的订单并没有被删除。如果想起到我们预期的效果,可以在客户类的映射文件中添加cascade="delete-orphan",如下:

<set name="orders" inverse="true" cascade="delete-orphan">
<key column="c_customer_id" />
<one-to-many class="cn.itheima.oneToMany.Order" />
</set>

cascade总结

使用cascade可以完成级联操作,它的常用可取值:

  • none:这是一个默认值
  • save-update:当我们配置它时,底层使用save、update或saveOrUpdate完成操作,级联保存临时对象,如果是游离对象,会执行update
  • delete:级联删除
  • delete-orphan:删除与当前对象解除关系的对象
  • all:它包含了save-update、delete操作
  • all-delete-orphan:它包含了delete-orphan与all操作