6.2.1 Hibernate的单、双向关联

 问题:Hibernate单、双向关联与inverse属性
6.2.1  Hibernate的单、双向关联
设计师L并不理解在Hibernate中单向关联与双向关联有什么区别。于是他也就无法告诉开发人员,在配置实体间关系时究竟如何配置。从Hibernate的文档中来看,官方并没有特别告诉使用者哪种关联方式可靠。而且从L的实验结果来看,通常的一些行为使用单向关联与双向关联的结果一样。那么究竟该如何看待这两种关联方式呢?
6.2.2  Hibernate的单向关联常规实现
依旧从实体房间(Room)与实体人(UserInfo)的一对多关联来看(这里将使用基于外键的一对多,对于连接表将放在另一个论题中),其两个实体如图6.2所示。
 
图6.2  两个实体的类图
实体房间(Room)与实体人(UserInfo)的代码实现见例6.8。
例6.8:实体房间(Room.java)与实体人(UserInfo.java)
Room.java
public class Room {
     //房间实体的主键对应
     private long id;
     //房间号
     private String roomnumber;
     //房间名称
     private String name;
     //房间中的人(这是一个Set类型的集合)
     private Set users;
     //get/set方法
     …
}
UserInfo.java
public class UserInfo {
     //实体人的主键
     private long id;
     //人的名字
     private String name;
     //人的性别
     private String sex;
     //实体房间的主键
     private long roomid;
     //get/set方法
     …
}
针对实体房间(Room)与实体人(UserInfo)的代码以及数据库表的单向一对多,其Hibernate映射文件见例6.9。
例6.9:实体房间(Room.java)与实体人(UserInfo.java)的映射文件
Room.hbm.xml
          "-//Hibernate/Hibernate Mapping DTD 3.0//EN" "http:// hibernate. sourceforge.net/hibernate-mapping-3.0.dtd">
    
    
         
         
              
              
         
         
         
         
         
         
         
         
              
              
              
              
         
    
UserInfo.hbm.xml
          "-//Hibernate/HibernateMappingDTD3.0//EN""http://hibernate.sourceforge. net/hibernate-mapping-3.0.dtd">
    
    
         
         
              
              
         
         
          
         
         
          
         
         
    
6.2.3  单向关联的实现和问题
下面针对当前Hibernate映射文件给出了具体实现。为了代码完整,先给出Hibernate的配置文件,见例6.10。
例6.10:hibernate.cfg.xml
          "-//Hibernate/Hibernate Configuration DTD 3.0//EN"
          "http://hibernate.sourceforge.net/hibernate-configuration-3.0.dtd">
    
         
          XXXX
          jdbc:oracle:thin:@XXX:1521:XXX
          org.hibernate.dialect.Oracle9Dialect
          XXXX
          oracle.jdbc.driver. OracleDriver
         
          true
         
          true
         
          true
         
         
         
         
    
接着给出的是Hibernate与数据库交互的测试代码,见例6.11。
例6.11:执行插入表
     public void run() {
          //创建Room实体
          Room room = new Room();
          //设置Room.name
          room.setName("rwhome");
          //设置Room.roomnumber
          room.setRoomnumber("001");
          //创建UserInfo实体
          UserInfo userInfo = new UserInfo();
          //设置UserInfo.name
          userInfo.setName("rw");
          //设置UserInfo.sex
          userInfo.setSex("M");
          //创建UserInfo集合userInfoSet
          Set userInfoSet = new HashSet();
          //添加UserInfo实体到集合userInfoSet
          userInfoSet.add(userInfo);
          //设置Room.users(这是一个集合类型)
          room.setUsers(userInfoSet);
         
          //创建Hibernate Session
          Session session = HibernateSessionFactory.currentSession();
          //启动事务
          Transaction tx = session.beginTransaction();
          //持久化Room实体
          session.save(room);    
          session.flush();
          //打印出持久化状态的各实体内容
          System.out.println("RoomId:" + room.getId());
          System.out.println("Name:" + room.getName());
          System.out.println("Roomnumber:" + room.getRoomnumber());
          Iterator it = room.getUsers().iterator();
          while (it.hasNext()) {
               UserInfo userInfoin = (UserInfo)it.next();
               System.out.println("UserId:" + userInfoin.getId());
               System.out.println("Name:" + userInfoin.getName());
               System.out.println("Roomid:" + userInfoin.getRoomid());
               System.out.println("Sex:" + userInfoin.getSex());
          }
          //提交事务
          tx.commit();
          //关闭Hibernate Session
          HibernateSessionFactory.closeSession();
     }
