Hibernate映射关系

映射组成关系

建立域模型和关系型数据库模型有着不同的出发点
域模型:由程序代码组成,通过细化持久化类的粒度可以提高代码的可重用性,简化编程。

在没有数据冗余的情况下,尽可能的减少表的数量,简化表之间的参照关系,以便提高数据的访问速度。

映射组成关系

Hibernate把持久化类的属性分为两种:

  • 值(value)类型:没有OID,不能被单独持久化,生命周期依赖于所属的持久化类的对象生命周期。
  • 实体(entity)类型:有OID,可以被单独持久化,有独立的生命周期。

示例:

首先创建被参照对象

public class Pay {
    private int monthlyPay;
    private int yearPay;
    private int vocationWithPay;

    //Getter and setter.
}

参照对象

public class Worker {
    private Integer id;
    private String name;

    private Pay pay;

    //Getter and setter.
}

配置文件

<hibernate-mapping>
    <class name="com.hibernate.session.Worker" table="worker">
        <id name="id" type="java.lang.Integer">
            <column name="ID"/>
            <generator class="native"/>
        </id>
        <property name="name" type="java.lang.String">
            <column name="NAME"/>
        </property>
        <component name="pay" class="com.hibernate.session.Pay">
            <property name="monthlyPay" column="MONTHLY_PAY"></property>
            <property name="yearPay" column="YEAR_PAY"></property>
            <property name="vocationWithPay" column="VOCATION_WITH_PAY"></property>
        </component>
    </class>
</hibernate-mapping>

无法直接使用<property>映射对象属性。需要使用<component>标签映射组成关系,该元素的表名pay属性是Worker类的一个组成部分。在Hibernate中称为组件。

class:设定关系属性的类型。
parent:指定属性所属的整体类。其中的name指定整体类在组件类中的属性名。
运行测试类

Worker worker = new Worker();
Pay pay = new Pay();

pay.setMonthlyPay(1000);
pay.setYearPay(80000);
pay.setVocationWithPay(5);

worker.setName("HFR");
worker.setPay(pay);

session.save(worker);

运行成功后我们发现,数据成功的插入到了数据库。


一对多的关联关系

以Customer和Order为例,一个用户能够发出多个订单,而一个订单只能属于一个用户。这就是一个一对多的关系。

单向n-1

单向n-1关联,只需要从n的一端就可以访问1的一端。
域模型:从Order到Customer的多对一单向关联需要在Order类中定义一个Customer属性,而在Customer类中无需定义存放Order对象的属性集合。


关系型数据库模型:ORDERS表中的CUSTOMER_ID参照CUSTOMER表的主键。


Hibernate示例:

首先创建Customer对象

public class Customer {
    private Integer customerId;
    private String customerName;

    //Getter and setter
}

之后是Order类型

public class Order {
    private Integer orderId;
    private String orderName;

    private Customer customer;

    //Getter and setter
}

编写hbm.xml文件

<hibernate-mapping>
    <class name="com.hibernate.n21.Customer" table="customer">
        <id name="customerId" type="java.lang.Integer">
            <column name="COSTUMER_ID"/>
            <generator class="native"/>
        </id>
        <property name="customerName" type="java.lang.String">
            <column name="CUSTOMER_NAME"/>
        </property>
    </class>
</hibernate-mapping>
<hibernate-mapping package="com.hibernate.n21">
    <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="customer" class="Customer" column="CUSTOMER_ID" fetch="join">
        </many-to-one>
    </class>
</hibernate-mapping>

<many-to-one>标签解释:
使用<many-to-one>对应多对一的关联关系。
name:多的一端关联的一的一端的属性名。
class:一的一端的属性对应的类型。
column:一那一端在多的一端对应的数据表中的外键的名称。
在hibernate.cfg.xml中加入配置

<mapping resource="com/hibernate/n21/Customer.hbm.xml"/>
<mapping resource="com/hibernate/n21/Order.hbm.xml"/>

测试类,进行保存数据操作

Customer customer = new Customer();
customer.setCustomerName("HFR");

Order order1 = new Order();
order1.setOrderName("order1");

Order order2 = new Order();
order2.setOrderName("order2");

order1.setCustomer(customer);
order2.setCustomer(customer);

session.save(customer);
session.save(order1);
session.save(order2);

