对象关系与数据表映射

时间:2022-02-14 12:21:57

Hibernate是当前流行的对象关系映射(ORM)框架,实现了程序对象到关系型数据库数据的映射。即然ORM实现的是对象和关系型数据表间的映射,它必然要在映射过程中解决对象层次结构中的关系问题。这里对映射关系作一个小结,以备以后查阅。

我们很多Web项目都是由前端页面驱动来进行架构设计,即首先作出Web原型后,并基于此原型来产生表结构。一般情况下,所产生的对象层次是扁平的,对应的表结构也很简单,表间通常没有复杂的关联关系。但也有很多时候,我们需要往先分析项目的业务,然后建立起业务模型,根据业务模型的描述分解出不同类型的对象类。在最后会考虑对其中需要持续化的实体对象建立相关的数据表。这是面向对象的分析方法。这时产生的表结构应该表现出对象间的关系。

啰嗦了几句,回过来我们看看在UML中的对象间关系。UML中的对象间的关系分为:继承、依赖、关联、聚合、组合。其中聚合和组合关系都属于是关联关系的特例,一般根据语义来区分,从代码层面上并不能有效区分,所以我们在关系映射时不作特别的区分。依赖关系在代码层面上表现为局部变量或方法的参数,以及对静态方法的调用,它不会以成员变量的形式存在于以来对象中,所以我们不对依赖关系进行映射。继承关系没得说,肯定是要处理的。

我们来看映射时对对象间关系是如何进行处理的。

1。对象继承关系的映射

Hibernate中支持3种形式的继承关系处理:

1).表与子类之间的一对一对应,父类的共性都写在每张子表里。

这种方案中,只有子表没有父表,所有子表都包括了父类属性对应的字段。这种映射方式的局限性在于,子表中的父类字段必须保持一致,一旦父类发生变动,所有子类表都必须进行修改。

   <class name="Users" abstract="true"> <!-- 没有父表 -->
      <!--不能使用class="native"-->
      <id name="userid"><generator class="assigned"/></id>
      <property name="pwd"/>
      <union-subclass name="Guest" table="guest"><!-- 子表定义,包含Guest子类的扩展属性 -->
         <property name="guestRemark"/>
      </union-subclass>
      <union-subclass name="Admin" table="admin"><!-- 子表定义,包含Admin子类的扩展属性 -->
         <property name="adminRemark"/>
      </union-subclass>
   </class>

   这里Hibernate使用union-subclass标签来定义子类的。同时,Union-subclass标签不需要包含key标签。需要注意的是,主键定义不能使用class="native",如果使用class="native",在创建guest和admin两个表时,hibernate不会为他们的主键设置AUTO_INCREMENT,所以在插入数据时,必须提供主键值,否则会出现错误。

2).每个子类对应一张子表,并与主类共享主表

这种方案中,父类与子类都独立映射成对应的主表和子表,子表中只包含子类所扩展的属性,同时,子类引用父类的主键或享用公共的字段或属性。这种映射方式的局限性在于,多表联接操作需要更大的性能消耗。

    <class name=”Users” table=”users”> <!-- 主表定义 -->
        <id name=”userid” column=”USERID” type=”string”>
            <generator class=”assigned”/>
        </id>
        <property name=”pwd” column=”pwd” type=”string” />
         <joined-subclass name=”Guest” table=”guest”> <!-- 子表定义 -->
             <key column=”USERID”/>
             <property name=”guestRemark” column=”guest_remark” type=”string” />
        </joined-subclass>
        <joined-subclass name=”Admin” table=”admin”> <!-- 子表定义 -->
             <key column=”USERID”/>
             <property name=”adminRemark” column=”admin_remark” type=”string” />
        </joined-subclass>
    </class>

   这里Hibernate是使用joined-subclass标签来指明子类,同时需要注意的是,Joined-subclass标签的name属性是子类的全路径名。Joined-subclass标签包含的key标签指明了子类和父类之间相关联的字段。

3).子类父类共用一个表,子类间通过类型(discriminator)字段加以区分

