[置顶] 回首Hibernate之映射篇

通过上一篇文章,我们了解了hibernate基本的增删改查功能,但是针对的都是单表的操作。在实际工作中,我们往往会遇到多表联查的情况,这时候就需要将每张表所代表的对象进行关联映射,这样就可以对多表进行操作了。下面我们就来看看比较常见的映射种类。

一对多映射

首先来看这样的示例。有这样一个学生管理系统,包括学生基本信息,学生联系地址信息等。一个学生可能存在多个联系地址的情况,所以这里的学生与地址就是一对多的关系。我们建立数据表来进行说明。

#创建student表
CREATE TABLE IF NOT EXISTS `student` ( `ID` int(11) NOT NULL AUTO_INCREMENT, `NAME` varchar(255) DEFAULT NULL, `AGE` int(11) DEFAULT NULL, `SEX` int(255) DEFAULT NULL, `MAJOR` varchar(255) DEFAULT NULL, PRIMARY KEY (`ID`) );

#为student表插入数据
INSERT INTO `student` (`ID`, `NAME`, `AGE`, `SEX`, `MAJOR`) VALUES (1, 'Jack', 22, 1, 'Psychology'), (2, 'Tom', 21, 1, 'Chemistry'), (3, 'Julia', 19, 0, 'Histroy'), (4, 'Alen', 25, 1, 'Histroy'), (6, 'Adam', 25, 1, 'Economics'), (7, 'Tony', 25, 1, 'Psychology'), (8, 'Daisy', 1, 0, 'Economics'), (106, 'Bill', 22, 1, 'Sociology'), (111, 'Emily', 26, 0, 'Biology');

#创建address表
CREATE TABLE IF NOT EXISTS `address` ( `id` int(10) NOT NULL AUTO_INCREMENT, `student_id` int(10) NOT NULL, `address_info` varchar(50) CHARACTER SET utf8 NOT NULL, PRIMARY KEY (`id`) );

#为address表插入数据
INSERT INTO `address` (`id`, `student_id`, `address_info`) VALUES (1, 1, '中国-北京'), (2, 1, '中国-西安'), (3, 4, '美国-纽约'), (4, 7, '中国-石泉'), (5, 7, '中国-北京'), (6, 7, '中国-西安'), (9, 106, '英国-伦敦'), (10, 106, '美国-休斯敦'), (11, 111, 'USA-NEWYORK'), (12, 111, 'CHINA-SHANGHAI');

对于这样的一对多的情况,我们会编写如下的SQL语句进行查询,SELECT s.name,a.address_info FROM STUDENT s,ADDRESS a WHERE s.id=a.student_id

那么在hibernate中是如何操作的呢?
1.在一的一方,也就是学生这一方,在Student类中添加多的一方(即地址)的集合属性Set或者List,如下:

public class Student {

    private int id;
    private String name;
    private int age;
    private int sex;
    private String major;
    private Set<Address> address=new HashSet<Address>();

    //TODO 属性的getter和setter方法

}

2.在一的一方,也就是学生这一方,在Student.hbm.xml中添加集合属性Set或者List的映射,如下:

<set name="address" cascade="delete" inverse="true">
    <!-- 映射表的外键 -->
    <key column="student_id"/>
    <one-to-many class="com.earl.entity.Address"/>
</set>

代码解释:

  • 上面的cascade表示级联操作,可以有save-update(当对主对象执行save和update操作时,关联属性也执行save和update操作),delete(当对主对象删除时,关联属性也执行删除),all(save-update与delete的合体)。
  • inverse指的是关联的两个对象的关系,由哪一方来维护。默认情况下是由双方共同来维护。但是出于对效率的考虑,将关系维护工作还是交给多的一方处理。对于这一点,可以这样来理解,老师与一群学生的关系,老师要记住每个学生的名字,很困难。但是如果让学生来记住老师的名字,那么就简单多了。

3.在调用的地方,使用student.getAddress就可以得到关于这个学生的所有的联系地址的信息了。

例如有如下的测试方法:

