Hibernate6-入门手册-二-

49 阅读1小时+

Hibernate6 入门手册(二)

原文:Beginning Hibernate 6

协议:CC BY-NC-SA 4.0

四、持久化生命周期

在本章中,我们将讨论 Hibernate 中持久对象的生命周期。这些持久对象可以是 POJOs,不需要任何特殊的标记接口或与 Hibernate 相关的继承。Hibernate 受欢迎的部分原因是它能够使用普通的对象模型。

我们还将介绍一些用于从 Hibernate 创建、检索、更新和删除持久对象的Session接口的方法。

生命周期介绍

将 Hibernate 添加到应用之后,您不需要更改现有的 Java 对象模型来添加持久标记接口或任何其他类型的 Hibernate 提示。相反,Hibernate 处理应用用new操作符创建的普通 Java 对象或其他对象创建的普通 Java 对象。

出于 Hibernate 的目的,这些可以分为两类:Hibernate 有实体映射的对象和 Hibernate 不能直接识别的对象。正确映射的实体对象将由被映射的字段和属性组成,这些字段和属性本身或者是对正确映射的实体的引用,或者是对这些实体的集合的引用,或者是“值”类型(原语、原语包装、字符串或它们的数组)。

给定一个映射到 Hibernate 的对象实例,它可以处于四种不同状态中的任何一种:暂时、持久、分离或删除。 1

短暂的对象只存在于内存中。Hibernate 不管理瞬态对象,也不保存对瞬态对象的更改。如果你有一个Person POJO,并且你用new Person()创建了一个实例,那么这个对象就是瞬态,并且只要它处于瞬态状态,就不期望它以某种方式在数据库中被表示出来。

要持久化对瞬态对象的更改,您必须请求会话将瞬态对象保存到数据库,这时 Hibernate 会给对象分配一个标识符,并将对象标记为处于持久状态。

持久对象存在于数据库中,Hibernate 管理持久对象的持久化。我们在图 4-1 中展示了对象和数据库之间的关系。如果持久化对象的字段或属性发生了变化,Hibernate 会在应用将这些变化标记为已提交时保持数据库表示的最新状态。

img/321250_5_En_4_Fig1_HTML.png

图 4-1

持久对象 由休眠维护

分离的对象在数据库中有表示,但对对象的更改不会反映在数据库中,反之亦然。对象和数据库的暂时分离如图 4-2 所示。分离的对象可以通过关闭与它相关联的会话来创建,或者通过调用会话的evict()方法将其从会话中逐出来创建。

考虑分离实体的一个原因是从数据库中读取一个对象,在内存中修改该对象的属性,然后将结果存储在数据库之外的某个地方。这将是对对象进行深层复制的一种替代方法。

img/321250_5_En_4_Fig2_HTML.png

图 4-2

分离的对象存在于数据库中,不受 Hibernate 维护

为了持久保存对分离对象所做的更改,应用必须将其重新附加到有效的 Hibernate 会话。当您的应用调用新会话上的load()refresh()merge()update()save()方法之一并引用分离的对象时,分离的实例可以与新的 Hibernate 会话相关联。调用后,分离的对象将是由新的 Hibernate 会话管理的持久对象。

移除的对象是由 Hibernate 管理的对象(换句话说,是持久对象),它们已经被传递给会话的remove()方法。当应用将会话中保存的更改标记为已提交时,数据库中对应于已删除对象的条目将被删除。

Hibernate 3 之前的版本支持生命周期和可验证的接口。这些允许您的对象使用对象上的方法监听保存、更新、删除、加载和验证事件。在 Hibernate 3 中,这个函数移到了事件和拦截器中,旧的接口被移除了。从 Hibernate 4 开始,还支持 JPA 持久化生命周期,因此可以将事件嵌入到对象中,并用注释进行标记。

实体、类和名称

实体用映射表示 Java 对象,映射允许它们存储在数据库中。映射指示对象的字段和属性应该如何存储在数据库表中。但是,您可能希望特定类型的对象在数据库中以两种不同的方式表示。例如,您可以为用户创建一个 Java 类,但是在数据库中有两个不同的表来存储用户。这可能不是最好的数据库设计,但类似的问题在遗留系统中很常见。其他不容易修改的系统可能依赖于现有的数据库设计,Hibernate 足够强大,可以覆盖这种场景。在这种情况下,Hibernate 如何选择使用哪个?

代表实体的对象将是一个普通的 Java 类。它还将有一个实体名称。默认情况下,实体的名称将与类类型的名称相同。 2 但是,您可以选择通过映射或注释来改变这一点,从而区分映射到不同表的相同类型的对象。因此,Session API 中有一些方法需要提供一个实体名称来确定适当的映射。如果省略了这一点,要么是因为不需要这种区分,要么是因为为了方便起见,该方法假设了最常见的情况——实体名称与类名相同——并复制了另一个更具体的方法的功能,该方法允许显式指定实体名称。

标识符

标识符或标识列映射到关系数据库中主键的概念。主键是一个或多个列的唯一集合,可用于指定特定的数据集合。

有两种标识符:自然的和人工的。

一个自然标识符是应用认为有意义的东西——例如,一个用户 ID,或者一个社会安全号码 3 或者等同物。

人工标识符的值是任意的。到目前为止,我们的代码使用数据库生成的值(标识列),这些值与该标识符关联的数据没有任何关系。这倾向于在关联和其他这样的交互方面产生更大的灵活性,因为在许多情况下,人工标识符可以比自然标识符小。

为什么人工标识符会比自然标识符更好?嗯,有几个可能的原因。人工标识符可能比自然标识符更好的一个原因是,人工标识符可能是比自然标识符更小的类型(在内存中)。

考虑一封用户电子邮件。在大多数情况下,用户的电子邮件地址不会改变,对于给定的用户来说,它们往往是唯一的;然而,电子邮件地址可能至少有 20 个字节长 4 (也可能更长)。整数用户 ID(long 或 int)的长度可能是 4 或 8 个字节,不会更长。

另一个更有说服力的原因是,人工标识符不会随着数据的自然生命周期而改变。例如,电子邮件地址可能会随着时间而改变;有人可能会放弃旧的电子邮件地址,而选择一个新的。任何依赖该电子邮件地址作为自然标识符的东西都必须被同步改变以允许更新。

还有一个原因是人工标识符很简单。数据库(和 Hibernate)允许使用复合标识符——由一个对象的多个属性构建的标识符。然而,这意味着当您引用数据库中的一个特定对象或行时,您必须将所有列包含在标识符中,无论是作为嵌入对象还是作为一组单独的列。这当然是可行的;一些数据模型需要它(例如,由于遗留或其他业务原因)。然而,出于效率的考虑,大多数人通常更喜欢人工密钥。

在 Hibernate 中,对象属性用@Id 注释标记为标识符,如清单 4-1 所示。

@Id
public Long id;

Listing 4-1A Typical Identifier Field

在清单 4-1 中,您会看到一个Long——H2 的“大整数”——它被标记为一个大概的人工标识符。需要先分配该值,然后才能持久保存具有该属性的对象。

不过,在我们目前的示例代码中,我们没有手动分配标识符。我们使用了另一个注释@GeneratedValue,它告诉 Hibernate 它负责分配和维护标识符。发生这种情况的机制很大程度上取决于 Hibernate 配置和使用的数据库。

你可能已经错过了,但是@Id并不意味着一个标识符被自动分配。如果您不想自己分配标识符值,您必须使用一个@GeneratedValue注释!

有五种不同的生成可能性:标识、序列、表、自动和无。身份生成依赖于自然的表排序。这是通过使用GenerationType.IDENTITY选项在@GeneratedValue注释中请求的,如清单 4-2 所示。

@Id
@GeneratedValue(strategy=GenerationType.IDENTITY)
public Long id;

Listing 4-2An Autogenerated Identity Field

身份生成非常方便,使用起来感觉非常自然。但是,在知道标识符之前,它需要将数据实际插入数据库,并且使用IDENTITY禁用插入的 JDBC 批处理(同样,因为在插入行的之后检测到标识符)。Hibernate 文档建议使用其他的生成策略,这一点毋庸置疑。

序列机制依赖于数据库创建表序列的能力(这倾向于将其限制在 PostgreSQL、Oracle 和其他一些数据库)。对应的是GenerationType.SEQUENCE策略。

机制使用一个表,其目的是存储人工标识符块;您可以让 Hibernate 为您生成,或者您可以用一个附加的@TableGenerator 注释来指定表的所有细节。要通过表使用人工密钥生成,请使用GenerationType.TABLE策略。

第四种人工密钥生成策略是自动,它通常映射到IDENTITY策略,但是依赖于所讨论的数据库。(它应该默认为对所讨论的数据库有效的东西。)要使用这个,使用GenerationType.AUTO策略。

第五种策略实际上根本不是策略:它依赖于手动分配标识符。如果用一个空的标识符调用Session.persist(),就会抛出一个IdentifierGenerationException

实体和协会

实体可以包含对其他实体的引用,可以直接作为嵌入的属性或字段,也可以通过某种集合(数组、集合、列表等)间接引用。).这些关联使用基础表中的外键关系来表示。这些外键将依赖于参与表所使用的标识符,这是更喜欢小(和人工)键的另一个原因。

当实体对中只有一个实体包含对另一个实体的引用时,关联是单向的。如果关联是相互的,那么它被称为是双向的

设计实体模型时的一个常见错误是试图使所有关联都是双向的。不属于对象模型自然组成部分的关联不应该被强制加入其中。Hibernate 查询语言通常提供了一种更自然的方式来访问相同的信息。

在关联中,其中一个(且只有一个)参与类被称为“管理关系”如果关联的两端都管理关系,那么当客户端代码在关联的两端都调用适当的 set 方法时,我们就会遇到问题。应该维护两个外键列——每个方向一个(存在循环依赖的风险)——还是只维护一个?

理想情况下,我们希望规定只有对关系一端的更改才会导致对外键的任何更新;事实上,Hibernate 允许我们通过将关联的一端标记为由另一端管理(由关联注释的mappedBy属性标记)来做到这一点。