由于在Hibernate映射文件中配置了级联(cascade="all"),因此只需要对Room实体进行持久化操作,会关联持久化UserInfo实体。
看起来现在一切都完好了,可惜这里却有一个小小的缺陷。这段代码在不同的前提下会生成两种结果。前提就是:room表与userinfo表是否存在外键关联。
(1)当room表与userinfo表未曾设置外键关联时,那么这段代码就是正确的,其执行后打印出来的SQL语句如下:
Hibernate:
    //对room表自增长字段(主键)的最大值获取
    select
        max(id)
    from
        room
Hibernate:
    //对userinfo表自增长字段(主键)的最大值获取
    select
        max(id)
    from
        userinfo
Hibernate:
/* 插入room表 */
 insert
        into
            room
            (NAME, roomnumber, id)
        values
            (?, ?, ?)
Hibernate:
/* 插入userinfo表,此时roomid字段为0 */
 insert
        into
            userinfo
            (NAME, SEX, roomid, id)
        values
            (?, ?, ?, ?)
Hibernate:
/* 通过一对多的关联映射更新userinfo表中当前记录的roomid字段
实现room表与userinfo表的关联 */
update
        userinfo
    set
        roomid=?
    where
        id=?
关于这段SQL的执行过程见注释。可以看到,Hibernate在处理一对多单向关联时,是通过三句SQL来完成的。首先是插入主表(room表),然后是插入子表(userinfo表),最后更新子表的关联字段(userinfo表的roomid字段)为主表的主键(room表的id字段)。
(2)当room表与userinfo表设置外键关联时,那么这段代码就是错误的,其执行后打印出来的SQL语句如下:
Hibernate:
/*插入room表*/
 insert
        into
            room
            (NAME, roomnumber, id)
        values
            (?, ?, ?)
Hibernate:
/*插入userinfo表,此时roomid字段为0*/
 insert
        into
            userinfo
            (NAME, SEX, roomid, id)
        values
            (?, ?, ?, ?)
2007-03-27 13:47:28,822 WARN [org.hibernate.util.JDBCExceptionReporter] - SQL Error: 2291, SQLState: 23000
2007-03-27 13:47:28,822 ERROR [org.hibernate.util.JDBCExceptionReporter] - ORA-02291: 违反完整约束条件 (TEST.FOREIGN) - 未找到父项关键字
和之前没有创建外键关联所不同的是,这里执行了第二句insert SQL后,由于插入roomid的时候插入的是0,而在主表room中没有这样的记录,因此会抛出“未找到父项关键字”的异常。为了解决这个问题,必须在映射文件上做文章。修改UserInfo.hbm.xml文件,使得其在插入userinfo表时不对roomid进行插入,也即保证该字段是个“外源性”的字段,见例6.12。
例6.12:修改后的UserInfo.hbm.xml
          "-//Hibernate/Hibernate Mapping DTD 3.0//EN" "http:// hibernate. sourceforge.net/hibernate-mapping-3.0.dtd">
    
    
         
         
              
              
         
         
         
         
         
         
         
         
         
         
    
关于修改的结果见注释,对于这样的改动,Hibernate执行的SQL语句如下:
Hibernate:
/*插入room表*/
 insert
        into
            room
            (NAME, roomnumber, id)
        values
            (?, ?, ?)
