1.2. 第二部分 - 关联映射

优质
小牛编辑
129浏览
2023-12-01

我 们已经映射了一个持久化实体类到表上。让我们在这个基础上增加一些类之间的关联。首先我们往应用程序里增加人(people)的概念,并存储他们所参与的 一个 Event 列表。(译者注:与 Event 一样,我们在后面将直接使用 person 来表示“人”而不是它的中文翻译)

1.2.1. 映射 Person 类

最初简单的 Person 类:

package org.hibernate.tutorial.domain;

public class Person {

    private Long id;
    private int age;
    private String firstname;
    private String lastname;

    public Person() {}

    // Accessor methods for all properties, private setter for 'id'

}

把它保存为文件 src/main/java/org/hibernate/tutorial/domain/Person.java

然后,创建新的映射文件 src/main/resources/org/hibernate/tutorial/domain/Person.hbm.xml

<hibernate-mapping package="org.hibernate.tutorial.domain">

    <class name="Person" table="PERSON">
        <id name="id" column="PERSON_ID">
            <generator class="native"/>
        </id>
        <property name="age"/>
        <property name="firstname"/>
        <property name="lastname"/>
    </class>

</hibernate-mapping
>

最后,把新的映射加入到 Hibernate 的配置中:

<mapping resource="events/Event.hbm.xml"/>
<mapping resource="events/Person.hbm.xml"/>

现在我们在这两个实体之间创建一个关 联。显然,persons 可以参与一系列 events,而 events 也有不同的参加者(persons)。我们需要处理的设计问题是关联方向(directionality),阶数(multiplicity)和集合 (collection)的行为。

1.2.2. 单向 Set-based 的关联

我们将向 Person 类增加一连串的 events。那样,通过调用 aPerson.getEvents(),就可以轻松地导航到特定 person 所参与的 events,而不用去执行一个显式的查询。我们使用 Java 的集合类(collection):Set,因为 set 不包含重复的元素及与我们无关的排序。

public class Person {

    private Set events = new HashSet();

    public Set getEvents() {
        return events;
    }

    public void setEvents(Set events) {
        this.events = events;
    }
}

在映射这个关联之前,先考虑一下此关联的另外一端。很显然,我们可以保持这个关联是单向的。或者,我们可以在 Event 里创建另外一个集合,如果希望能够双向地导航,如:anEvent.getParticipants()。从功能的角度来说,这并不是必须的。因为你总可以显式地执行一个查询,以获得某个特定 event 的所有参与者。这是个在设计时需要做出的选择,完全由你来决定,但此讨论中关于关联的阶数是清楚的:即两端都是“多”值的,我们把它叫做多对多(many-to-many)关联。因而,我们使用 Hibernate 的多对多映射:

<class name="Person" table="PERSON">
    <id name="id" column="PERSON_ID">
        <generator class="native"/>
    </id>
    <property name="age"/>
    <property name="firstname"/>
    <property name="lastname"/>

    <set name="events" table="PERSON_EVENT">
        <key column="PERSON_ID"/>
        <many-to-many column="EVENT_ID" class="Event"/>
    </set>

</class
>

Hibernate 支持各种各样的集合映射,<set> 使用的最为普遍。对于多对多关联(或叫 n:m 实体关系), 需要一个关联表(association table)。里面的每一行代表从 person 到 event 的一个关联。表名是由 set 元素的 table 属性配置的。关联里面的标识符字段名,对于 person 的一端,是由 <key> 元素定义,而 event 一端的字段名是由 <many-to-many> 元素的 column 属性定义。你也必须告诉 Hibernate 集合中对象的类(也就是位于这个集合所代表的关联另外一端的类)。

因而这个映射的数据库 schema 是:

    _____________        __________________
   |             |      |                  |       _____________
   |   EVENTS    |      |   PERSON_EVENT   |      |             |
   |_____________|      |__________________|      |    PERSON   |
   |             |      |                  |      |_____________|
   | *EVENT_ID   | <--> | *EVENT_ID        |      |             |
   |  EVENT_DATE |      | *PERSON_ID       | <--> | *PERSON_ID  |
   |  TITLE      |      |__________________|      |  AGE        |
   |_____________|                                |  FIRSTNAME  |                                  |  LASTNAME   |                                  |_____________|
 

1.2.3. 使关联工作

我们把一些 people 和 events 一起放到 EventManager 的新方法中:

    private void addPersonToEvent(Long personId, Long eventId) {
        Session session = HibernateUtil.getSessionFactory().getCurrentSession();
        session.beginTransaction();

        Person aPerson = (Person) session.load(Person.class, personId);
        Event anEvent = (Event) session.load(Event.class, eventId);
        aPerson.getEvents().add(anEvent);

        session.getTransaction().commit();
    }