这种方案中,父类与子类都映射到同一张表中,子类的扩展属性和父类的公共属性成为表中的字段,但子表中的扩展字段可为空,子类间通过类型(discriminator)字段来加以区分。这种映射方式的局限性在于使用了冗余字段,需要更多的存储空间。

    <class name=”Users” table=”users” discriminator-value=”Users”> <!-- 仅有一张表 -->
       <discriminator column=”DISCRIMINATOR_USERTYPE” type=”string”/><!-- 冗余字段定义 -->
        <subclass name=”Guest” discriminator-value=”guest”> <!-- 子类冗余字段的取值 guest -->
           <property name=”guestRemark” column=”guest_remark” type=”string” />
        </subclass>
        <subclass name=”admin” discriminator-value=”admin”> <!-- 子类冗余字段的取值 admin -->
           <property name=”adminRemark” column=”admin_remark” type=”string” />
        </subclass>
    </class>

  这里在父类中定义一个discriminator标记来指定子类区分字段的名称和类型,在子类中使用<subclass>标签定义,在标签定义中用discriminator-value属性来表明子类的discriminator字段的值。在存储的时候hibernate会自动地将该category字段值插入到数据库中,而在加载数据时,hibernate就能根据这个category字段值加载相对应的对象。

 

2.对象关联关系的映射

对象间的关联关系有一对一、一对多、多对一或多对多,所以Hibernate中映射关系也主要处理一对一、一对多、多对一或多对多的关联关系。

1)一对一

一对一关联包括如下两种类型:主键关联和唯一外键关联。

      •   主键关联

一对一的主键关联方式是直接通过两个表的主键来形成一对一的映射关系,两张表共享相同的主键。

如:

      人员信息表person(personId编号、name姓名、sex姓别、。。。)

      员工表employee(工号empId、姓名name、部门depId)

      对于员工和员工信息进行主键一对一关联。首先要在employee对象的配置文件里定义到person对象的关联:

<class name="employee" class =”Employee” table="employee">
    <id name="empId" type="java.lang.Long">
      <column name="empId" />
      <generator class="identity" />
    </id>
    <property name="name" type="java.lang.String">
      <column name="name" not-null="true">
        <comment>姓名</comment>
      </column>
    </property>

….
    <!-- cascade="all":在保存employee对象的时候,级联保存关联的person对象    -->
    <one-to-one name=" person" class=”Person” cascade="all" />
  </class>

添加employee的时候,不会出现TransientObjectException异常,因为一对一主键关联映射中,默认了cascade属性。只有加上约束(constrained=true)后,从对象才和主对象建立关联,从对象的主键和外键是同一个对象,都是主对象的主键。

然后在person对象的配置文件中定义到employee对象的关联:

<class name="person" class="Person"  table=" person ">
    <id name="personId" type="java.lang.Long">
      <column name="personId" />
      <!-- 使用foreign 表示一对一主键映射中,使用另外一个相关联的对象的标识符,确保两张表的主键相等 -->
      <generator class="foreign">
        <param name="property"> employee </param>
      </generator>
    </id>
    <property name="name" type="java.lang.String">
      <column name="name" not-null="true">
        <comment>姓名</comment>
      </column>
    </property>
    <!-- 表示在person表存在一个外键约束,constrained属性表明主键同时作为外键参照参考相关联的表employee -->
    <one-to-one name=" employee " class="Employee" constrained="true" />
  </class>

这样就完成了通过主键关联关系的方式实现对象之间一对一。

 

小结:在Hibernate中,可以通过“foreign”类型的主键生成器与外键共享主键值。同时,one-to-one节点的constrained属性必须设定为“true”,以告知Hibernate当前表主键上存在一个约束:关联对象(被动方person)引用主动方(employee)的主键。

在关联对象的<one-to-one>配置时的属性是不同的,因为主动的一方是employee,被动的一方是person,在主动方employee的<one-to-one>节点上要求一个配置cascade,cascade为all表示表示主动一方无论执行任何操作,其关联对象执行同样的操作

      •   唯一外键关联

Hibernate中的唯一外键关联由“many-to-one”节点定义,一个表的某个外键字段关联到另一个表的主键上。表示被动方唯一从属于主动方的一个属性组。唯一外键关联的一对一关系只是多对一关系的一个特例。

如:

      员工表employee(工号empId、姓名name、部门depId)

      员工信息表person(personId编号、工号empid、name姓名、sex姓别、。。。)

其中,person中的empid与员工表中的employee中的empId关联。

首先配置person对象的映射文件(<many-to-one name="employee" class="employee" column="empId" unique="true" />)

    <class name="person" table="person">  

        <id name="personId" type="java.lang.Long">  

            <column  name=" personId " />  

            <generator class="identity"></generator>  

        </id>  

        <property name="name" type="java.lang.String">  

            <column name="name" />  

        </property>  

        <!--column="gradeClassId" 表示的是user表中和班级表关联字段gradeClassId -->  

        <many-to-one name="employee" column="empId" class="employee" unique="true" />      

    </class> 