Hibernate:
/*插入userinfo表,此时由于设置了insert="false",所以roomid字段不被插入*/
 insert
        into
            userinfo
            (NAME, SEX, id)
        values
            (?, ?, ?)
Hibernate:
    /*通过一对多的关联映射更新userinfo表中当前记录的roomid字段
实现room表与userinfo表的关联*/
update
        userinfo
    set
        roomid=?
    where
        id=?
单向关联满足了一定的业务要求,但是当抓取UserInfo实体而又要同时抓取Room实体的业务时就无法被满足。此时就需要进行双向关联的设置。
6.2.4  Hibernate的双向关联常规实现
Hibernate的双向关联除了在主表(当前可以看作room表)的映射文件中设置一对多(one-to-many)外,还需要在从表(当前可以看作userinfo表)设置多对一(many-to-one)。首先需要在UserInfo.java实体类中增加一个Room实体类型的属性,其代码实现见例6.13。
例6.13:增加Room实体类型属性的UserInfo.java
public class UserInfo {
     //实体人的主键
     private long id;
     //人的名字
     private String name;
     //人的性别
     private String sex;
     //实体房间的主键(非必需的)
     private long roomid;
     //实体房间
     private Room room;
     public Room getRoom() {
          return room;
     }
     public void setRoom(Room room) {
          this.room = room;
     }
    //省略其他get/set方法
}
在保证Room实体的映射不变,即Room.hbm.xml不变的情况下,需要修改UserInfo.hbm.xml来实现多对一。其映射配置见例6.14。
例6.14:多对一配置的UserInfo.hbm.xml
          "-//Hibernate/Hibernate Mapping DTD 3.0//EN" "http://hibernate. sourceforge.net/hibernate-mapping-3.0.dtd">
    
    
         
         
              
              
         
         
         
         
         
          
         
    
完成所有这些之后双向关联就配置完毕。
6.2.5  双向关联的实现和问题
接着该是双向关联的实现了,将例6.11的实现代码直接使用,为了呈现结果,可以通过UserInfo实体获取Room实体,再增加一个事务来打印结果,其代码实现见例6.15。
例6.15:双向关联的实现代码
     public void run() {
          //与例6.4相同
          …
          //创建一个新事务来获取Room实体
          session = HibernateSessionFactory.currentSession();
          tx = session.beginTransaction();
         //根据room表的主键抓取Room持久化实体
          Room room1 = (Room)session.get(Room.class, room.getId());
         //打印各实体信息
          System.out.println("Name:" + room1.getName());
          System.out.println("Roomnumber:" + room1.getRoomnumber());
          System.out.println("Id:" + room1.getId());
         //抓取从表UserInfo实体的集合,并迭代打印结果
          Iterator it = room1.getUsers().iterator();
          while (it.hasNext()) {
               UserInfo userInfoin = (UserInfo)it.next();
               System.out.println("Id:" + userInfoin.getId());
               System.out.println("Name:" + userInfoin.getName());
               System.out.println("Roomid:" + userInfoin.getRoomid());
               System.out.println("Sex:" + userInfoin.getSex());
              //通过UserInfo实体的room属性获取Room实体的内容
               System.out.println("RoomId:" + userInfoin.getRoom().getId());
               System.out.println("Name:" + userInfoin.getRoom().getName());
               System.out.println("Roomnumber:"+userInfoin.getRoom(). getRoomnumber());
          }
          tx.commit();
          HibernateSessionFactory.closeSession();
     }
下面执行例6.11的插入代码,来完成双向关联,其SQL语句如下:
Hibernate:
/* 插入room表 */
 insert
        into
            room
            (NAME, roomnumber, id)
        values
            (?, ?, ?)
Hibernate:
/*插入userinfo表,此时roomid为空*/
 insert
        into
            userinfo
            (NAME, SEX, roomid, id)
        values
            (?, ?, ?, ?)