在加载一 PersonEvent 后,使用普通的集合方法就可容易地修改我们定义的集合。如你所见,没有显式的 update()save(),Hibernate 会自动检测到集合已经被修改并需要更新回数据库。这叫做自动脏检查(automatic dirty checking),你也可以尝试修改任何对象的 name 或者 date 属性,只要他们处于持久化状态,也就是被绑定到某个 Hibernate 的 Session 上(如:他们刚刚在一个单元操作被加载或者保存),Hibernate 监视任何改变并在后台隐式写的方式执行 SQL。同步内存状态和数据库的过程,通常只在单元操作结束的时候发生,称此过程为清理缓存(flushing)。在我们的代码中,工作单元由数据库事务的提交(或者回滚)来结束——这是由 CurrentSessionContext 类的 thread 配置选项定义的。

当然,你也可以在不同的单元操作里面加载 person 和 event。或在 Session 以外修改不是处在持久化(persistent)状态下的对象(如果该对象以前曾经被持久化,那么我们称这个状态为脱管(detached))。你甚至可以在一个集合被脱管时修改它:

    private void addPersonToEvent(Long personId, Long eventId) {
        Session session = HibernateUtil.getSessionFactory().getCurrentSession();
        session.beginTransaction();

        Person aPerson = (Person) session
                .createQuery("select p from Person p left join fetch p.events where p.id = :pid")
                .setParameter("pid", personId)
                .uniqueResult(); // Eager fetch the collection so we can use it detached
        Event anEvent = (Event) session.load(Event.class, eventId);

        session.getTransaction().commit();

        // End of first unit of work

        aPerson.getEvents().add(anEvent); // aPerson (and its collection) is detached

        // Begin second unit of work

        Session session2 = HibernateUtil.getSessionFactory().getCurrentSession();
        session2.beginTransaction();
        session2.update(aPerson); // Reattachment of aPerson

        session2.getTransaction().commit();
    }

update 的调用使一个脱管对象重新持久化,你可以说它被绑定到一个新的单元操作上,所以在脱管状态下对它所做的任何修改都会被保存到数据库里。这也包括你对这个实体对象的集合所作的任何改动(增加/删除)。

这对我们当前的情形不是很有用,但它是非常重要的概念,你可以把它融入到你自己的应用程序设计中。在EventManager的 main 方法中添加一个新的动作,并从命令行运行它来完成我们所做的练习。如果你需要 person 及 event 的标识符 — 那就用 save() 方法返回它(你可能需要修改前面的一些方法来返回那个标识符):

        else if (args[0].equals("addpersontoevent")) {
            Long eventId = mgr.createAndStoreEvent("My Event", new Date());
            Long personId = mgr.createAndStorePerson("Foo", "Bar");
            mgr.addPersonToEvent(personId, eventId);
            System.out.println("Added person " + personId + " to event " + eventId);
        }

上面是个关于两个同等重要的实体类间关联的例子。像前面所提到的那样,在特定的模型中也存在其它的类和类型,这些类和类型通常是“次要的”。你已看到过其中的一些,像 intString。我们称这些类为值类型(value type),它们的实例依赖(depend)在某个特定的实体上。这些类型的实例没有它们自己的标识(identity),也不能在实体间被共享(比如,两个 person 不能引用同一个 firstname 对象,即使他们有相同的 first name)。当然,值类型并不仅仅在 JDK 中存在(事实上,在一个 Hibernate 应用程序中,所有的 JDK 类都被视为值类型),而且你也可以编写你自己的依赖类,例如 AddressMonetaryAmount

你也可以设计一个值类型的集合,这在概念上与引用其它实体的集合有很大的不同,但是在 Java 里面看起来几乎是一样的。

1.2.4. 值类型的集合

让我们在 Person 实体里添加一个电子邮件的集合。这将以 java.lang.String 实例的 java.util.Set 出现:

    private Set emailAddresses = new HashSet();

    public Set getEmailAddresses() {
        return emailAddresses;
    }

    public void setEmailAddresses(Set emailAddresses) {
        this.emailAddresses = emailAddresses;
    }

这个 Set 的映射如下:

        <set name="emailAddresses" table="PERSON_EMAIL_ADDR">
            <key column="PERSON_ID"/>
            <element type="string" column="EMAIL_ADDR"/>
        </set
>

比较这次和此前映射的差别,主要在于 element 部分,这次并没有包含对其它实体引用的集合,而是元素类型为 String 的集合(在映射中使用小写的名字”string“是向你表明它是一个 Hibernate 的映射类型或者类型转换器)。和之前一样,set 元素的 table 属性决定了用于集合的表名。key 元素定义了在集合表中外键的字段名。element 元素的 column 属性定义用于实际保存 String 值的字段名。