在这里就形成了一个person对象到employee对象的单向一对一关系。
接下来在employee对象的配置文件进行相应的配置实现对象之间的双向一对一关联关系。
    <class name="employee" table="employee">          

        <id name="personId" column="personId_id" type=" java.lang.Long " >

                <generator class="increment" />

        </id>

        <property name="empid" column="empid" type=”java.lang.Long” />          

        <property name="name" column="name" type=”java.lang.String” />

        <one-to-one name="person" class="Person" property-ref=" employee " />

    </class>

 如果只需要单向一对一关系,则将employee对象配置文件中的<one-to-one/>配置去除就是了。

 

2)一对多关联

一对多关联在Hibernate中是通过one-to-many节点来声明的。对于one-to-many关联关系,采用java.util.Set(或org.sf.hibernate.collection.Bag)类型的Collection,在XML映射文件中表现为<set>..</set>(或<bag>….</bag>)节点。

一对多关联分为单向一对多关系和双向一对多关系。

单向一对多关系只需在“一”方进行配置,双向一对多关系需要在关联双方均加以配置。

       •   单向一对多关系

单向: 在多的一端加入一个外键指向一的一端,它维护的关系是一指向多

加载时: 加载一的一端时,会把多的一端的数据也都加载上(相当于执行两次查找操作)
如:

      人员信息表person(personId编号、name姓名、sex姓别、。。。)

      员工表employee(工号empId、姓名name、部门depId)

      部门表 department(depId编号、name部门名称。。。)

在一的一端:

    <class name="employee" class="employee" table="employee">         

        <id name="empId" type="java.lang.Long">
            <column name="empId" />
            <generator class="identity" />
        </id>

        <set name="department" inverse="false" cascade="save-update">
            <key column="depId" not-null="true"/>
            <one-to-many class="department" />
        </set><!-- depId为department表的外键 reference 到 user的主键-->           

        <property name="name" column="name" type=”java.lang.String” />

    </class>

在多的一端:

    <class name="department" class="department" table="department”>
        <id name="depId" type="java.lang.Long">
            <column name="depId"/>
            <generator class="identity" />
        </id>
        <property name="name" type="string">
            <column name="name"/>
        </property>
    </class>

 

单向一对多关联存在的问题是:只能通过主控方对被动方进行级联更新来保持关联关系。如果被关联方的关联字段为“NOT NULL“,当Hibernate创建或更新关联关系时,可能出现约束违例。

针对这个问题,我们可以在设计时借助一些方式进行调整,以避免这样的约束违例。如将关联字段设置为允许为NULL值,或将被动方的关联字段从其映射文件中剔除等。单向一对多关联中,对被动方的数据插入,Hibernate的实现机制采用了两条SQL语句进行一次数据插入操作。第一个SQL语句用于插入新记录,第二条SQL语句用于更新关联字段的值。针对这种情况,双向一对多关系可以解决两条SQL的问题。

 

 

      •   双向一对多关联

双向: 在一的一端的集合上使用,在对方表中加入一个外键指向一的一端
双向一对多关联实际上是“一对多”与“多对一”关联的组合。即必须在主控方配置单向一对多关系的基础上,在被动方配置与其对应的多对一关系。

如:

      员工表employee(工号empId、姓名name、部门depId)

      部门表 department(depId编号、name部门名称。。。)

在一的一端:

    <class name="employee" class="employee" table="employee">         

        <id name="empId" type="java.lang.Long">
            <column name="empId" />
            <generator class="identity" />
        </id>

        <set name="department" inverse="false" cascade="save-update">
            <key column="depId" not-null="true"/>
            <one-to-many class="department" />
        </set><!-- depId为department表的外键 reference 到 user的主键-->           

        <property name="name" column="name" type=”java.lang.String” />

    </class>

 

在多的一端:

<class name="department" class="department" table="department”>
        <id name="depId" type="java.lang.Long">
            <column name="depId"/>
            <generator class="identity" />
        </id>
        <property name="name" type="string">
            <column name="name"/>
        </property>

    <many-to-one name=" employee" column="empId" class="employee"/>
    </class>

 

在one-to-many关系中,将many一方设置为主控方(inverse=false)将有助性能的改善。

 