Hibernate:
/*通过一对多的关联映射更新userinfo表中当前记录的roomid字段
实现room表与userinfo表的关联*/
update
        userinfo
    set
        roomid=?
    where
        id=?
在执行例6.15的代码后打印结果如下:
Id:11117
Name:rw
Roomid:0
Sex:M
RoomId:25
Name:rwhome
Roomnumber:001
可以看到,通过Room实体获取UserInfo实体,再反向获取Room实体完成了。
只是这么实现的话,并没有达到最好的效果。因为SQL执行插入时总是要执行三句SQL,这样在效率上是有问题的。要达到效率上的提高就需要做另一个实现,那就是在配置文件中加入inverse属性。
6.2.6  inverse属性与双向关联
使用双向关联执行三句SQL的原因在于:插入room表后,需要插入根据一对多关联的userinfo表,但是插入userinfo表的前提是session.save(room);,也即通过Room实体来维护二者之间的关系。这也就意味着Room实体需要通过自身包含的UserInfo实体一一更新其外键,达到关联的目的。
而inverse属性就提供了另外一个更好的做法,它将关联关系反向交给UserInfo实体来完成,这也就意味着虽然通过session.save(room);来执行插入,但是却是由UserInfo实体来维护二者之间的关系。所做的更改有两个地方,首先是对Room. hbm.xml中一对多部分的修改,见例6.16。
例6.16:增加inverse属性的一对多
    
         
         
         
         
    
其次还需要在实现代码中,将UserInfo与Room实体的关系告诉UserInfo实体,也即让userinfo表的记录得到room表记录的主键。这段实现代码见例6.17。
例6.17:UserInfo实体参考Room实体
     public void run() {
          //创建Room实体
          Room room = new Room();
          //设置Room.name
          room.setName("rwhome");
          //设置Room.roomnumber
          room.setRoomnumber("001");
          //创建UserInfo实体
          UserInfo userInfo = new UserInfo();
          //设置UserInfo.name
          userInfo.setName("rw");
          //设置UserInfo.sex
          userInfo.setSex("M");
          //保证UserInfo实体得到与Room实体的关系,以帮助由UserInfo来维护外键关联
          userInfo.setRoom(room);
          //创建UserInfo集合userInfoSet
          Set userInfoSet = new HashSet();
          //添加UserInfo实体到集合userInfoSet
          userInfoSet.add(userInfo);
          //设置Room.users(这是一个集合类型)
          room.setUsers(userInfoSet);
         
          //创建Hibernate Session
          Session session = HibernateSessionFactory.currentSession();
          //启动事务
          Transaction tx = session.beginTransaction();
          //持久化Room实体
          session.save(room);     
          //提交事务
          tx.commit();
          //关闭Hibernate Session
          HibernateSessionFactory.closeSession();
     }
执行插入表操作,其显示出来的SQL语句如下:
Hibernate:
/* 插入room表 */
 insert
        into
            room
            (NAME, roomnumber, id)
        values
            (?, ?, ?)
Hibernate:
/*插入userinfo表,此时roomid通过UserInfo参考Room实体已经获取并插入了*/
 insert
        into
            userinfo
            (NAME, SEX, roomid, id)
        values
            (?, ?, ?, ?)
这样的SQL语句在批量插入userinfo表时效率高了许多,是双向关联中效率最高的一种插表方式。值得注意的是,执行插表语句中的userInfo.setRoom(room);必须写在代码中,否则SQL语句同样是执行两句插入,但是在userinfo表中将会插入一个为null的roomid。
6.2.7  结语
单向关联的功能比双向关联要弱,而且单向关联在操作数据库表时总是会执行三句SQL。因此在一般设计和实现中,通常应该优先选择使用双向关联。而使用双向关联时,inverse属性也是不能忽视的一个重点。通过多端来控制外键值的插入是值得推荐的。
 

你可能感兴趣的:(6.2.1 Hibernate的单、双向关联)