public void testOneToMany(){
    Session session=ThreadSessionUtil.getSession();
    Student student=session.load(Student.class, 7);
    System.out.println(student);
    System.out.println("-----------address-----------");
    Set<Address> addresses=student.getAddress();
    for(Address address:addresses){
        System.out.println(address.getAddress_info());
    }
    ThreadSessionUtil.closeSession();
}

先查询出学生对象后,通过调用学生对象的getAddress()方法,hibernate会通过延迟加载的形式去数据库查询与该学生所关联的所有的address。hibernate会打印出如下2条SQL,查询出我们需要的信息。

Hibernate: 
    select
        student0_.id as id1_6_0_,
        student0_.name as name2_6_0_,
        student0_.age as age3_6_0_,
        student0_.sex as sex4_6_0_,
        student0_.major as major5_6_0_ 
    from
        student student0_ 
    where
        student0_.id=?
Hibernate: 
    select
        address0_.student_id as student_3_0_0_,
        address0_.id as id1_0_0_,
        address0_.id as id1_0_1_,
        address0_.address_info as address_2_0_1_,
        address0_.student_id as student_3_0_1_ 
    from
        address address0_ 
    where
        address0_.student_id=?

多对一映射

还是使用上面所说的学生与地址的关系。有多个地址信息都与一个学生对应,所以当我得到地址信息的同时想查询出相应的学生信息时,我们可以这样写SQL来查询:SELECT s.ID,s.NAME,a.address_info FROM STUDENT AS s,ADDRESS AS a WHERE a.STUDENT_ID=s.ID AND s.ID=?

那么在hibernate中是如何操作的呢?
1.在多的一方(也就是地址对象)的类中添加一的一方(也就是学生对象)的类型的属性Student,并去掉他们之间关联字段的属性student_id,如下:

public class Address {

    private int id;
    private String address_info;
    private Student student;

    //TODO 各个属性的getter和setter方法
}
  1. 在多的一方的hbm映射文件添加<many-to-one>映射,并且去掉原有关联字段的映射,如下:
<class name="com.earl.entity.Address" table="address" >
    <id name="id" column="id" type="integer">
        <generator class="identity"></generator>
    </id>
    <property name="address_info" column="address_info" type="string"/>
    <many-to-one name="student" class="com.earl.entity.Student" column="student_id"></many-to-one>
</class>

代码解释:<many-to-one>中的name为关联的属性名称,column为两表之间的关联字段。

3.在调用的地方,通过getXXX方法获取相应的对象。

例如有如下的测试方法:

public void testManyToOne(){
    Session session=ThreadSessionUtil.getSession();
    Address address=session.get(Address.class, 1);
    System.out.println("----------获取学生数据----------");
    Student student=address.getStudent();
    System.out.println(student);
    System.out.println(student.getName());
    ThreadSessionUtil.closeSession();
}

先查询出地址信息,再根据地址信息中的student_id关联查询到student的信息。hibernate会打印出如下的两条SQL。

Hibernate: 
    select
        address0_.id as id1_0_0_,
        address0_.address_info as address_2_0_0_,
        address0_.student_id as student_3_0_0_ 
    from
        address address0_ 
    where
        address0_.id=?

Hibernate: 
    select
        student0_.id as id1_6_0_,
        student0_.name as name2_6_0_,
        student0_.age as age3_6_0_,
        student0_.sex as sex4_6_0_,
        student0_.major as major5_6_0_ 
    from
        student student0_ 
    where
        student0_.id=?

继承映射

继承映射涉及到了子类父类之间的关系映射。在hibernate中有三种定义不同继承映射的方式:

  • subclass
    将子类父类对象都映射到父类的数据表中。还是从实际案例中来说明。
    对于一个考题系统来说,有问题Question这个父类,还有选择题ChoiceQuestion和简答题EssayQuestion这两个子类。我们不用将ChoiceQuestion和EssayQuestion单独拿出来建立各自的数据表,我们只需要建立一张Question表来存放所有的问题,并用一个标识来区分他们即可。所以在hibernate中我们可以有如下的配置:
    首先由ChoiceQuestion和EssayQuestion来继承Question。
public class Question {

    private int id;
    private String question_description;
    private String level;

    //TODO 各个属性的getter和setter方法
}