mappedBy纯粹是关于如何保存实体之间的外键关系。这与拯救实体本身无关。尽管如此,它们经常与完全正交的级联功能相混淆(在本章的“级联操作”一节中描述)。

虽然 Hibernate 允许我们指定对一个关联的更改将导致对数据库的更改,但是它不允许我们将对关联一端的更改自动反映到 Java POJOs 的另一端。

让我们来看看一些代码。这是第四章 ?? 的pom.xml,来自这本书的源代码;这不是特别有启发性,因为它很大程度上是其他章节的项目模型的直接复制,但它是很好的彻底。

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xmlns:="http://maven.apache.org/POM/4.0.0"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
         http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>hibernate-6-parent</artifactId>
        <groupId>com.autumncode.books.hibernate</groupId>
        <version>5.0</version>
    </parent>

    <modelVersion>4.0.0</modelVersion>
    <artifactId>chapter04</artifactId>

    <dependencies>
        <dependency>
            <groupId>com.autumncode.books.hibernate</groupId>
            <artifactId>util</artifactId>
            <version>${project.version}</version>
        </dependency>
    </dependencies>
</project>

Listing 4-3pom.xml

让我们在chapter04.broken包中创建一个例子,一个Message和一个Email关联,没有“拥有对象”首先是Message类,如清单 4-4 所示。

package chapter04.broken;

import javax.persistence.*;

@Entity
public class Message {
  @Id
  @GeneratedValue(strategy = GenerationType.AUTO)
  Long id;

  @Column
  String content;

  @OneToOne
  Email email;

  public Message() {
  }

  public Message(String content) {
    setContent(content);
  }

  // accessors and mutators ignored for brevity

  @Override
  public String toString() {
    // note use of email.subject because otherwise properly constructed
    // relationships would cause an endless loop that never ends
    // and therefore runs endlessly.
    return String.format(
        "Message{id=%d, content='%s', email.subject='%s'}",
        id,
        content,
        (email != null ? email.getSubject() : "null")
    );
  }
}

Listing 4-4A Broken Model, Beginning with Message

清单 4-5 是Email级。

package chapter04.broken;

import javax.persistence.*;

@Entity
public class Email {
  @Id
  @GeneratedValue(strategy = GenerationType.AUTO)
  Long id;
  @Column
  String subject;
  @OneToOne
  // (mappedBy = "email")
  Message message;

  public Email() {
  }

  public Email(String subject) {
    setSubject(subject);
  }

  // accessors and mutators ignored for brevity

  @Override
  public String toString() {
    // note use of message.content because otherwise properly constructed
    // relationships would cause an endless loop that never ends
    // and therefore runs endlessly.
    return String.format(
        "Email{id=%s, subject=`%s`, message.content=%s}",
        id,
        subject,
        (message != null ? message.getContent() : "null")
    );
  }
}

Listing 4-5A Broken Model’s Email Class

对于这些类,没有“拥有关系”;电子邮件中的mappedBy属性被注释掉。这意味着我们需要更新电子邮件和信息,以使我们的关系在两个方向上正确建模。先看完整的chapter04.broken.BrokenInversionTest类,看到源码后再分解。

package chapter04.broken;

import com.autumncode.hibernate.util.SessionUtil;
import org.hibernate.Session;
import org.hibernate.Transaction;
import org.testng.annotations.Test;

import static org.testng.Assert.*;

public class BrokenInversionTest {
  @Test()
  public void testBrokenInversionCode() {
    Long emailId;
    Long messageId;
    Email email;
    Message message;

    try (Session session = SessionUtil.getSession()) {
      Transaction tx = session.beginTransaction();

      email = new Email("Broken");
      message = new Message("Broken");

      email.setMessage(message);
      // message.setEmail(email);

      session.save(email);
      session.save(message);

      emailId = email.getId();
      messageId = message.getId();

      tx.commit();
    }

    assertNotNull(email.getMessage());
    assertNull(message.getEmail());

    try (Session session = SessionUtil.getSession()) {
      email = session.get(Email.class, emailId);
      System.out.println(email);
      message = session.get(Message.class, messageId);
      System.out.println(message);
    }

    assertNotNull(email.getMessage());
    assertNull(message.getEmail());
  }
}

Listing 4-6src/main/java/chapter04/broken/BrokenInversionTest.java

message.getEmail()的最后一次调用将返回null(假设使用了简单的访问器和赋值器)。为了获得想要的效果,两个实体都必须更新。如果Email实体拥有关联,这仅仅确保外键列值的正确赋值。没有message.setEmail(email)没有的隐式调用。这必须显式给出,如清单 4-7 所示。

package chapter04.broken;

import com.autumncode.hibernate.util.SessionUtil;
import org.hibernate.Session;
import org.hibernate.Transaction;
import org.testng.annotations.Test;

import static org.testng.Assert.assertNotNull;

public class ProperSimpleInversionTest {
  @Test
  public void testProperSimpleInversionCode() {
    Long emailId;
    Long messageId;
    Email email;
    Message message;

    try (Session session = SessionUtil.getSession()) {
      Transaction tx = session.beginTransaction();

      email = new Email("Proper");
      message = new Message("Proper");

      email.setMessage(message);
      message.setEmail(email);

      session.save(email);
      session.save(message);

      emailId = email.getId();
      messageId = message.getId();

      tx.commit();
    }

    assertNotNull(email.getMessage());
    assertNotNull(message.getEmail());

    try (Session session = SessionUtil.getSession()) {
      email = session.get(Email.class, emailId);
      System.out.println(email);
      message = session.get(Message.class, messageId);
      System.out.println(message);
    }

    assertNotNull(email.getMessage());
    assertNotNull(message.getEmail());
  }
}

Listing 4-7src/main/java/chapter04/broken/ProperSimpleInversionTest.java

最后一个断言是assertNotNull(),在BrokenInversionTest中是assertNull()

对于刚接触 Hibernate 的用户来说,对这一点感到困惑是很常见的。发生这种情况的原因是 Hibernate 正在使用实体的实际当前状态。在BrokenInversionTest.java中,当您在电子邮件中设置消息,而不是在消息中设置电子邮件时,Hibernate 在对象模型中保存实际的关系,而不是试图推断一个关系,即使该关系是预期的。额外的关系将是一个意想不到的副作用,即使它在这种特殊情况下可能是有用的。

如果我们包括映射(mappedBy 属性),我们会得到不同的结果。我们将修改Message(通过将它移动到一个新的包中,chapter04.mapped)和Email(通过移动它并包含前面清单中注释掉的mappedBy属性)。

除了包和实体名(这意味着 Hibernate 将使用“Message2”作为该类型的表名),代码与“破损”版本相同,如清单 4-8 所示。

package chapter04.mapped;

import javax.persistence.*;

@Entity(name = "Message2")
public class Message {
  @Id
  @GeneratedValue(strategy = GenerationType.AUTO)
  Long id;

  @Column
  String content;

  @OneToOne
  Email email;

  public Message() {
  }

  public Message(String content) {
    setContent(content);
  }

  // accessors and mutators omitted

  @Override
  public String toString() {
    // note use of email.subject because otherwise properly constructed
    // relationships would cause an endless loop that never ends
    // and therefore runs endlessly.
    return String.format(
        "Message{id=%d, content='%s', email.subject='%s'}",
        id,
        content,
        (email != null ? email.getSubject() : "null")
    );
  }
}

Listing 4-8src/main/java/chapter04/mapped/Message.java

Email代码除了更改实体名称和包之外,还添加了mappedBy属性。这实际上给Message数据库表示添加了一列,表示电子邮件 ID。参见清单 4-9 。

package chapter04.mapped;

import javax.persistence.*;

@Entity(name = "Email2")
public class Email {
  @Id
  @GeneratedValue(strategy = GenerationType.AUTO)
  Long id;
  @Column
  String subject;
  @OneToOne(mappedBy = "email")
  Message message;

  public Email() {
  }

  public Email(String subject) {
    setSubject(subject);
  }

  // accessors and mutators omitted

  @Override
  public String toString() {
    // note use of message.content because otherwise properly constructed
    // relationships would cause an endless loop that never ends
    // and therefore runs endlessly.
    return String.format(
        "Email{id=%s, subject=`%s`, message.content=%s}",
        id,
        subject,
        (message != null ? message.getContent() : "null")
    );
  }
}

Listing 4-9src/main/java/chapter04/mapped/Email.java

使用Message中包含的映射,会有一些意想不到的结果。我们之前的测试无法重新建立一些关系,需要在EmailMessage中设置它们。这里,我们有几乎相同的构造,但是没有相同的结果:我们只需要设置关系的一边,而不是手动维护两个引用。

首先我们来看测试代码,如清单 4-10 所示;注意,这个测试使用了chapter04.mapped包,所以它得到了我们刚刚看到的EmailMessage类。

package chapter04.mapped;

import com.autumncode.hibernate.util.SessionUtil;
import org.hibernate.Session;
import org.hibernate.Transaction;
import org.testng.annotations.Test;

import static org.testng.Assert.*;
import static org.testng.Assert.assertNotNull;

public class ImplicitRelationshipTest {
  @Test
  public void testImpliedRelationship() {
    Long emailId;
    Long messageId;
    Email email;
    Message message;

    try (Session session = SessionUtil.getSession()) {
      Transaction tx = session.beginTransaction();

      email = new Email("Inverse Email");
      message = new Message("Inverse Message");

      // email.setMessage(message);
      message.setEmail(email);

      session.save(email);
      session.save(message);

      emailId = email.getId();
      messageId = message.getId();

      tx.commit();
    }

    assertEquals(email.getSubject(), "Inverse Email");
    assertEquals(message.getContent(), "Inverse Message");
    assertNull(email.getMessage());
    assertNotNull(message.getEmail());

    try (Session session = SessionUtil.getSession()) {
      email = session.get(Email.class, emailId);
      System.out.println(email);
      message = session.get(Message.class, messageId);
      System.out.println(message);
    }

    assertNotNull(email.getMessage());
    assertNotNull(message.getEmail());
  }
}

Listing 4-10src/test/java/chapter04/mapped/ImplicitRelationshipTest.java