看一下修改后的数据库 schema。

  _____________        __________________
 |             |      |                  |       _____________
 |   EVENTS    |      |   PERSON_EVENT   |      |             |       ___________________
 |_____________|      |__________________|      |    PERSON   |      |                   |
 |             |      |                  |      |_____________|      | PERSON_EMAIL_ADDR |
 | *EVENT_ID   | <--> | *EVENT_ID        |      |             |      |___________________|
 |  EVENT_DATE |      | *PERSON_ID       | <--> | *PERSON_ID  | <--> |  *PERSON_ID       |
 |  TITLE      |      |__________________|      |  AGE        |      |  *EMAIL_ADDR      |
 |_____________|                                |  FIRSTNAME  |      |___________________|                                |  LASTNAME   |                                |_____________|
 

你可以看到集合表的主键实际上是个复合主键,同时使用了两个字段。这也暗示了对于同一个 person 不能有重复的 email 地址,这正是 Java 里面使用 Set 时候所需要的语义(Set 里元素不能重复)。

你现在可以试着把元素加入到这个集合,就像我们在之前关联 person 和 event 的那样。其实现的 Java 代码是相同的:

    private void addEmailToPerson(Long personId, String emailAddress) {
        Session session = HibernateUtil.getSessionFactory().getCurrentSession();
        session.beginTransaction();

        Person aPerson = (Person) session.load(Person.class, personId);
        // adding to the emailAddress collection might trigger a lazy load of the collection
        aPerson.getEmailAddresses().add(emailAddress);

        session.getTransaction().commit();
    }

这次我们没有使用 fetch 查询来初始化集合。因此,调用其 getter 方法会触发另一附加的 select 来初始化集合,这样我们才能把元素添加进去。检查 SQL log,试着通过预先抓取来优化它。

1.2.5. 双向关联

接 下来我们将映射双向关联(bi-directional association)— 在 Java 里让 person 和 event 可以从关联的任何一端访问另一端。当然,数据库 schema 没有改变,我们仍然需要多对多的阶数。一个关系型数据库要比网络编程语言更加灵活,所以它并不需要任何像导航方向(navigation direction)的东西 — 数据可以用任何可能的方式进行查看和获取。

注意

关系型数据库比网络编程语言更为灵活,因为它不需要方向导航,其数据可以用任何可能的方式进行查看和提取。

首先,把一个参与者(person)的集合加入 Event 类中:

    private Set participants = new HashSet();

    public Set getParticipants() {
        return participants;
    }

    public void setParticipants(Set participants) {
        this.participants = participants;
    }

Event.hbm.xml 里面也映射这个关联。

        <set name="participants" table="PERSON_EVENT" inverse="true">
            <key column="EVENT_ID"/>
            <many-to-many column="PERSON_ID" class="events.Person"/>
        </set
>

如你所见,两个映射文件里都有普通的 set 映射。注意在两个映射文件中,互换了 keymany-to-many 的字段名。这里最重要的是 Event 映射文件里增加了 set 元素的 inverse="true" 属性。

这意味着在需要的时候,Hibernate 能在关联的另一端 — Person 类得到两个实体间关联的信息。这将会极大地帮助你理解双向关联是如何在两个实体间被创建的。

1.2.6. 使双向连起来

首先请记住,Hibernate 并不影响通常的 Java 语义。 在单向关联的例子中,我们是怎样在 PersonEvent 之间创建联系的?我们把 Event 实例添加到 Person 实例内的 event 引用集合里。因此很显然,如果我们要让这个关联可以双向地工作,我们需要在另外一端做同样的事情 - 把 Person 实例加入 Event 类内的 Person 引用集合。这“在关联的两端设置联系”是完全必要的而且你都得这么做。

许多开发人员防御式地编程,创建管理关联的方法来保证正确的设置了关联的两端,比如在 Person 里:

    protected Set getEvents() {
        return events;
    }

    protected void setEvents(Set events) {
        this.events = events;
    }

    public void addToEvent(Event event) {
        this.getEvents().add(event);
        event.getParticipants().add(this);
    }

    public void removeFromEvent(Event event) {
        this.getEvents().remove(event);
        event.getParticipants().remove(this);
    }

注 意现在对于集合的 get 和 set 方法的访问级别是 protected — 这允许在位于同一个包(package)中的类以及继承自这个类的子类可以访问这些方法,但禁止其他任何人的直接访问,避免了集合内容的混乱。你应尽可能 地在另一端也把集合的访问级别设成 protected。

inverse 映射属性究竟表示什么呢?对于你和 Java 来说,一个双向关联仅仅是在两端简单地正确设置引用。然而,Hibernate 并没有足够的信息去正确地执行 INSERTUPDATE 语句(以避免违反数据库约束),所以它需要一些帮助来正确的处理双向关联。把关联的一端设置为 inverse 将告诉 Hibernate 忽略关联的这一端,把这端看成是另外一端的一个镜象(mirror)。这就是所需的全部信息,Hibernate 利用这些信息来处理把一个有向导航模型转移到数据库 schema 时的所有问题。你只需要记住这个直观的规则:所有的双向关联需要有一端被设置为 inverse。在一对多关联中它必须是代表多(many)的那端。而在多对多(many-to-many)关联中,你可以任意选取一端,因为两端之间并没有差别。