public class ChoiceQuestion extends Question {

    private String choice_option;
    private String choice_answer;

    //TODO 各个属性的getter和setter方法
}

public class EssayQuestion extends Question {

    private String essay_answer;

    //TODO 属性的getter和setter方法
}

然后在Question.hbm.xml中对其进行配置,如下:

<class name="com.earl.entity.Question" table="question" >
    <id name="id" column="id" type="integer">
        <generator class="identity"></generator>
    </id>
    <!--discriminator指的是标识符,我们需要在Question对应的数据表中添加一个标识符字段-->
    <!--discriminator必须定义在主键后,在其他位置将会抛错Could not parse mapping document-->
    <discriminator column="flag"></discriminator>
    <property name="question_description" column="question_description" type="string"/>
    <property name="level" column="level" type="string"/>

    <!--discriminator-value指的是在Question表中与flag相匹配的参考值,这里等于1,即在做添加操作时,会将1添加给flag。在查询时,hibernate会自动将flag=1作为查询choiceQuestion的条件-->
    <subclass name="com.earl.entity.ChoiceQuestion" discriminator-value="1">
        <property name="choice_option" column="choice_option" type="string"/>
        <property name="choice_answer" column="choice_answer" type="string"/>
    </subclass>

    <subclass name="com.earl.entity.EssayQuestion" discriminator-value="2">
        <property name="essay_answer" column="essay_answer" type="string"/>
    </subclass>

</class>

具体的测试方法请参考文末的源码。

  • joined-subclass
    子类有子类的表,父类有父类的表。在实际案例中说明吧。
    例如,在京东上,有商品Product类,还有其子类图书Book,Book继承自Product。
public class Product {

    private int product_id;
    private String product_name;
    private String description;

    //TODO 属性的getter和setter方法
}

public class Book extends Product {

    private int book_id;
    private String book_name;
    private String book_species;
    private BigDecimal book_price;
    private int product_id;

    //TODO 属性的getter和setter方法
}

在数据库中分别创建商品表和图书表。