几个需要注意的点

  1. 先插入1的一端,再插入多的一端。
    如果先插入n的一端,再插入1的一端,则会多出update语句。由于在插入多的一端时还无法确定外键值,只有在插入1的一端之后才能确定外键值。所以会多出update。Hibernate会在插入时把缺少的外键值设为null,等插入1的一端后有了外键值,再去执行update。这是多出update原因的根本原因。所以推荐先插入1的一端,再插入多的一端。

  2. 查询功能:
    若只查询多的一端的对象,则不会将其关联的1的对象查到。
    在需要使用到关联的对象值,才发送对应的SQL语句。
    由多的一端导航到1的一端时,若此时session关闭,一定会发生懒加载异常(LazyInitException)。
    获取Order对象及关联的Customer对象时,其关联的Customer对象是一个代理对象。直到需要用的时候才会对代理对象进行初始化。这就是所说的懒加载。

  3. 级联问题
    在不设定级别关系的情况下,不能直接删除1的一端的对象且1这一端的对象有多个对象在引用则不能删除。

双向1-n

双向1-n和双向n-1是两种完全相同的情形。
双向1-n需要在1的一端能够访问n的一端。反之亦然。
域模型:从Order到Customer的多对一双向关联需要在Order类定义一个Customer属性,而在Customer类中需要定义存放Order对象的属性集合。



完善Customer类

public class Customer {
    private Integer customerId;
    private String customerName;

    private Set<Order>orders = new HashSet<>();

    //Getter and setter
}

完善hbm.xml

<hibernate-mapping>
    <class name="com.hibernate.n21both.Customer" table="customer">
        <id name="customerId" type="java.lang.Integer">
            <column name="COSTUMER_ID"/>
            <generator class="native"/>
        </id>
        <property name="customerName" type="java.lang.String">
            <column name="CUSTOMER_NAME"/>
        </property>
        
        <!-- table:set中元素对应的记录放在那一个数据表中,该列的值需要和n对1中n的表的名字一致 -->
        <set name="orders" table="orders">
            <!-- n的表中的外键名 -->
            <key column="CUSTOMER_ID"></key>
             <!-- 指定映射类型 -->
            <one-to-many class="com.hibernate.n21both.Order"/>
        </set>
    </class>
</hibernate-mapping>

测试类

Customer customer = new Customer();
customer.setCustomerName("HFR");

Order order1 = new Order();
order1.setOrderName("order1");

Order order2 = new Order();
order2.setOrderName("order2");

customer.getOrders().add(order1);
customer.getOrders().add(order2);

session.save(customer);
session.save(order1);
session.save(order2);

执行后我们发现,执行了3条insert语句,2条update语句。为什么会多出2条update语句呢?

因为1的一端和n的一端都维护关联关系,所以多出了两条update。如果希望不执行多余的update,则需要1的一端不维护关联关系。

可以使用set集合的inverse属性,让1的一端放弃维护关联关系。

下面来详细解释一下<set>元素的inverse属性:
在hibernate中通过对inverse属性来决定由双向关联的哪一方维护表与表之间的关系,inverse为false的则为主动方。inverse为true的则为被动方。由主动方负责维护关联关系。

在没有设置inverse为true的情况下,父子两方都维护关联关系。

在1-n的关系中,将n的一方设为主动方有助于性能的改善。就像如果要求国家元首把全国人民的名字都记住,这是不现实的,但要全国人民知道国家元首的名字则会很容易。

在1-n的关系中,将1设为主动方:会额外多出很多update语句。插入数据时无法同时插入外键列,因而无法向外键列添加非空约束。

可以在1的一端的set结点指定inverse为true,使1的一端放弃维护关联关系。建议先插入1的一端,后插入n的一端,这样不会多出update语句。

Customer customer = session.get(Customer.class, 3);
System.out.println(customer);

对n的一端的集合使用延迟加载。返回的n的一端的集合是Hibernate内置的集合类型。该类型具有延迟加载和存放外部对象的功能。

我们输出customer中orders的类型,我们发现是这种类型

class org.hibernate.collection.internal.PersistentSet

因此声明集合类型时需要使用接口类型。因为Hibernate在获取集合类型时返回的是HIbernate内置的集合类型,而不是JavaSE的标准的实现。所以,需要在声明orders的时候将其声明为Set类型。而不能是HashSet类型,因为HashSet不是PersistentSet的子类。

private Set<Order>orders = new HashSet<>();

可能会抛出LazyLoadingException。会在sesssion.close()的时候抛出。

同样的,由于是懒加载,只会在需要使用集合中元素的时候进行初始化。

一对一的关联关系

基于外键的一对一映射查询,外键可以放在任意一端,在需要存放外键的一端添加many-to-one元素,为many-to-one元素添加unique="true"的属性。另一端使用one-to-one元素,该元素使用property-ref属性指定被关联实体主键以外的字段作为关联字段。