Hibernate术语“Inverse”,指定关联关系中的方向。关联关系中,inverse=“false”的为主动方,由主动方负责维护关联关系。

Inverse和cascade的区别:inverse指的是关联关系的控制方向,而cascade指的是层次之间的连锁操作

标签指定的外键字段必须和指定的外键字段一致,否则引用字段的错误。

一对多的关系一般采用双向的,在一的一端设置inverse属性,把关联关系交给多的一端,避免发出多余的update。
     inverse: 配置在主控方,指定外键的关系有谁控制
         inverse=false 是主控方,外键是由它控制的
         inverse=true   是被控方,外键与它没关系
             要想实现主控方的控制必须将被控方作为主控方的属性
    cascade: 级联, 主表增从表增;主表修从表修;主表删从表删

    lazy: 延迟
         lazy=false:一下将所有的内容取出,不延时(常用)
         lazy=true:  取出部分内容,其余内容动态去取
通过get可以取出对方的所有内容

 

 

3)多对一

多对一: 会在多的一端加入一个外键,指向一的一端
Hibernate多对一单向映射和一多都是为了完成同一个目的,只不过处理的方式不一样,改变的只是管理的方向,在一对多里面设置主控方在一方,而在多对一里只是改变了控制的主体,把控制方交给多方了。

通过many-to-one元素,可以定义一种常见的与另一个持久化类的关联.这种关系模型是多对一关联:这个表的一个外键引用目标表的主键字段

 

       •   单向多对一关系

在一的一端:

    <class name="employee" class="employee" table="employee">         

        <id name="empId" type="java.lang.Long">
            <column name="empId" />
            <generator class="identity" />
        </id>              

        <property name="name" column="name" type=”java.lang.String” />

    </class>

在多的一端:

<class name="department" class="department" table="department”>
        <id name="depId" type="java.lang.Long">
            <column name="depId"/>
            <generator class="identity" />
        </id>
        <property name="name" type="string">
            <column name="name"/>
        </property>

        <many-to-one name="employee" not-null="true" cascade="save-update"
            class="employee" column="empId" lazy="false">
        </many-to-one>
    </class>

 

       •   双向多对一关系

在配置形式上与双向一对多关系相同。

 

4)多对多

多对多(many-to-many):在操作和性能方面都不太理想,所以多对多的映射使用较少,实际使用中我们都是采用引入第三方表来描述它们之间的关联的。

多对多关联需要借助中间表完成多对多映射信息的保存。

由于多对多关联的性能不佳(由于引用中间表,一次读取操作需要反复数次查询),因此在设计中应该避免大量使用。同时,在多对多关系中,应根据情况,采取延迟加载(Lazy Loading)机制来避免无谓的性能开销。

多对多关系中,由于关联关系是两张表相互引用,因此在保存关联状态时必须对双方同时保存。

数据关联机制相关的其他技术关注点,如集合、延迟加载、以及关联读取的操作方式等参见相关内容。

 

       •   单向多对多关系

如果要求拿到用户需要知道它的角色,而不去关心反向的加载。那么这个就是单向的。

如:

      员工表employee(工号empId、姓名name。。)

      角色表 role(roleId编号、name名称。。。)

 

    <class name="employee" class ="employee" table="employee">  

        <id name="empId">  

            <generator class="native"/>  

        </id>  

        <property name="name"/>  

        <set name="roles" table="employee_role">  

            <key column="roleId"/>  

            <many-to-many class=" role" column="roleId"/>          

        </set>  

    </class>  

 

    <class name="role" class="role" table="role">  

        <id name="roleId">  

            <generator class="native"/>  

        </id>  

        <property name="name"/>  

    </class>  

这里面employee实体持有一个role的set集合,使用第三方表employee_role把两个表的主键关联起来.

 

 

       •   双向多对多关系

双向多对多就是双方都运用对方的一个引用。在任何一方加载的时候都会自动加载与其关联的另一端数据。

    <class name="role" class="role" table="role">  

        <id name="roleId">  

            <generator class="native"/>  

        </id>  

        <property name="name"/>  

        <set name="employee" class=”employee” table="employee_role">  

            <key column="empId" />  

            <many-to-many class="employee" column="empId"/>  

        </set>

    </class>  

一定要注意映射文件中<many-to-many class="employee" column="empId"/>中class的值,它必须与你另一个关联映射文件中的class属性的name值一致,其实就是与你的实体类的类名一致,不一致将会抛出异常:An association from the table employee_role refers to an unmapped class:

 