这个测试通过了,尽管我们没有设置Email's Message

那个mappingBy属性就是原因。在数据库中,Message2表将有一个名为“email_id”的列,当我们更新Message's email属性时,它被设置为Email's的唯一标识符。当我们关闭会话并重新加载时,仅通过该列设置关系,这意味着关系设置“正确”,即使我们在第一次创建数据时没有正确创建关系。

如果我们要管理Email实体中的关系(即,在Message.java中设置mappedBy属性,而不是Email.java,情况将会相反:设置Message's email属性不会反映在数据库中,但是设置Email's message属性会。

以下是对这些观点的总结:

  1. 您必须显式管理关联的两端。

  2. 只有对关联所有者的更改才会在数据库中生效。

  3. 当您从数据库加载一个分离的实体时,它将反映数据库中持久化的外键关系。

表 4-1 显示了如何选择应该成为双向关联所有者的关系方。请记住,要使关联成为所有者,您必须将另一端标记为由另一端映射。

表 4-1

标记关联的所有者

| 一对一 | 任何一端都可以成为所有者,但其中一个(且只有一个)应该成为所有者;如果不指定这一点,将会导致循环依赖。 | | 一对多 | “一”端必须成为关联的所有者。 | | 多对一 | 从相反的角度来看,这与一对多关系相同,因此同样的规则也适用:多端必须成为关联的所有者。 | | 多对多 | 关联的任何一端都可以成为所有者。 |

如果这一切看起来相当混乱,请记住关联所有权只与数据库中的外键管理有关,随着您进一步使用 Hibernate,事情会变得更加清楚。关联和映射将在接下来的几章中详细讨论。

保存实体

创建用 Hibernate 映射映射的类的实例不会自动将对象保存到数据库中。在您显式地将对象与有效的 Hibernate 会话相关联之前,该对象是暂时的*,就像任何其他 Java 对象一样。在 Hibernate 中,我们使用save()persist()中的一个,它是Session接口上的save()方法的同义词,来在数据库中存储一个瞬态对象,如下所示:*

public Serializable save(Object object)
public Serializable save(String entityName, Object object)

两个save()方法都将一个瞬态对象引用(不能是null)作为参数。Hibernate 希望为瞬态对象的类找到一个映射(注释或 XML 映射);Hibernate 不能 6 持久化任意未映射的对象。如果已经将多个实体映射到一个 Java 类,那么可以用entityName参数指定要保存哪个实体(Hibernate 不会只知道 Java 类名)。

这些save()方法都创建一个新的org.hibernate.event.spi.SaveOrUpdateEvent事件。事件是 Hibernate 中相当高级的主题,大多数读者不需要,但是感兴趣的读者可以在 Hibernate 6 文档的“事件”一章的 https://red.ht/3iGN7tZ 阅读更多内容。

最简单的方法是,我们用 Java 创建一个新对象,设置它的一些属性,然后通过会话保存它。这里有一个简单的对象,如清单 4-11 所示。

package chapter04.model;

import javax.persistence.*;

@Entity
public class SimpleObject {
  @Id
  @GeneratedValue(strategy = GenerationType.AUTO)
  Long id;
  @Column
  String key;
  @Column
  Long value;

  public SimpleObject() {
  }

  // mutators and accessors not included for brevity
  // equals() and hashCode() will be covered later in this chapter
}

Listing 4-11src/main/java/chapter04/model/SimpleObject.java

清单 4-12 展示了如何在testSaveLoad()方法中保存这个对象,如chapter04.general.SaveLoadTest所示。

package chapter04.general;

import chapter04.model.SimpleObject;
import com.autumncode.hibernate.util.SessionUtil;
import org.hibernate.Session;
import org.hibernate.Transaction;
import org.testng.annotations.Test;

import static org.testng.Assert.*;

public class SaveLoadTest {
  @Test
  public void testSaveLoad() {
    Long id = null;
    SimpleObject obj;

    try (Session session = SessionUtil.getSession()) {
      Transaction tx = session.beginTransaction();

      obj = new SimpleObject();
      obj.setKey("sl");
      obj.setValue(10L);

      session.save(obj);
      assertNotNull(obj.getId());
      // we should have an id now, set by Session.save()
      id = obj.getId();

      tx.commit();
    }

    try (Session session = SessionUtil.getSession()) {
      // we're loading the object by id
      SimpleObject o2 = session.load(SimpleObject.class, id);
      assertEquals(o2.getKey(), "sl");
      assertNotNull(o2.getValue());
      assertEquals(o2.getValue().longValue(), 10L);

      SimpleObject o3 = session.load(SimpleObject.class, id);

      // since o3 and o2 were loaded in the same session, they're not only
      // equivalent - as shown by equals() - but equal, as shown by ==.
      // since obj was NOT loaded in this session, it's equivalent but
      // not ==.
      assertEquals(o2, o3);
      assertEquals(obj, o2);
      assertEquals(obj, o3);

      assertSame(o2, o3);
      assertFalse(o2 == obj);

      assertSame(obj, o3);
      assertFalse(obj == o3);
    }
  }
}

Listing 4-12src/test/java/chapter04/general/SaveLoadTest.java

保存已经持久化的对象是不合适的。这样做将更新对象,这实际上将最终创建一个具有新标识符的副本。这可以在DuplicateSaveTest,清单 4-13 中看到。

package chapter04.general;

import chapter04.model.SimpleObject;
import com.autumncode.hibernate.util.SessionUtil;
import org.hibernate.Session;
import org.hibernate.Transaction;
import org.testng.annotations.Test;

import java.util.List;

import static org.testng.Assert.*;

public class DuplicateSaveTest {
  @Test
  public void duplicateSaveTest() {
    Long id;
    SimpleObject obj;

    try (Session session = SessionUtil.getSession()) {
      Transaction tx = session.beginTransaction();

      obj = new SimpleObject();

      obj.setKey("Open Source and Standards");
      obj.setValue(10L);

      session.save(obj);
      assertNotNull(obj.getId());

      id = obj.getId();

      tx.commit();
    }

    try (Session session = SessionUtil.getSession()) {
      Transaction tx = session.beginTransaction();

      obj.setValue(12L);

      // this is not good behavior!
      session.save(obj);

      tx.commit();
    }

    // note that save() creates a new row in the database!
    // this is wrong behavior. Don't do this!
    assertNotEquals(id, obj.getId());

    try (Session session = SessionUtil.getSession()) {
      List<SimpleObject> objects=session
          .createQuery("from SimpleObject", SimpleObject.class)
          .list();

      // again, this is a value we DO NOT WANT.
      assertEquals(objects.size(), 2);
    }
  }
}

Listing 4-13src/test/java/chapter04/general/DuplicateSaveTest.java

当这个测试运行时,两个标识符应该是相等的,但是它们不是;检查这些值产生了等效的对象,除了 id,它们是按指定的SimpleObject @Id代顺序分配的。

但是,您可以用Session.saveOrUpdate()(当然也可以用Session.update())更新一个对象。

如果对象不存在,saveOrUpdate()会调用save(),而update()不会;如果您的目标是确保数据库中存在一个对象,那么saveOrUpdate()会稍微安全一些;update()如果数据库中不存在该对象,将会失败并出现异常。例如,如果您试图更新订单发票,这将是合适的;如果它还不存在,您不会想要创建一个。

清单 4-14 显示了另一个类SaveOrUpdateTest

package chapter04.general;

import chapter04.model.SimpleObject;
import com.autumncode.hibernate.util.SessionUtil;
import org.hibernate.Session;
import org.hibernate.Transaction;
import org.testng.annotations.Test;

import java.util.List;

import static org.testng.Assert.*;

public class SaveOrUpdateTest {
  @Test
  public void testSaveOrUpdateEntity() {
    Long id;
    SimpleObject obj;
    try (Session session=SessionUtil.getSession()) {
      Transaction tx = session.beginTransaction();
      // this only works for simple objects
      session
          .createQuery("delete from SimpleObject")
          .executeUpdate();
      tx.commit();
    }

    try (Session session = SessionUtil.getSession()) {
      Transaction tx = session.beginTransaction();

      obj = new SimpleObject();

      obj.setKey("Open Source and Standards");
      obj.setValue(14L);

      session.save(obj);
      assertNotNull(obj.getId());

      id = obj.getId();

      tx.commit();
    }

    try (Session session = SessionUtil.getSession()) {
      Transaction tx = session.beginTransaction();

      obj.setValue(12L);

      // if the key didn't exist in the database,
      // it would after this call.
      session.saveOrUpdate(obj);

      tx.commit();
    }

    // saveOrUpdate() will update a row in the database
    // if one matches. This is what one usually expects.
    assertEquals(id, obj.getId());

    try (Session session = SessionUtil.getSession()) {
      List<SimpleObject> objects=session
          .createQuery("from SimpleObject", SimpleObject.class)
          .list();

      assertEquals(objects.size(), 1);
    }
  }
}

Listing 4-14src/test/java/chapter04/general/SaveOrUpdateTest.java

在生产代码中尝试匹配这种代码结构是不明智的。

对象从瞬时状态(创建时)变为持久状态(第一次保存时),然后返回瞬时状态(会话关闭时)。然后,我们在对象处于瞬态时更新它,并在调用Session.saveOrUpdate()时将它移回持久状态。

理想情况下,您首先要做的是从会话中加载对象(就像我们在其他大多数显示更新的例子中所做的那样);这意味着更新发生在持久对象上,我们实际上根本不需要调用Session.save()Session.update()Session.saveOrUpdate()7 显式调用更新方法之一不是错误,但也不是必须的。

一旦对象处于持久状态,Hibernate 就会在您更改对象的字段和属性时管理数据库本身的更新。

Hibernate 在跟踪更改方面非常有效,它只跟踪更改过的字段。在正常情况下,如果您有一个有 30 个属性的实体,并且更改了一个,Hibernate 将发出一个相当小的 SQL UPDATE来修改数据库记录。

对象相等和相同

当我们讨论 Hibernate 中的持久对象时,我们还需要考虑 Hibernate 中对象相等性和身份扮演的角色。当我们在 Hibernate 中有一个持久对象时,这个对象既代表特定 Java 虚拟机(JVM)中的一个类的实例,也代表数据库表中的一行(或多行)。

从同一个 Hibernate 会话再次请求一个持久对象会返回一个类的同一个 Java 实例,这意味着您可以使用标准的 Java ==等式语法来比较对象。但是,如果您从多个 Hibernate 会话中请求一个持久对象,Hibernate 将从每个会话中提供不同的实例,如果您比较这些对象实例,==操作符将返回false

考虑到这一点,如果您在两个不同的会话中比较对象,您将需要在您的 Java 持久化对象上实现equals()方法,无论如何您应该经常这样做。(只是别忘了一起实现hashCode()。)

实现equals()可能会很有趣。Hibernate 将实际的对象包装在代理中(出于各种性能增强的原因,比如按需加载数据),所以您需要考虑类层次结构的等价性;不要检查实际类型的等效性,而是检查类型是否为可分配的兼容的。与实际的字段相比,在你的equals()hashCode()方法中使用访问器通常更有效。

大多数 ide 会生成equals()hashCode()来使用实例引用本身,而不是访问器。这对于大多数对象来说非常有效,并且是正确的行为;毕竟,访问器通常是返回引用的单行方法。然而,一个访问器没有成为一行方法;它可能会创建一个引用的副本或计算一个值,这两种操作可能会也可能不会很昂贵。但是,在 Hibernate 的情况下,调用访问器也使代理有机会从数据库加载属性(如果它还不存在的话),这在大多数情况下是一个重要且有用的特性。

清单 4-15 是我们一直在使用的 SimpleObject 实体的equals()hashCode()的实现,由 IntelliJ IDEA8生成并修改为使用访问器。

package chapter04.model;

import javax.persistence.*;

@Entity
public class SimpleObject {
  @Id
  @GeneratedValue(strategy = GenerationType.AUTO)
  Long id;
  @Column
  String key;
  @Column
  Long value;

  public SimpleObject() {
  }

  // mutators and accessors not included for brevity

  @Override
  public boolean equals(Object o) {
    if (this == o) return true;
    if (!(o instanceof SimpleObject)) return false;

    SimpleObject that = (SimpleObject) o;

    // we prefer the method versions of accessors, because of Hibernate's proxies.
    if (getId() != null
        ? !getId().equals(that.getId())
        : that.getId() != null)
      return false;
    if (getKey() != null
        ? !getKey().equals(that.getKey())
        : that.getKey() != null)
      return false;
    return getValue() != null
        ? getValue().equals(that.getValue())
        : that.getValue() == null;
  }

  @Override
  public int hashCode() {
    int result = getId() != null ? getId().hashCode() : 0;
    result = 31 * result + (getKey() != null ? getKey().hashCode() : 0);
    result = 31 * result + (getValue() != null ? getValue().hashCode() : 0);
    return result;
  }
}

Listing 4-15src/main/java/chapter04/model/SimpleObject.java

TestSaveLoad.java类展示了等式的各种可能性和条件,如我们前面看到的清单 4-12 所示。

注意,在该代码中,o2o3等于(它们持有相同的引用),而o2obj等价的(引用不同但持有相同的数据)。同样,您不应该在生产代码中依赖这一点;对象等价应该总是用equals()来测试。

加载实体

Hibernate 的Session接口提供了几种从数据库加载实体的load()方法。每个load()方法都需要对象的主键作为标识符。 9

除了 ID 之外,Hibernate 还需要知道使用哪个类名或实体名来查找具有该 ID 的对象。如果您没有将类类型传递给load(),您还需要将结果转换为正确的类型。基本的load()方法如下:

public <T> T load(Class<T> theClass, Object id)
public Object load(String entityName, Object id)
public void load(Object object, Object id)

最后一个load()方法将一个Object作为第一个参数。该对象应该与您想要加载的对象具有相同的类类型,并且应该是空的(即,已构造,但其值对于您的应用来说缺乏意义-例如,考虑使用通过默认构造函数构造的对象)。Hibernate 将使用您请求的对象填充该对象。虽然这类似于 Java 中的其他库调用——即java.util.List.toArray()——但这种语法可能没有太大的实际好处。

其他的load()方法以锁模式作为参数。 10 锁模式指定 Hibernate 是否应该在缓存中查找对象,以及 Hibernate 应该对表示该对象的数据行使用哪个数据库锁级别。Hibernate 开发人员声称 Hibernate 通常会为您选择正确的锁模式,尽管我们已经看到手动选择正确的锁非常重要的情况。此外,您的数据库可以选择自己的锁定策略——例如,锁定整个表而不是表中的多行。按照限制最少到限制最多的顺序,您可以使用以下各种锁定模式: 11

  • NONE:不使用行级锁定,使用缓存对象(如果可用);这是休眠默认设置。

  • READ:防止其他SELECT查询在事务提交之前读取事务中的数据(因此可能是无效的)。

  • UPGRADE:使用SELECT FOR UPDATE SQL 语法(或等效语法)锁定数据,直到事务完成。(这个其实已经弃用了;用PESSIMISTIC_WRITE代替。)

  • UPGRADE_NOWAIT:使用NOWAIT关键字(对于 Oracle),如果有另一个线程使用该行,则立即返回错误;否则,这就类似于UPGRADE

  • UPGRADE_SKIPLOCKED:跳过已经被其他更新锁定的行的锁定,但在其他方面类似于UPGRADE

  • OPTIMISTIC:该模式假设更新不会经历争用。该实体的内容将在接近事务结束时得到验证。

  • OPTIMISTIC_FORCE_INCREMENT:这类似于OPTIMISTIC,除了它强制对象的版本在接近事务结束时递增。

  • PESSIMISTIC_READPESSIMISTIC_WRITE:这两个都在访问行时立即获得锁。

  • PESSIMISTIC_FORCE_INCREMENT:这将在访问行时立即获得锁,并立即更新实体版本。

所有这些锁定模式都是org.hibernate.LockMode枚举上的静态字段。(我们将在第八章更详细地讨论与事务相关的锁定和死锁。)使用锁定模式的load()方法如下:

public <T> T load(Class<T> theClass, Object id, LockMode lockMode)
public Object load(String entityName, Object id, LockMode lockMode)

除非你确定对象存在,否则不应该使用load()方法。如果您不确定,那么使用get()方法之一。如果在数据库中没有找到惟一的 ID,load()方法将抛出一个异常,而get()方法将仅仅返回一个空引用。

load()非常相似,get()方法接受一个标识符和一个实体名或一个类。还有两个get()方法将锁定模式作为参数。get()方法如下:

public <T> T get(Class<T> entityType, Object id)
public Object get(String entityName, Object id)
public <T> T get(Class<T> entityType, Object id, LockMode lockMode)
public Object get(String entityName, Object id, LockMode lockMode)

也有使用LockOptionloadget变体,但是大多数用户最终会指定映射到LockMode特性的组合。

如果您需要确定给定对象的实体名称(默认情况下,这与类名相同),您可以在Session接口上调用getEntityName()方法,如下所示:

public String getEntityName(Object object)

使用get()load()方法很简单。例如,通过 web 应用,某人可以为 ID 为 1 的供应商选择一个Supplier详细页面 12 。如果我们不确定供应商是否存在,我们使用get()方法来检查是否为空,如下所示:

// get an id from some other Java class, for instance, through a web application
Supplier supplier = session.get(Supplier.class,id);
if (supplier == null) {
    System.out.println("Supplier not found for id " + id);
    return;
}

我们还可以从 Hibernate 中检索实体名称,并将其用于get()load()方法。如上所述,如果找不到具有该 ID 的对象,load()方法将抛出一个异常。

String entityName = session.getEntityName(supplier);
Supplier secondarySupplier = (Supplier) session.load(entityName,id);

还值得指出的是,您可以查询实体,这允许您查找具有特定标识符的对象,以及匹配其他标准的对象集。还有一个 Criteria API,允许您使用声明性机制来构建查询。这些主题将在后面的章节中讨论。

合并实体

当您希望将分离的实体再次更改为持久状态时,将执行合并,并将分离的实体的更改迁移到(或覆盖)数据库。合并操作的方法签名如下:

Object merge(Object object)
Object merge(String entityName, Object object)

合并与refresh()相反,它用数据库中的值覆盖分离实体的值。首先,让我们构建一个实用方法(在它自己的类中)来帮助我们验证一个对象的值。

package chapter04.general;

import chapter04.model.SimpleObject;
import com.autumncode.hibernate.util.SessionUtil;
import org.hibernate.Session;

import static org.testng.Assert.assertEquals;

public class ValidateSimpleObject {
  public static SimpleObject validate(
      Long id,
      Long expectedValue,
      String expectedKey) {
    SimpleObject so = null;
    try (Session session = SessionUtil.getSession()) {
      // will throw an Exception if the id isn't found
      // in the database
      so = session.load(SimpleObject.class, id);

      assertEquals(so.getKey(), expectedKey);
      assertEquals(so.getValue(), expectedValue);
    }

    return so;
  }
}

Listing 4-16src/test/java/chapter04/general/ValidateSimpleObject.java

现在我们可以看一看MergeTest

package chapter04.general;

import chapter04.model.SimpleObject;
import com.autumncode.hibernate.util.SessionUtil;
import org.hibernate.Session;
import org.hibernate.Transaction;
import org.testng.annotations.Test;

public class MergeTest {
  @Test
  public void testMerge() {
    Long id;
    try (Session session = SessionUtil.getSession()) {
      Transaction tx = session.beginTransaction();

      SimpleObject simpleObject = new SimpleObject();

      simpleObject.setKey("testMerge");
      simpleObject.setValue(1L);

      session.save(simpleObject);

      id = simpleObject.getId();

      tx.commit();
    }

    SimpleObject so = ValidateSimpleObject.validate(id, 1L, "testMerge");

    // the 'so' object is detached here.
    so.setValue(2L);

    try (Session session = SessionUtil.getSession()) {
      // merge is potentially an update, so we need a TX
      Transaction tx = session.beginTransaction();

      session.merge(so);

      tx.commit();
    }

    ValidateSimpleObject.validate(id, 2L, "testMerge");
  }
}

Listing 4-17src/test/java/chapter04/general/MergeTest.java

这段代码创建一个实体(a SimpleObject)然后保存它;然后它验证对象的值(用来自ValidateSimpleObjectvalidate()方法),这本身返回一个分离的实体。我们更新分离的对象并merge()它——它应该更新数据库中写入的值,这是我们验证的。

刷新实体

Hibernate 提供了一种机制来刷新持久对象的数据库表示,覆盖内存中对象可能有的值。使用会话接口上的refresh()方法之一来刷新持久对象的实例,如下所示:

public void refresh(Object object)
public void refresh(Object object, LockMode lockMode)

如上所述,这些方法将从数据库重新加载对象的属性,覆盖它们;因此,refresh()merge()的逆。合并用先前瞬态对象的值覆盖数据库,而refresh()用数据库中的值覆盖瞬态对象中的值。

Hibernate 通常会很好地为您处理这个问题,所以您不必经常使用refresh()方法。然而,也有 Java 对象表示与对象的数据库表示不同步的情况。例如,如果您使用 SQL 来更新数据库,Hibernate 将不会意识到表示发生了变化。但是,您不需要经常使用这种方法。 13load()方法类似,refresh()方法可以以一个锁模式作为自变量;请参阅上一节“加载实体”中对锁模式的讨论。

让我们看看清单 4-18 中使用 refresh()的代码——基本上是我们看到的演示merge()的代码的逆。

package chapter04.general;

import chapter04.model.SimpleObject;
import com.autumncode.hibernate.util.SessionUtil;
import org.hibernate.Session;
import org.hibernate.Transaction;
import org.testng.annotations.Test;

public class RefreshTest {
  @Test
  public void testRefresh() {
    Long id;
    try (Session session = SessionUtil.getSession()) {
      Transaction tx = session.beginTransaction();

      SimpleObject simpleObject = new SimpleObject();

      simpleObject.setKey("testMerge");
      simpleObject.setValue(1L);

      session.save(simpleObject);

      id = simpleObject.getId();

      tx.commit();
    }

    SimpleObject so = ValidateSimpleObject.validate(id, 1L, "testMerge");

    // the 'so' object is detached here
    so.setValue(2L);

    try (Session session = SessionUtil.getSession()) {
      // note that refresh is a read,
      // so no TX is necessary unless an update occurs later
      session.refresh(so);
    }

    ValidateSimpleObject.validate(id, 1L, "testMerge");
  }
}

Listing 4-18src/test/java/chapter04/general/RefreshTest.java

这段代码与merge()测试相同,有两处变化:第一处是它调用了refresh()而不是merge()(惊喜!);另一个是它期望对象的数据从数据库恢复到原始状态,验证refresh()覆盖了瞬态对象的数据。

在本书之前的版本中,merge()refresh()测试——以及他们使用的validate()方法——都在同一个类中。在这里,它们被分开,主要是因为这允许使用完整的源代码清单。

更新实体

Hibernate 自动将对持久对象所做的更改保存到数据库中。 14 如果一个持久对象的属性发生了变化,相关的 Hibernate 会话将使用 SQL 把这个变化排队保存到数据库中。从开发人员的角度来看,您不需要做任何工作来存储这些更改,除非您想强制 Hibernate 提交队列中的所有更改。您还可以确定会话是否是脏的,是否需要提交更改。当您提交 Hibernate 事务时,Hibernate 会为您处理这些细节。

flush()方法强制 Hibernate 刷新会话,如下所示:

public void flush() throws HibernateException

您可以使用 is dirty()方法确定会话是否脏,如下所示:

public boolean isDirty() throws HibernateException

您还可以通过setHibernateFlushMode() 15 方法指示 Hibernate 为会话使用刷新模式。getHibernateFlushMode()方法返回当前会话的刷新模式,如下所示:

public void setHibernateFlushMode(FlushMode flushMode)
public FlushMode getHibernateFlushMode()

可能的冲洗模式如下:

  • ALWAYS:每个查询在执行之前都会刷新会话。这会非常慢。

  • AUTO : Hibernate 管理查询刷新,保证查询返回的数据是最新的。

  • COMMIT : Hibernate 在事务提交时刷新会话。

  • MANUAL:您的应用需要使用 flush()方法来管理会话刷新。Hibernate 从不刷新会话本身。

默认情况下,Hibernate 使用AUTO刷新模式。通常,您应该使用事务边界来确保进行适当的刷新,而不是试图在适当的时间“手动”刷新。

删除实体

为了方便从数据库中删除实体,Session接口提供了一个delete()方法,如下所示:

public void delete(Object object)
public void delete (String entityName, Object object)

这个方法接受一个持久对象作为参数。该参数也可以是一个瞬态对象,其标识符设置为需要擦除的对象的 ID。

在最简单的形式中,您只是删除一个与其他对象没有关联的对象,这很简单;但是许多对象确实与其他对象有关联。为了实现这一点,Hibernate 可以被配置为允许从一个对象到其相关对象的级联删除。

例如,考虑这样一种情况,您有一个包含子对象集合的父对象,您想删除所有子对象。处理这个问题最简单的方法是在 Hibernate 映射中对集合的元素使用 cascade 属性。如果将“级联”属性设置为“删除”或“全部”,删除将级联到所有关联的对象。Hibernate 会帮你删除这些:删除父对象会删除相关的对象。

Hibernate 还支持批量删除,即应用对数据库执行删除 HQL 语句。这对于一次删除多个对象非常有用,因为每个对象不需要为了删除而加载到内存中,如下所示:

session.createQuery("delete from User").executeUpdate();

与针对每个实体标识符单独发出delete()调用相比,网络流量大大减少,内存需求也大大减少。

批量删除不会导致级联操作的执行。如果需要级联行为,您将需要自己执行适当的删除(就像使用 SQL 一样)或使用会话的delete()方法。

级联操作

当您在实体上执行本章中描述的操作之一时,这些操作不会在关联的实体上执行,除非您明确地告诉 Hibernate 执行它们。当操作影响关联的实体时,它们被称为“级联”操作,因为操作从一个对象流向另一个对象。

例如,当我们尝试提交事务时,清单 4-19 中的代码将会失败,因为与Email实体相关联的Message实体还没有被持久化到数据库中,所以Email实体不能在它的表中被准确地表示(用它的外键表示到适当的消息行上)。

try(Session session = SessionUtil.getSession()) {
  Transaction tx=session.beginTransaction();
  Email email = new Email("Email title");
  Message message = new Message("Message content");
  email.setMessage(message);
  message.setEmail(email);
  session.save(email);
  tx.commit();
}

Listing 4-19A Failed save() Due to Cascading

理想情况下,我们希望保存操作从电子邮件实体传播到其关联的消息对象。我们通过为实体的属性和字段设置级联操作(或者为整个实体分配一个适当的默认值)来实现这一点。因此,如果至少为电子邮件实体的消息属性设置了PERSIST级联操作,清单 4-19 中的代码将正确执行。Java 持久化 API 支持的级联类型如下:

  • PERSIST

  • MERGE

  • REFRESH

  • REMOVE

  • DETACH

  • ALL

值得指出的是,Hibernate 有自己的级联配置选项, 16 代表了其中的一个超集;然而,我们很大程度上遵循 Java 持久化 API 规范进行建模,因为这通常比特定于 Hibernate 的建模更常见: 17

  • CascadeType.PERSIST表示save()persist()业务级联到相关实体;对于我们的EmailMessage示例,如果Email's @OneToOne注释包含PERSIST,保存Email也会保存Message

  • CascadeType.MERGE表示当所属实体合并时,相关实体合并为托管状态。

  • CascadeType.REFRESHrefresh()操作做同样的事情。

  • CascadeType.REMOVE删除所有实体时,删除与此设置相关的所有相关实体。

  • 如果要进行手动分离,则分离所有相关实体。

  • CascadeType.ALL是所有级联操作的简写。

级联配置选项接受一组CascadeType引用;因此,要在一对一关系的级联操作中仅包括刷新和合并,您可能会看到以下内容:

@OneToOne(cascade={CascadeType.REFRESH, CascadeType.MERGE})
EntityType otherSide;

还有一个级联操作不是正常集合的一部分,称为孤儿移除,当一个拥有的对象从其拥有关系中移除时,它会从数据库中移除该拥有的对象。但是,不建议将其用作级联类型;建议使用注释选项orphanRemoval,这样@OneToMany的注释可能看起来像OneToMany(orphanRemoval=true)

假设我们有一个Library实体,它包含一个Book实体的列表。这是我们在LibraryBook的列表。

package chapter04.orphan;

import javax.persistence.*;

@Entity
public class Book {
  @Id
  @GeneratedValue(strategy = GenerationType.AUTO)
  Long id;
  @Column
  String title;
  @ManyToOne
  Library library;

  public Book() {
  }

  public Long getId() {
    return id;
  }

  public void setId(Long id) {
    this.id = id;
  }

  public String getTitle() {
    return title;
  }

  public void setTitle(String title) {
    this.title = title;
  }

  public Library getLibrary() {
    return library;
  }

  public void setLibrary(Library library) {
    this.library = library;
  }

}

Listing 4-21src/main/java/chapter04/orphan/Book.java

package chapter04.orphan;

import javax.persistence.*;
import java.util.ArrayList;
import java.util.List;

@Entity
public class Library {
  @Id
  @GeneratedValue(strategy = GenerationType.AUTO)
  Long id;
  @Column
  String name;
  @OneToMany(orphanRemoval = true, mappedBy = "library")
  List<Book> books = new ArrayList<>();

  public Library() {
  }

  public Long getId() {
    return id;
  }

  public void setId(Long id) {
    this.id = id;
  }

  public String getName() {
    return name;
  }

  public void setName(String name) {
    this.name = name;
  }

  public List<Book> getBooks() {
    return books;
  }

  public void setBooks(List<Book> books) {
    this.books = books;
  }
}

Listing 4-20src/main/java/chapter04/orphan/Library.java

请注意在@OneToMany 注释中使用了 orphanRemoval。现在让我们看一些测试代码,这些代码相当冗长,因为我们需要验证我们的初始数据集,更改它,然后重新验证;参见清单 4-22 。

package chapter04.orphan;

import com.autumncode.hibernate.util.SessionUtil;
import org.hibernate.Session;
import org.hibernate.Transaction;
import org.hibernate.query.Query;
import org.testng.annotations.Test;

import java.util.List;

import static org.testng.Assert.assertEquals;
import static org.testng.Assert.assertNull;

public class OrphanRemovalTest {
  @Test
  public void orphanRemovalTest() {
    Long id = createLibrary();

    try (Session session = SessionUtil.getSession()) {
      Transaction tx = session.beginTransaction();

      Library library = session.load(Library.class, id);
      assertEquals(library.getBooks().size(), 3);

      library.getBooks().remove(0);
      assertEquals(library.getBooks().size(), 2);

      tx.commit();
    }

    try (Session session = SessionUtil.getSession()) {
      Transaction tx = session.beginTransaction();

      Library l2 = session.load(Library.class, id);
      assertEquals(l2.getBooks().size(), 2);

      Query<Book> query = session
        .createQuery("from Book b", Book.class);
      List<Book> books = query.list();
      assertEquals(books.size(), 2);

      tx.commit();
    }
  }

  @Test
  public void deleteLibrary() {
    Long id = createLibrary();
    try (Session session = SessionUtil.getSession()) {
      Transaction tx = session.beginTransaction();
      Library library = session.load(Library.class, id);
      assertEquals(library.getBooks().size(), 3);
      session.delete(library);
      tx.commit();
    }

    try (Session session = SessionUtil.getSession()) {
      Transaction tx = session.beginTransaction();
      Library library = session.get(Library.class, id);
      assertNull(library);
      List<Book> books=session
        .createQuery("from Book b", Book.class)
        .list();
      assertEquals(books.size(), 0);
    }
  }

  private Long createLibrary() {
    Library library = null;
    try (Session session = SessionUtil.getSession()) {
      Transaction tx = session.beginTransaction();

      library = new Library();
      library.setName("orphanLib");
      session.save(library);

      Book book = new Book();
      book.setLibrary(library);
      book.setTitle("book 1");
      session.save(book);
      library.getBooks().add(book);

      book = new Book();
      book.setLibrary(library);
      book.setTitle("book 2");
      session.save(book);
      library.getBooks().add(book);

      book = new Book();
      book.setLibrary(library);
      book.setTitle("book 3");
      session.save(book);
      library.getBooks().add(book);

      tx.commit();
    }

    return library.getId();
  }
}

Listing 4-22src/test/java/chapter04/orphan/OrphanRemovalTest.java

这并不复杂:它构建了一个包含三本书的图书馆。然后,它从数据库加载图书馆,验证它看起来像它应该的那样(“一个有三本书的图书馆”),并从图书馆中删除一本。它不删除被删除的Book实体;它只是将它从图书馆的图书集中删除,从而使它成为孤儿。

在提交了图书馆对象的新状态之后——通过tx.commit()——我们从数据库中重新加载图书馆,并验证它现在只有两本书。我们搬走的那本书从图书馆不见了。

但是,这并不意味着它实际上已经被删除了,所以我们然后查询数据库中所有的Book实体,看看我们是否有两个或三个。我们应该只有两个,事实也的确如此。更新库时,我们删除了孤立对象。

如果你想让这本书在搬走后还在,或者被分配到其他图书馆,那么orphanRemoval是不正确的;你会希望这本书能够作为一个孤儿存在。

延迟加载、代理和集合包装器

考虑典型的互联网 web 应用:在线商店。这家商店有一份产品目录。在最粗糙的层次上,这可以被建模为管理一系列产品实体的目录实体。在大型商店中,可能有成千上万的产品被分成不同的重叠类别。

当客户访问商店时,必须从数据库中加载目录。我们可能不希望实现将代表成千上万个产品的每个实体都加载到内存中。对于一个足够大的零售商来说,考虑到机器上可用的物理内存量,这甚至是不可能的。即使这是可能的,它也可能会削弱网站的性能。

相反,我们只希望加载目录,也可能加载类别。只有当用户深入到类别中时,才应该从数据库中加载该类别中产品的子集。

为了解决这个问题,Hibernate 提供了一个名为 lazy loading 的工具。启用时(这是使用 XML 映射时的默认设置,而不是使用注释时的默认设置,默认设置为急切加载,实体的关联实体只有在被直接请求时才会被加载,这可以提供相当可观的性能优势,这是可以想象的。例如,以下代码仅从数据库中加载一个实体:

Email email = session.get(Email.class,new Integer(42));

但是,如果访问了类的关联,并且延迟加载生效,则仅在需要时从数据库中提取关联。例如,在下面的代码片段中,关联的消息对象将被加载,因为它被显式引用:

// surely this email is about the meaning of life, the universe, and everything
Email email = session.get(Email.class,new Integer(42));
String text = email.getMessage().getContent();

Hibernate 将这种行为强加于您的实体的最简单方法是提供它们的代理实现。 18 Hibernate 通过替换从实体的类中派生的代理来拦截对实体的调用。如果缺少请求的信息,那么在将控制权交给父实体的实现之前,将从数据库中加载该信息。在将关联表示为集合类的情况下,会创建一个包装器(本质上是集合的代理,而不是它所包含的实体的代理)并替换原始集合。

Hibernate 只能通过会话访问数据库。如果当我们试图访问一个还没有加载的关联(通过代理或集合包装器)时,一个实体从会话中分离出来,Hibernate 抛出一个LazyInitializationException。解决方法是通过将实体附加到会话来确保实体再次持久化,或者在实体从会话分离之前访问所有需要的字段。

如果您需要确定一个代理、一个持久化集合或者一个属性是否已经被延迟加载,您可以调用org.hibernate.Hibernate类上的isInitialized(Object proxy)isPropertyInitialized(Object proxy, String propertyName)方法。您还可以通过调用org.hibernate.Hibernate类上的initialize(Object proxy)方法来强制代理或集合完全填充。如果使用此方法初始化集合,还需要初始化集合中包含的每个对象,因为只有集合保证会被初始化。

查询对象

Hibernate 提供了几种不同的方法来查询存储在数据库中的对象。显然,如果您已经知道一个对象的标识符,您可以使用该标识符从数据库中加载它。标准查询 API 是一个 Java API,用于将查询构造为一个对象。HQL 是一种面向对象的查询语言,类似于 SQL,您可以使用它来检索与查询匹配的对象。我们将在第九章和第十章中进一步讨论这些问题。如果您有使用 SQL 的遗留应用,或者如果您需要使用 HQL 和条件查询 API 不支持的 SQL 特性,Hibernate 提供了一种直接对数据库执行 SQL(通过“原生查询”)以检索对象的方法。

摘要

Hibernate 提供了一个简单的 API,用于通过会话接口创建、检索、更新和删除关系数据库中的对象。理解 Hibernate 中瞬时对象、持久对象和分离对象之间的区别,将使您理解对对象的更改如何更新数据库表。

我们已经提到了创建映射的需要,以便将数据库表与您想要持久化的 Java 对象的字段和属性相关联。下一章将详细介绍这些,并讨论为什么需要它们以及它们可以包含什么。

Footnotes 1

如果这些术语有点熟悉,你可能已经读过第三章。

  2

正如我们在第三章看到的,HQL 使用的是实体名,而不是类名;但是因为我们没有指定任何定制的实体名,类名和实体名是相同的。

  3

美国社会安全管理局表示,他们有足够的社会安全号码来分配“几代人”的唯一身份。(参见 www.ssa.gov/history/hfaq.html ,Q20。)对于自然标识符来说,这可能已经足够好了,尽管隐私倡导者会理所当然地抱怨;另外,请注意“几代人”可能还不够。程序员们绝对肯定,没有人还会有两位数年份的数据…直到 2000 年,花了很多工时来修复。

  4

在早期的印刷中,这里的数字是“十”但是”@gmail.com”有十个字符长,都是它自己。公平地说,担心几十个字节可能是不明智的,但浪费内存也是不明智的。

  5

例如,你的作者至少有七个“主要电子邮件地址”可供选择。

  6

更正:Hibernate 持久化任意未映射的对象。这是一件好事。

  7

我们在第三章中看到 Hibernate 在不调用save()的情况下更新一个对象 : chapter03.hibernate.RankingTest’s changeRanking()方法对一个持久对象进行就地更新。

  8

以防你不知道:IDEA 是 Java 的 IDE。它有一个免费的社区版和一个商业“终极”版。可见于 http://jetbrains.com/idea

  9

像往常一样,除了我们在这里讨论的,还有更多内容。随着我们不断了解 Hibernate 的功能,我们将在这个列表中添加更多的方法。为了简单起见,我们保持列表较小。

  10

也有接受LockOption参数的表单,但是LockMode表单甚至在 Hibernate 的文档中也被描述为指定锁选项的一种便捷方式。

  11

https://docs.jboss.org/hibernate/orm/6.0/javadocs/org/hibernate/LockMode.html 可以看到LockMode的文档。

  12

不是这个java.util.function.Supplier,而是一个的商业实体,名为Supplier

  13

如果你发现自己经常使用refresh(),你可能会想办法消除你不得不使用refresh()的原因。并不是说refresh()不好,而是它是一种不应该经常使用的纠正措施。

  14

我们已经几次提到 Hibernate 更新附加到Session的对象,以及测试代码。

  15

为了避免 Java 持久化 API 标准中的定义和含义,setHibernateFlushMode()的名称已经从 Hibernate 的早期版本中改变了。

  16

如果你想看到 Hibernate 的层叠选项的完整列表,请看 https://docs.jboss.org/hibernate/orm/6.0/javadocs/org/hibernate/annotations/CascadeType.html

  17

这就是关于标准的事情:他们是标准的。

  18

代理是我们在equals()hashCode()示例中使用访问器的原因。如果需要的话,使用访问器给代理一个加载信息的机会。

 

*

五、映射概述

Hibernate 的目的是允许您将数据库视为存储 Java 对象。然而,在实践中,关系数据库不存储对象——它们将数据存储在表和列中。不幸的是,没有简单的方法可以将存储在关系数据库中的数据与 Java 对象表示的数据一致地关联起来。 1

面向对象的关联和关系的关联之间的区别是根本性的。考虑一个简单的类来表示用户,另一个类来表示电子邮件地址,如图 5-1 所示。

img/321250_5_En_5_Fig1_HTML.png

图 5-1

简单的实体关系图

这里,User对象包含引用Email对象的字段。协会有方向;给定一个User对象,您可以确定其关联的Email对象。比如考虑上市 5-1 。

User user = getUserSomehow();
Email email = user.email;

Listing 5-1Acquiring the Email Object

from User

然而,反之则不然。在数据库中表示这种关系的自然方式,如图 5-2 所示,表面上是相似的。

img/321250_5_En_5_Fig2_HTML.png

图 5-2

相关的联系

尽管如此,这种联系的方向实际上是相反的。给定一个Email行,您可以立即确定它在数据库中属于哪个用户行;这种关系是由外键约束规定的。通过适当使用 SQL,有可能逆转数据库世界中的关系——这是另一个不同之处。

考虑到这两个世界之间的差异,有必要手动干预以确定您的 Java 类应该如何在数据库表中表示。

为什么映射不容易实现自动化

为什么不能创建简单的规则来将 Java 对象存储在数据库中,以便可以轻松地检索它们,这并不总是显而易见的。例如,最显而易见的规则是 Java 类必须与单个表相关。例如,图 5-2 中定义的User类的实例肯定可以用一个简单的表来表示,如清单 5-2 所示。

public class User {
  String name;
  String password;
}

Listing 5-2A Simple User Class

with a Password

确实可以,但一些问题出现了:

  • 如果你保存一个用户两次,你会得到多少行?

  • 你可以保存一个没有名字的用户吗?

  • 允许你保存一个没有密码的用户吗?

当您开始考虑引用其他类的类时,还有一些额外的问题需要考虑。看看清单 5-3 中显示的客户和电子邮件类。

public class Customer {
  int customerId;
  int customerReference;
  String name;
  Email email;
}

public class Email {
  String address;
}

Listing 5-3Customer and Email Classes

基于此,出现了以下问题:

  • 唯一客户是通过其客户 ID 还是客户证明来识别的?

  • 一个电子邮件地址可以被多个客户使用吗?

  • 客户可以有多个电子邮件 ID 吗?

  • 客户表中是否应该表示这种关系?

  • 是否应该在电子邮件表中表示这种关系?

  • 这种关系应该在第三个(链接)表中表示吗?

根据对这些问题的回答,您的数据库表可能会有很大的不同。你可以尝试一个合理的设计,如图 5-3 所示,基于你对现实世界中可能发生的情况的直觉。

img/321250_5_En_5_Fig3_HTML.png

图 5-3

相关的联系

这里,我们有一个表,其中一个客户有一个假的客户 Id;电子邮件地址只能由一个客户使用,关系由Email表维护。

让 JBoss Tools 或 IDEA Ultimate 之类的工具从数据库表中生成 Hibernate 实体是完全可能的(实际上也相当常见),因为工具可以访问实际的数据库结构;工具可以看出customerIdEmail的外键,期望在Customer表中有相应的值,并且可以找出合适的名称(列名),这可能是一种惯用的映射。

但是 Hibernate 并不强制要求这些;你可以拥有 Hibernate 可以映射的多层模式关系,但是一个工具可能会设计得很糟糕。

主键

大多数提供 SQL 访问的关系数据库都准备接受没有预定义主键的表。Hibernate 没那么宽容;即使创建的表没有主键,Hibernate 也会要求您指定一个主键。对于熟悉 SQL 和数据库,但不熟悉 ORM 工具的用户来说,这似乎有悖常理。因此,让我们更深入地检查没有主键时出现的问题。

首先,如果没有主键,就不可能(容易地)唯一标识表中的一行。例如,考虑表 5-1 。

表 5-1

一种表,其中的行不容易被唯一标识

|

用户

|

年龄

| | --- | --- | | dminter | 35 | | dminter | 40 | | dminter | 55 | | dminter | 40 | | jlinwood | 57 |

该表清楚地包含了关于用户及其各自年龄的信息。然而,有四个用户具有相同的标识符(戴夫明特、丹尼斯明特、丹尼尔明特和达希尔明特)。在系统中的其他地方可能有办法区分他们——也许是通过电子邮件地址或用户号码。但是如果你想知道用户 ID 为 32 的 Dashiel Minter 的年龄,就没有办法从表 5-1 中获取。

虽然 Hibernate 不允许您省略主键,但是它允许您从一组列中形成主键,形成一个“组合键”例如,表 5-2 可以用组合键usernumberuser来标识。

表 5-2

可能存在复合主键的表

|

用户

|

用户编号

|

年龄

| | --- | --- | --- | | dminter | 1 | 35 | | dminter | 2 | 40 | | dminter | 3 | 55 | | dminter | 32 | 40 | | jlinwood | 1 | 57 |

UserUsernumber都不包含唯一的条目,但是它们组合起来唯一地标识了特定用户的年龄,因此它们可以作为主键被 Hibernate 接受。

为什么 Hibernate 需要唯一标识条目,而 SQL 不需要?因为 Hibernate 表示的是 Java 对象,这些对象总是唯一可识别的。新 Java 开发人员犯的经典错误是使用==操作符而不是equals()方法来比较字符串。您可以区分对代表相同文本的两个 String 对象的引用和对同一String对象的两个引用。 2 SQL 没有这样的义务,可以说在某些情况下,放弃进行区分的能力是可取的。

例如,如果 Hibernate 不能用主键唯一地标识一个对象,那么下面的代码在基础表中可能会有几种结果:

String customer = getCustomerFromHibernate("dcminter");
customer.setAge(10);
saveCustomerToHibernate(customer);

假设该表最初包含表 5-3 中所示的数据。

表 5-3

更新不明确的表

|

用户

|

年龄

| | --- | --- | | dminter | 35 | | dminter | 40 |

结果表中应包含以下哪一项?

  • 用户 dcminter 的单行,年龄设置为 10

  • 用户的两行,两个年龄都设置为 10 岁

  • 用户的两行,一行年龄设置为 10,另一行设置为 40(更新第一个dminter记录)

  • 用户的两行,一行年龄设置为 10,另一行设置为 30(更新第二个记录)

  • 用户有三行,一行年龄设置为 10,其他的年龄设置为 35 和 40(总共创建一个新的dminter记录)

有很多关于我们虚构的saveCustomerToHibernate()在这里做什么的假设,其中一些表面上听起来绝对疯狂…但是这里考虑的想法是,如果我们说“将记录的年龄设置为一个给定值”,数据库的状态可能是什么?**

*简而言之,Hibernate 开发人员决定在创建映射时强制使用主键,这样就不会出现这个问题。Hibernate 确实提供了一些工具,允许您在绝对必要的情况下解决这个问题(您可以创建视图或存储过程来“伪造”适当的键,或者您可以使用传统的 JDBC 来访问表数据),但是在使用 Hibernate 时,如果可能的话,最好是使用正确指定了主键的表。

惰性装载

当您将类从数据库加载到内存中时,您不一定希望所有的信息都被加载。举一个(相当)极端的例子,加载电子邮件列表不应该导致每封电子邮件的全文和附件都被加载到内存中。

首先,电子邮件的全部内容可能需要比实际可用内存更多的内存。

第二,即使电子邮件能够被存储,也可能需要很长时间才能获得所有这些信息。(请记住,数据通常通过网络从数据库进程传输到应用,即使您的网络很快,数据传输仍然需要时间。)

如果您要在 SQL 中解决这个问题,您可能会为查询选择适当字段的子集以获得列表,或者限制数据的范围。以下是选择数据子集的示例:

SELECT from, to, date, subject FROM email WHERE username = 'dcminter';

Hibernate 将允许您设计与此非常相似的查询,但它也提供了一种更灵活的方法,称为延迟加载。某些关系可以被标记为“懒惰”,它们不会从数据库中被加载,直到它们被真正需要。

Hibernate 的默认设置是类(包括像SetList这样的集合)应该被延迟加载。例如,当从数据库加载下一个清单中给出的 User 类的实例时,加载后立即初始化的字段只有userIdusername : 3

public class User {
   int userId;
   String username;
   EmailAddress emailAddress;
   Set<Role> roles;
}

根据这一定义,如果会话仍处于活动状态,那么当访问emailAddressroles的适当对象时,将从数据库中加载这些对象。

这只是默认行为;映射可用于指定哪些类和字段应该以这种方式运行。

联合

当我们看到为什么映射过程不能自动化时,我们讨论了 Java 中可能看起来像这样的类:

public class Customer {
   int customerId;
   int customerReference;
   String name;
   StreetAddress address;
}
public class StreetAddress {
   String address;
}

我们也给出了它所提出的以下五个问题:

  • 唯一客户是通过其客户 ID 还是客户证明来识别的?

  • 一个给定的电子邮件地址可以被多个客户使用吗?

  • 客户表中是否应该表示这种关系?

  • 是否应该在电子邮件表中表示这种关系?

  • 这种关系应该在第三个(链接)表中表示吗?

第一个问题可以简单回答;这取决于您将哪个列指定为主键。剩下的四个问题是相关的,它们的答案取决于对象关系。此外,如果您的客户类使用集合类或数组来表示与 EmailAddress 的关系,那么一个用户可能会有多个电子邮件地址: 4

public class Customer {
   int customerId;
   int customerReference;
   String name;
   Set<EmailAddress> email;
}

所以,你应该再加一个问题:一个客户可以有一个以上的邮箱吗?该集合可能包含单个条目,因此您不能自动推断出这种情况。

先前选项的关键问题如下:

  • Q1:一个电子邮件地址可以属于多个用户吗?

  • Q2:客户可以有一个以上的电子邮件地址吗?

这些问题的答案可以形成一个真值表,如表 5-4 所示。

表 5-4

决定实体关系的基数

|

Q1 回答

|

Q2 回答

|

CustomerEmail的关系

| | --- | --- | --- | | 不 | 不 | 一对一 | | 是 | 不 | 多对一 | | 不 | 是 | 一对多 | | 是 | 是 | 多对多 |

这是表示对象之间关系的基数 5 的四种方式。然后,可以用各种方式在映射表中表示每个关系。

一对一的联系

类之间的一对一关联可以用多种方式表示。最简单的方法是,在同一个表中维护两个类的属性。例如,用户和电子邮件类之间的一对一关联可以表示为单个表,如表 5-5 所示。

表 5-5

一个组合的User / Email

|

身份

|

用户名

|

电子邮件

| | --- | --- | --- | | 1 | dminter | dminter@example.com | | 2 | jlinwood | jlinwood@example.com | | 3 | jbo | whackadoodle@example.com |

或者,实体可以用相同的主键(如此处所示)在不同的表中维护,或者用一个键从一个实体维护到另一个实体,如表 5-6 和 5-7 所示。

表 5-7

Email一对一的牌桌

|

身份

|

电子邮件

| | --- | --- | | 1 | dminter@example.com | | 2 | jlinwood@example.com | | 3 | whackadoodle@example.com |

表 5-6

User一对一的牌桌

|

身份

|

用户名

| | --- | --- | | 1 | dminter | | 2 | jlinwood | | 3 | jbo |

可以创建从一个实体到另一个实体的强制外键关系,但是这不应该在两个方向上都应用,因为这样会创建循环依赖。也可以完全省略外键关系,依靠 Hibernate 来管理键的选择和分配。

使用外键关系!如果您的数据集非常非常小,它们可能不会有太大帮助——但在任何真实的数据库环境中,它们可以防止处理数据时出现一些真正令人沮丧的延迟。

如果这两个表不适合共享主键,那么可以维护这两个表之间的外键关系,对外键列应用一个UNIQUE约束。例如,重用我们刚刚看到的User表,可以适当地填充Email表,如表 5-8 所示。

表 5-8

与辅助外键一对一的Email

|

身份

|

使用者辩证码

|

电子邮件

| | --- | --- | --- | | 34 | 1 | dminter@example.com | | 37 | 2 | jlinwood@example.com | | 639 | 3 | whackadoodle@example.com |

这样做的好处是,通过删除外键列上的唯一约束,关联可以很容易地从一对一变为多对一。

一对多和多对一关联

一对多关联(或者从另一个类的角度来看,多对一关联)可以简单地用外键来表示,没有额外的约束。

该关系也可以通过使用链接表来维护。这将在每个关联表中维护一个外键,该外键本身将形成链接表的主键。对于多对多的关系,链接表实际上是强制性的,但是对于在关系的一端基数为 1 的关系,当关系的状态没有在对象本身中反映出来时(比如在List中某个东西可能在哪里),或者当对象不应该有对另一个实体的显式引用时,倾向于使用链接表。

表 5-11

在 1:M 关系中连接电子邮件和用户的链接表

|

使用者辩证码

|

电子邮件 ID

| | --- | --- | | 1 | 1 | | 1 | 2 | | 2 | 3 | | 2 | 4 |

表 5-10

简单的电子邮件表

|

身份

|

电子邮件

| | --- | --- | | 1 | dcminter@example.com | | 2 | dave@example.com | | 3 | jlinwood@example.com | | 4 | jeff@example.com |

表 5-9

一个简单的用户表

|

身份

|

用户名

| | --- | --- | | 1 | dcminter | | 2 | jlinwood |

可以将附加列添加到链接表中,以维护关联中实体的排序信息。

必须将唯一约束应用于关系的“一”方(表 5-11 中用户电子邮件表的UserID列);否则,链接表可以表示UserEmail实体之间所有可能关系的集合,这是一个多对多的集合关联。

多对多关联

正如上一节末尾所提到的,如果在使用链接表时没有对关系的“一”端应用惟一的约束,那么它就变成了一种有限的多对多关系。可以表示UserEmail的所有可能组合,但是对于同一个用户来说,不可能将同一个电子邮件地址实体关联两次,因为这需要复制复合主键。

如果不是将外键一起用作复合主键,而是给链接表一个自己的主键(通常是一个代理键),那么两个实体之间的关联就可以转化为完全的多对多关系,如表 5-12 所示。

表 5-12

多对多用户/电子邮件链接表

|

身份

|

使用者辩证码

|

电子邮件 ID

| | --- | --- | --- | | 1 | 1 | 1 | | 2 | 1 | 2 | | 3 | 1 | 3 | | 4 | 1 | 4 | | 5 | 2 | 1 | | 6 | 2 | 2 |

表 5-12 可能描述了一种情况,其中用户dcminter接收发送到四个地址中任何一个的所有电子邮件,而jlinwood只接收发送到他自己账户的电子邮件。(“EmailId”是接收的电子邮件地址,两个用户 Id 都引用编号为 1 和 2 的电子邮件地址;只有dcminter有 id 为 3 和 4 的电子邮件地址的参考。)

当链接表有自己独立的主键时,应该考虑这样一种可能性,即需要创建一个新的类来将链接表的内容表示为一个独立的实体。这允许您在链接对象中嵌入一个附加状态(比如“这个电子邮件地址被使用了多少次?”).

将映射应用于关联

应用映射来表达在基础表中形成关联的各种不同方式;没有绝对正确的方法来表示它们。 6

除了基本的方法选择之外,映射还用于指定表表示的细节。虽然 Hibernate 倾向于尽可能使用合理的默认值,但通常最好覆盖这些值。例如,Hibernate 自动生成的外键名实际上是随机的,而有见识的开发人员可以应用一个名称(例如,FK_USER_EMAIL_LINK)来帮助在运行时调试违反约束的情况。

其他支持的功能

虽然 Hibernate 可以为映射确定许多合理的默认值,但是大多数默认值都可以被基于注释和基于 XML 的 7 方法中的一种或两种方法覆盖。有些直接应用于映射;其他的,比如外键名,实际上只有在映射用于创建数据库模式时才是相关的。最后,一些映射还可以提供一个配置一些特性的地方,这些特性可能不是最纯粹意义上的“映射”。

除了已经提到的特性之外,本章的最后几节还将讨论 Hibernate 支持的特性。

(数据库)列类型和大小的规范

Java 提供了基本类型,并允许用户声明接口和类来扩展这些类型。关系数据库通常提供一小部分“标准”类型,然后提供额外的专有类型。

将自己局限于专有类型仍然会导致问题,因为在这些类型和 Java 原语类型之间只有近似的对应关系。

有问题类型的一个典型例子是java.lang.String(Hibernate 将其视为原始类型,因为它被频繁使用),默认情况下,它将被映射到固定大小的字符数据数据库类型。通常,如果选择了一个无限大小的字符字段,数据库的性能会很差,但是冗长的String字段会被截断,因为它们被持久化到数据库中。在大多数数据库中,您会选择将一个冗长的String字段表示为TEXTCLOB或 long VARCHAR类型(假设数据库支持特定类型)。这就是 Hibernate 不能为您完成所有映射的原因之一,也是您在创建使用 ORM 的应用时仍然需要理解一些数据库基础知识的原因之一。

通过指定映射细节,开发人员可以在存储空间、性能和对原始 Java 表示的保真度之间做出适当的权衡。

继承关系到数据库的映射

没有 SQL 标准来表示表中数据的继承关系;虽然一些数据库实现为此提供了专有的语法,但并不是所有的都这样。Hibernate 提供了几种可配置的方法来表示继承关系,这种映射允许用户为他们的模型选择合适的方法。

主关键字

正如本章前面所述(在“主键”一节中),Hibernate 要求使用主键来标识实体。可以通过配置来选择代理键、从业务数据中选择的键和/或复合主键。

当使用代理键时,Hibernate 还允许从一系列可移植性和效率不同的技术中选择键生成技术。(这显示在第四章的“标识符”部分。)

使用基于 SQL 公式的属性

有时,希望实体的属性不是作为直接存储在数据库中的数据来维护,而是作为对该数据执行的函数来维护——例如,小计字段不应该由 Java 逻辑直接管理,而是作为其他一些属性的聚合函数来维护。

强制性和唯一性约束

除了主键或外键关系的隐式约束,您还可以指定字段不能重复——例如,username字段通常应该是唯一的。 8

字段也可以是强制性的,例如,要求消息实体同时具有主题和消息文本。生成的数据库模式将包含相应的NOT NULLUNIQUE约束,因此用无效数据破坏表非常非常困难(相反,如果试图这样做,应用逻辑将抛出异常)。

请注意,主键隐含地既是强制的又是唯一的。

摘要

本章概述了为什么需要映射,以及除了这些绝对需求之外,它们还支持哪些特性。它讨论了各种类型的关联,以及在什么情况下您会选择使用它们。下一章着眼于如何指定映射。

Footnotes 1

如果有简单、一致和准确的方法来关联对象结构和关系数据库,像这样的书可能就不会存在了。

  2

比较对象的等价性时,使用equals()。这就像比较两个门把手:这两个是互相像,还是同一个门把手*?equals()方法检查它们是否相似。==操作员检查它们是否是同一个门把手。*

*  3

这是有条件的。在我们看到的大多数例子中,大多数列都是通过它们的属性引用直接访问的,不管是什么类型,延迟加载都是很常见的。如有疑问,请具体说明并测试。

  4

事实上,我们的 ERD 指出一个Customer实际上可以有多个电子邮件地址,有鱼尾纹;Email表上的“鱼尾纹”表示一对多关系。

  5

基数指的是编号,所以关系中的基数表示每个参与者有多少被关系的任何一方引用。

  6

事实上,没有完美、正确的方法来概括实体之间的关系,这就是为什么像 Hibernate 这样出色的工具还没有取代优秀的数据库分析师。

  7

我们实际上没有在 XML 配置上花太多时间,这是有充分理由的:除非绝对必要,否则大多数人不会在现实世界中使用它。它也过于冗长,尤其是与注释相比。

  8

当你的作者重读“一个username字段应该经常是唯一的”时,一个想法出现了:什么时候一个曾经希望不是这样?