在对应的表中已经有了外键,当前持久化类使用one-to-one进行映射。
首先创建两个类Manger和Department,它们之间是一对一的对应关系。
Manger

public class Manager {
    private Integer mgrId;
    private String mgrName;

    private Department dept;

    //Getter and setter
}

Department

public class Department {
    private Integer deptId;
    private String deptName;

    private Manager manager;

    //Getter and setter
}

hbm.xml文件

<hibernate-mapping package="com.hibernate.one2one">
    <class name="Manager" table="manager">
        <id name="mgrId" type="java.lang.Integer">
            <column name="MGR_ID"/>
            <generator class="native"/>
        </id>
        <property name="mgrName" type="java.lang.String">
            <column name="MGR_NAME"/>
        </property>
        <one-to-one name="dept" class="Department"></one-to-one>
    </class>
</hibernate-mapping>
<hibernate-mapping package="com.hibernate.one2one">
    <class name="Department" table="department">
        <id name="deptId" type="java.lang.Integer">
            <column name="DEPT_ID"/>
            <generator class="native"/>
        </id>
        <property name="deptName" type="java.lang.String">
            <column name="DEPT_NAME"/>
        </property>
        <many-to-one name="manager" class="Manager" column="MGR_ID" unique="true"></many-to-one>
    </class>
</hibernate-mapping>

运行程序之后,将会在数据库中创建相关的表。
之后我们要执行的是保存操作。

Department department = new Department();
department.setDeptName("Dept-A");

Manager manager = new Manager();
manager.setMgrName("MGR-A");

department.setManager(manager);
manager.setDept(department);

session.save(manager);
session.save(department);

建议先保存没有外键列的对象,这样会减少update语句。

Department dept = session.get(Department.class, 12);
System.out.println(dept.getDeptName());

默认情况下对关联属性使用懒加载。所以会出现懒加载异常的问题。

当我们需要懒加载一个对象的时候

Manager mgr = dept.getManager();
System.out.println(mgr.getMgrName());

虽然顺利的获取了对象,但是当我们查看SQL语句的时候发现了有些地方不对



查询Manger对象的连接条件应该是Manger表的MGR_ID等于DEPARTMENT的MGR_ID。这里确是Manger的MGR_ID等于DEPARTMENT的DEPT_ID。

<one-to-one name="dept" class="Department" property-ref="manager"></one-to-one>

由于我们使用了property-ref。此时连接查询的时候我们会使用dept的manager列对应的属性作为连接条件。没使用该属性的情况我们使用dept的主键作为连接条件。

没有外键的一端使用one-to-one元素,该元素使用property-ref属性指定关联实体主键以外的字段作为关联字段。

在查询没有外键的实体对象时,使用的是左外连接查询,一并查询出关联的对象并进行初始化。

基于主键映射的1对1关系

基于主键的映射策略,指一端的主键生成器使用foreign策略,表明根据对方的主键来生成自己的主键,自己并不需要独立生成主键。<param>属性指定使用当前持久化类的主键作为“对方”。

采用foreign主键生成器策略的一端使用one-to-one元素映射关联属性,one-to-one元素还应该增加constrained=“true“属性,另一端增加one-to-one元素映射关联属性。相当于添加外键约束。

示例

依然使用上述的类,只是配置文件(hbm.xml)有所不同。

<hibernate-mapping package="com.hibernate.primary">
    <class name="Manager" table="managers">
        <id name="mgrId" type="java.lang.Integer">
            <column name="MGR_ID"/>
            <generator class="native"/>
        </id>
        <property name="mgrName" type="java.lang.String">
            <column name="MGR_NAME"/>
        </property>
        <one-to-one name="dept" class="Department"></one-to-one>
    </class>
</hibernate-mapping>
<hibernate-mapping package="com.hibernate.primary">
    <class name="Department" table="departments">
        <id name="deptId" type="java.lang.Integer">
            <column name="DEPT_ID"/>
            <generator class="foreign">
                <param name="property">manager</param>
            </generator>
        </id>
        <property name="deptName" type="java.lang.String">
            <column name="DEPT_NAME"/>
        </property>
        <one-to-one name="manager" class="Manager" constrained="true"></one-to-one>
    </class>
</hibernate-mapping>

插入操作

Department department = new Department();
department.setDeptName("Dept-A");

Manager manager = new Manager();
manager.setMgrName("MGR-A");

department.setManager(manager);
manager.setDept(department);

session.save(manager);
session.save(department);

无论先插入那一个都不会有多余的update操作。
主键列不能设置为null,因此必须先插入有效的主键。这也是不会产生多余的update的原因。