其实多对多就是两个一对多,它的配置没什么新奇的相对于一对多。在多对多的关系设计中,一般都会使用一个中间表将他们拆分成两个一对多。<set>标签中的"table"属性就是用于指定中间表的。中间表一般包含两个表的主键值,该表用于存储两表之间的关系。由于被拆成了两个一对多,中间表是多方,它是使用外键关联的,<key>是用于指定外键的,用于从中间表取出相应的数据。中间表每一行数据只包含了两个关系表的主键,要获取与自己关联的对象集合,还需要取出由外键所获得的记录中的另一个主键值,由它到对应的表中取出数据,填充到集合中。<many-to-many>中的"column"属性是用于指定按那一列的值获取对应的数据。

 

工具使用

上面描述了对象关系映射,但在实际操作中,我们可以借助一些工具来做这些事情。虽然工具可以做这些事,但理解它的映射关系还是非常必要的。要解决的映射工作无非一是已经存在有数据表,从数据表导出对象映射关系,包括POJO和hbm文件,一是从已有的POJO来生成对象映射关系,另一个就是从已有的对象映射文件来生成相关的POJO文件。我在工作中一般使用Middlegen来处理映射关系。

 

 

 

 

 

 

[注]

Hibernate的核心接口一共有5个,分别为:Session、SessionFactory、Transaction、Query和Configuration。通过这些接口实现了对持久化对象进行存取和事务控制。
    •Session接口: Session接口负责执行被持久化对象的CRUD操作。同时,Session对象是非线程安全的。
  •SessionFactory接口: SessionFactroy接口负责初始化Hibernate,充当数据源代理,并负责创建Session对象。SessionFactory不是轻量级的,通常一个项目中只需要一个SessionFactory。当跨多个数据库存储时,可以为每个数据库创建一个SessionFactory。
  •Configuration接口: Configuration接口负责配置并启动Hibernate,创建SessionFactory对象。
  •Transaction接口: Transaction接口负责事务相关的操作。
  •Query和Criteria接口: Query和Criteria接口负责执行各种数据库查询。它支持HQL语言或SQL语句两种表达方式。

工作原理:

Hibernate通过映射文件获得对象对应的数据表名以及数据字段,然后通过反射机制持久化对象的各个属性,最后生成向数据库插入的SQL insert语句。在调用了session.save()方法后,POJO对象会标识成持久化状态存放在session中,此时,这个对象就是一个持久化了的对象,但同时Hibernate还未执行insert语句,只有当session的刷新同步或事务提交时,Hibernate才会把session缓存中的所有SQL语句一起执行。更新、删除操作采用类似的机制。在事务提交成功后,所有写操作就会被永久地保存进数据库中。如果设置了二级缓存,这些操作还会同步到二级缓存中。

具体的工作过程如下:
1. 读取并解析配置文件
2. 读取并解析映射信息,创建SessionFactory
3. 打开Sesssion
4. 创建事务Transation
5. 持久化操作
6. 提交事务
7. 关闭Session
8. 关闭SesstionFactory

Hibernate编程步骤

1. 编写domain类:一些属性和get、set方法

2. 编写映射文件.hbm.xml:把java对象和关系模型对应起来。

3. 编写配置文件 hibernate.cfg.xml:用于配置Hibernate,初始化时首先读取此配置文件。

4. 编程代码:代码的一般样式如下:

//创建Configeration类的实例,它的构造方法:将配置信息(Hibernate config.xml)读入到内存。一个Configeration 实例代表Hibernate 所有Java类到Sql数据库映射的集合。

Configuration config = new Configuration().configure();

//创建SessionFactory实例,把Configeration 对象中的所有配置信息拷贝到SessionFactory的缓存中,SessionFactory的实例代表一个数据库存储员源,创建后不再与Configeration 对象关联。

SessionFactory sf = conf.buildSessionFactory();  

//调用SessionFactory创建Session的方法

Session session = sf.openSession();  

//通过Session 接口提供的各种方法来操纵数据库访问。

try{  

  Transaction tx = session.beginTransaction();  

  User user = new User();  

  user.setName("wuyuan");  

  user.setBirthday(new Date());  

  session.save(user);  

  tx.commit();  

}catch(Exception e){  

  e.printStackTrace();  

} finally{  

  session.close();  

  session = null;  

}