CREATE TABLE IF NOT EXISTS `product` ( `id` int(10) NOT NULL AUTO_INCREMENT, `product_name` varchar(50) DEFAULT NULL, `description` varchar(500) DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8;

CREATE TABLE IF NOT EXISTS `book` ( `book_id` int(10) NOT NULL, `book_name` varchar(50) DEFAULT NULL, `book_species` varchar(50) DEFAULT NULL, `book_price` decimal(5,2) DEFAULT NULL, PRIMARY KEY (`book_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8;

接下来就来看看子表和父表是如何关联的吧。因为使用的是joined-subclass,不需要对子表建立关系映射文件,子父类的关系有父类来维护。所以我们来看一下product.hbm.xml的内容:

<hibernate-mapping>
    <class name="com.earl.entity.Product" table="product" >
        <id name="id" column="id" type="integer">
            <generator class="identity"></generator>
        </id>
        <property name="product_name" column="product_name" type="string"/>
        <property name="description" column="description" type="string"/>

        <joined-subclass name="com.earl.entity.Book" table="book">
            <!-- book表的主键id,book表的id不能为自增长,他需要与product表的id做等值关联。 -->
            <key column="book_id"></key>

            <property name="book_name" column="book_name" type="string"/>
            <property name="book_species" column="book_species" type="string"/>
            <property name="book_price" column="book_price" type="java.math.BigDecimal"/>
        </joined-subclass>
    </class>
</hibernate-mapping>

当建立了这样的关系后,我们就可以对子父类进行操作了。首先来看看插入操作。

@Test
public void testJoinedSubclass(){
    Session session=ThreadSessionUtil.getSession();
    Transaction transaction=session.beginTransaction();

    Book book=new Book();
    //因为book继承了product,所以book拥有product的属性,那么对相关的product属性赋值,相应的也会存储到Product表。
    book.setProduct_name("book");
    book.setDescription("reading change world");
    book.setBook_name("Harry Potter");
    book.setBook_price(new BigDecimal("68.99"));
    book.setBook_species("novel");

    System.out.println("-------save book start-------");
    session.save(book);

    transaction.commit();
    ThreadSessionUtil.closeSession();
}

当执行了该测试方法后,我们来看看hibernate为我们做了什么?观察控制台输出,hibernate打印了如下的sql:

Hibernate: 
    insert into product (product_name, description) values (?, ?) Hibernate: insert into book (book_name, book_species, book_price, book_id) values (?, ?, ?, ?)

说明hibernate先为父类插入了数据,接着又为子类数据表插入了数据。

再来看看查询。有如下测试方法:

@Test
public void testJoinedSubclassQuery(){
    Session session=ThreadSessionUtil.getSession();
    Book book=session.load(Book.class, 1);
    System.out.println(book.getId());
    System.out.println(book.getProduct_name());
    System.out.println(book.getBook_name());

    ThreadSessionUtil.closeSession();
}

当我们查询book,调用getProduct_name()和getBook_name()方法时,hibernate将book表和其父类product表做了一个连接查询的操作,所以hibernate会输出如下的sql:

Hibernate: 
    select book0_.book_id as id1_6_0_, book0_1_.product_name as product_2_6_0_, book0_1_.description as descript3_6_0_, book0_.book_name as book_nam2_1_0_, book0_.book_species as book_spe3_1_0_, book0_.book_price as book_pri4_1_0_ from book book0_ inner join product book0_1_ on book0_.book_id=book0_1_.id where book0_.book_id=?

于是乎就得到了子表和父表的相应数据。

以上就是joined-subclass的基本用法。

  • union-subclass(不常用)
    父类中的属性,添加到子类对应的表中

组件映射

hibernate的组件映射其实是基于面向对象的组件重复利用思想而设计的。将一部分属性封装为组件来使用,简化实体类定义。下面来看看组件映射的使用。
对于电商平台来说,通常会有订单Order这个类,如下:

public class Order {

    private int id;
    private String goods_name;
    private String receiver;
    private String order_amount;
    private ReceiveAddress receiveAddress;

    //TODO 各个属性的getter和setter
}

对于订单来说,收货地址的信息我们可以将其封装为ReceiveAddress这个类,如下:

public class ReceiveAddress {

    private int id;
    private String receive_address;
    private String address_postcode;

    //TODO 各个属性的getter和setter
}

当然,ReceiveAddress作为组件,那么也需要编写相应的hbm.xml文件,这里不再赘述。
接着建立Order数据表和ReceiveAddress数据表,如下:

#创建orderinfo表
CREATE TABLE IF NOT EXISTS `orderinfo` ( `id` int(10) NOT NULL AUTO_INCREMENT, `goods_name` varchar(50) DEFAULT NULL, `receiver` varchar(50) DEFAULT NULL, `receiver_id` int(10) DEFAULT NULL, `order_amount` varchar(10) DEFAULT NULL, `receive_address` varchar(50) DEFAULT NULL, `address_postcode` varchar(6) DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8;

#创建receiveaddress表
CREATE TABLE IF NOT EXISTS `receiveaddress` ( `id` int(10) NOT NULL AUTO_INCREMENT, `receive_address` varchar(50) DEFAULT NULL, `address_postcode` varchar(6) DEFAULT NULL, `receiver_id` int(10) DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8;

#为receiveaddress表初始化数据
INSERT INTO `receiveaddress` (`id`, `receive_address`, `address_postcode`, `receiver_id`) VALUES (1, 'beijing', '100008', 1), (2, 'xian', '710000', 2);

对Order.hbm.xml作如下配置:

<hibernate-mapping>
    <class name="com.earl.entity.Order" table="orderinfo" >
        <id name="id" column="id" type="integer">
            <generator class="identity"></generator>
        </id>
        <property name="goods_name" column="goods_name" type="string"/>
        <property name="receiver" column="receiver" type="string"/>
        <property name="order_amount" column="order_amount" type="string"/>

        <!--组件映射配置-->
        <component name="receiveAddress" class="com.earl.entity.ReceiveAddress">
            <property name="receive_address" column="receive_address" type="string"/>
            <property name="address_postcode" column="address_postcode" type="string"/>
        </component>
    </class>
</hibernate-mapping>

现在我们就已经将ReceiveAddress作为组件映射到Order中了。我们来测试一下吧。
看如下的insert方法:

@Test
public void testAdd(){
    Session session=ThreadSessionUtil.getSession();
    Transaction transaction=session.beginTransaction();

    Order order=new Order();
    order.setGoods_name("ipad Air2");
    order.setReceiver("Earl");
    order.setOrder_amount("4288.00");

    //查询ReceiveAddress记录,并将其赋值给Order
    ReceiveAddress receiveAddress=session.get(ReceiveAddress.class, 1);

    order.setReceiveAddress(receiveAddress);

    session.save(order);

    transaction.commit();
    ThreadSessionUtil.closeSession();
}

如上方法执行时,hibernate会先去查询ReceiveAddress 的记录,然后再进行insert操作,于是乎会打印如下的sql:

Hibernate: 
    select receiveadd0_.id as id1_8_0_, receiveadd0_.receive_address as receive_2_8_0_, receiveadd0_.address_postcode as address_3_8_0_, receiveadd0_.receiver_id as receiver4_8_0_ from receiveaddress receiveadd0_ where receiveadd0_.id=? Hibernate: insert into orderinfo (goods_name, receiver, order_amount, receiver_id, receive_address, address_postcode) values (?, ?, ?, ?, ?, ?)

关于组件映射,还可以有多个组件联合使用的方式,比较简单,这里不加赘述,详细请参照文末源码中关于Person和PersonAddress的相关代码及配置。

联合主键映射

在实际工作中,可能会对一张表的多个字段设置主键,称之为联合主键。关于联合主键的使用,其实不算太多。这里简单介绍一下联合主键映射的相关知识。还是直接从实例中来说明吧。
有雇员Emp类,他有简单属性(姓,名,性别),其中姓和名为联合主键。
hibernate中对于联合主键的操作,需要将其封装成一个类,并且需要实现Serializable接口,如下:

public class EmpKey implements java.io.Serializable {

    private String first_name;
    private String last_name;

    //TODO 各个属性的getter和setter
}

然后在Emp这个类中创建联合主键属性,如下:

public class Emp {

    private EmpKey key;
    private String sex;

    //TODO 各个属性的getter和setter
}

接下来看看hbm.xml中对联合主键是如何映射的。如下:

<hibernate-mapping>
    <class name="com.earl.entity.Emp" table="emp" >
        <!-- 联合主键映射 -->
        <composite-id name="key" class="com.earl.entity.EmpKey">
            <key-property name="first_name" type="string" column="first_name"/>
            <key-property name="last_name" type="string" column="last_name"/>
        </composite-id>
        <property name="sex" type="string" column="sex"/>
    </class>
</hibernate-mapping>

现在我们就已经对联合主键映射完成了配置。下面我们来看看基本的添加和查询操作。
添加:

@Test
public void testAdd(){
    Session session=ThreadSessionUtil.getSession();
    Transaction transaction=session.beginTransaction();

    Emp emp=new Emp();
    //设置主键
    emp.setKey(new EmpKey("Kobe", "Bryant"));
    emp.setSex("male");
    session.save(emp);

    transaction.commit();
    ThreadSessionUtil.closeSession();
}

执行testAdd方法后,hibernate为我们执行了一条insert的sql语句。

查询:

@Test
public void testQuery(){
    Session session=ThreadSessionUtil.getSession();

    //这里的EmpKey必须实现Serializable接口,才可以使用get或者load方法进行查询。
    Emp emp=session.get(Emp.class, new EmpKey("Kobe", "Bryant"));
    System.out.println(emp);

    ThreadSessionUtil.closeSession();
}

执行查询时,hibernate会将EmpKey的值作为查询条件进行查询。

总结

以上就是hibernate中比较常用到的关系映射,在实际使用中,还是需要多分析类与类之间的关系,这样才能更好的使用hibernate为我们提供的关系映射,从而提高开发效率。

点此下载源代码
说明:本文总结的是hibernate中关系映射的相应内容,请读者参照源代码时,只参照com.earl.test包下以Mapping结尾的测试类,其他的可自动忽略。

你可能感兴趣的:(Hibernate,关系映射)