多对多的关联关系

域模型:



关系数据模型:



n-n的关联必须使用中间表
与1-n映射类似,必须为set集合元素添加key子元素。指定CATEGORIES_ITEMS表中参照CATEGORIES表的外键为CATEGORY_ID,与1-n关联映射不同的是,建立n-n关联时,集合中所用的元素为many-to-many。many-to-many的子元素使用的是class属性,指定items集合中存放的是Item对象。column属性指定CATEGORIES_ITEMS表中参照Items表中的外键为ITEM_ID。

示例:

Category类

public class Category {
    private Integer id;
    private String name;
    private Set<Item> items = new HashSet<>();

    //Getter and setter
}

Item类

public class Item {
    private Integer id;
    private String name;

    //Getter and setter
}

配置文件Item.hbm.xml

<hibernate-mapping package="com.hibernate.n2n">
    <class name="Item" table="items">
        <id name="id" type="java.lang.Integer">
            <column name="ID"/>
            <generator class="native"/>
        </id>
        <property name="name" type="java.lang.String">
            <column name="NAME"/>
        </property>
    </class>
</hibernate-mapping>

category.hbm.xml

<hibernate-mapping package="com.hibernate.n2n">
    <class name="Category" table="category">
        <id name="id" type="java.lang.Integer">
            <column name="ID"/>
            <generator class="native"/>
        </id>
        <property name="name" type="java.lang.String">
            <column name="NAME"/>
        </property>
         <!-- table指定中间表 -->
        <set name="items" table="category_item">
            <key>
                <column name="Category_Id"/>
            </key>
            <!-- 使用many-to-many指定多对多关系,column执行Set在持久化类中在中间表的外键类名称 -->
            <many-to-many class="Item" column="Item_Id"/>
        </set>
    </class>
</hibernate-mapping>

测试类

Category category1 = new Category();
category1.setName("Category 1");

Category category2 = new Category();
category2.setName("Category 2");

Item item1 = new Item();
item1.setName("item1");
Item item2 = new Item();
item2.setName("item2");

category1.getItems().add(item1);
category1.getItems().add(item2);
category2.getItems().add(item1);
category2.getItems().add(item2);

session.save(category1);
session.save(category2);
session.save(item1);
session.save(item2);

双向n-n关联

双向n-n关联需要两端都使用集合属性。
双向n-n关联必须使用连接表。
集合属性应增加key子元素用以映射外键列,集合元素里还应该增加many-to-many子元素关联实体类。
在双向n-n关联的两边都需要指定连接表的表名和外键列的列名,两个集合元素set的table元素的值必须指定,而且必须相同。set元素的两个子元素,key和many-to-many必须指定column属性。其中key和many-to-many分别指定本持久化类和关联类在连接表中的外键列的列名。因此两边的key和many-to-many的column属性交叉相同。

对于双向n-n关联,必须把一段的inverse设为true。否则两端都维护关联关系可能会造成主键冲突。

基于双向n-n关系改写上述操作
Item类

public class Item {
    private Integer id;
    private String name;

    private Set<Category>categories = new HashSet<>();

    //Getter and setter.
}

Item.hbm.xml

<hibernate-mapping package="com.hibernate.n2n">
    <class name="Item" table="items">
        <id name="id" type="java.lang.Integer">
            <column name="ID"/>
            <generator class="native"/>
        </id>
        <property name="name" type="java.lang.String">
            <column name="NAME"/>
        </property>

        <set name="categories" table="category_item" inverse="true">
            <key column="Item_Id"></key>
            <many-to-many class="Category" column="Category_Id"></many-to-many>
        </set>
    </class>
</hibernate-mapping>

测试类

Category category1 = new Category();
category1.setName("Category 1");

Category category2 = new Category();
category2.setName("Category 2");

Item item1 = new Item();
item1.setName("item1");
Item item2 = new Item();
item2.setName("item2");

category1.getItems().add(item1);
category1.getItems().add(item2);
category2.getItems().add(item1);
category2.getItems().add(item2);

item1.getCategories().add(category1);
item1.getCategories().add(category2);
item2.getCategories().add(category1);
item2.getCategories().add(category2);

session.save(category1);
session.save(category2);
session.save(item1);
session.save(item2);
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 218,386评论 6 506
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,142评论 3 394
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,704评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,702评论 1 294
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,716评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,573评论 1 305
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,314评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,230评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,680评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,873评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,991评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,706评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,329评论 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,910评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,038评论 1 270
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,158评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,941评论 2 355

推荐阅读更多精彩内容