Hibernate6-入门手册-三-

82 阅读1小时+

Hibernate6 入门手册(三)

原文:Beginning Hibernate 6

协议:CC BY-NC-SA 4.0

六、使用注解的映射

在第五章中,我们讨论了在数据库模型和对象模型之间创建映射的需要。可以通过两种不同的方式创建映射:通过内联注解(正如我们到目前为止在本书中所做的那样),或者作为两种主要格式之一的单独的 XML 文件(Hibernate 的内部 XML 格式和 JPA 的映射格式,这两种格式都有价值,但不建议用于大多数应用)。

基于 XML 的映射很少在需要将对象模型映射到预先存在的模式的情况之外使用;即使这样,对注解的熟练使用也可以匹配 XML 配置的特性。

使用注解创建 Hibernate 映射

在内联注解出现之前,创建映射的唯一方式是通过 XML 文件——尽管 Hibernate 和第三方项目的工具 1 允许部分或全部从 Java 源代码中生成。尽管使用注解是定义映射的最新方法,但它并不是最好的方法。在讨论何时以及如何应用注解之前,我们将简要讨论注解的缺点和优点。

注解的缺点

如果您从早期的 Hibernate 环境升级,您可能已经有了基于 XML 的映射文件来支持您的代码库。在其他条件相同的情况下,您不会希望仅仅为了重新表达这些映射而使用注解。

如果您正在从遗留环境中迁移,您可能不希望改变预先存在的 POJO 源代码,以免用可能的 bug 污染已知的好代码。 2 注解毕竟被编译到类文件中,因此可能被认为是对源代码或交付的工件的更改。

如果您没有 POJOs 的源代码(因为它是由自动化工具或类似工具生成的),那么您可能更喜欢使用外部的基于 XML 的映射来反编译类文件,以获得用于修改的 Java 源代码。

将映射信息作为外部 XML 文件进行维护,可以修改映射信息以反映业务变化或模式变更,而不必强制您重新构建整个应用。然而,当您有一个构建系统(Maven 或 Gradle,以及您可能有的任何持续集成工具)时,构建一个应用通常是非常容易的,所以这无论如何都不是一个令人信服的论点。

注解的好处

考虑到缺点,使用注解有一些强大的好处。

首先,也许是最有说服力的一点,我们发现基于注解的映射比基于 XML 的映射更直观,因为它们与相关的属性一起直接出现在源代码中。大多数编码人员倾向于使用注解,因为需要保持相互同步的文件更少。

部分由于这个原因,注解比它们的 XML 对等物更少冗长,清单 6-1 和 6-2 之间的对比证明了这一点,两者都会在数据库中创建一个Sample实体。这里的 XML 直接映射到正在使用的注解。

<?xml version='1.0' encoding='utf-8'?>
<!DOCTYPE
   hibernate-mapping
   PUBLIC
   "-//Hibernate/Hibernate Mapping DTD//EN"
   "http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd">
<hibernate-mapping default-access="field">
   <class name="Sample">
      <id type="int" column="id">
         <generator class="native"/>
      </id>
      <property name="name" type="string"/>
   </class>
</hibernate-mapping>

Listing 6-2An Equivalent Mapping with XML

import javax.persistence.* ;
@Entity
public class Sample {
  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  public Integer id;
  public String name;
}

Listing 6-1A Minimal Class Mapped with Annotations

后一个清单中的一些冗长是 XML 本身的特性(标记名和样板文档类型声明),还有一些是由于注解与源代码的紧密集成。例如,在这里,XML 文件必须显式声明使用字段访问来代替属性访问(即,直接访问字段,而不是通过它们的 get/set 方法);但是注解从它被应用于id字段而不是getId()方法的事实中推断出这一点。

Hibernate 使用并支持 JPA 2 持久化注解。如果您选择在代码和注解中不使用特定于 Hibernate 的特性,那么您可以自由地使用其他支持 JPA 2 的 ORM 工具将您的实体部署到环境中。

最后——也许是次要的一点——因为注解被直接编译到适当的类文件中,所以缺少或陈旧的映射文件在部署时引起问题的风险更小(这一点可能对那些已经对 XML 技术的这种危险有一些经验的人最有说服力)。

选择要使用的映射机制

一般来说,更喜欢注解;注解本身在 JPA 实现中是可移植的,这是众所周知的。工具可以直接从数据库中创建带注解的源代码,因此,即使使用预先存在的模式,同步也不是什么大问题。

XML 映射可以用 Hibernate 的专有格式或 JPA 的标准 XML 配置来完成,这两种格式类似但不完全相同;如果您发现 XML 是一种更好的配置格式,那么最好使用行业标准 JPA 配置中的 XML 格式。 3

JPA 2 持久化注解

当您使用注解进行开发时,您从一个 Java 类开始,然后用元数据符号注解源代码清单。Hibernate 在运行时使用反射来读取注解并应用映射信息。如果您想使用 Hibernate 工具来生成数据库模式,您必须首先编译包含注解的实体类。在这一节中,我们将介绍 JPA 2 注解的重要核心,以及一组简单的类来说明它们是如何应用的。

最常见的注解是@Entity@Id@Column,仅供参考;其他常见的还有@GenerationStrategy(与@Id关联)以及@OneToOne@ManyToOne@OneToMany@ManyToMany等与关联相关的注解。

示例类集代表出版商的图书目录。您从一个单独的类Book开始,它没有注解或映射信息。我们还将添加Author作为一个实体。就本例而言,您没有现有的数据库模式可以使用,因此您需要在使用过程中定义您的关系数据库模式。

这个Book类非常简单,就像我们开始时一样。它有两个字段,titlepages,以及一个标识符,id,它是一个整数。标题是一个String对象,pages是一个整数。在这个例子中,我们将向Book类添加注解、字段和方法。本章末尾给出了BookAuthor类的完整源代码清单;其余部分的源文件可以在 Apress 网站上本章的源代码下载中获得( www.apress.com )。

清单 6-3 以未标注的形式给出了 Book 类源代码的基本外壳,作为该示例的起点。

package chapter06.primarykey.before;

public class Book {
  String title;
  int pages;
  int id;

  public Book() {
  }

  public String getTitle() {
    return title;
  }

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

  public int getPages() {
    return pages;
  }

  public void setPages(int pages) {
    this.pages = pages;
  }

  public int getId() {
    return id;
  }

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

Listing 6-3src/main/java/chapter06/primarykey/before/Book.java

正如你所看到的,这是一个 POJO,尽管没有一些我们可能希望它拥有的东西,比如equals()hashCode()toString()。我们将继续注解这个类,解释注解背后的概念。最后,我们将把它转移到一个不同的包中,这样我们就可以对实体应该是什么样的有一个好的前后对比图。

带有@Entity的实体 Beans

第一步是将Book类注解为 JPA 2 实体 bean。我们将@Entity注解添加到Book类中,如下所示:

import javax.persistence.Entity;

@Entity
public class Book
  // Declaration of instance variables goes here
  public Book() {
  }
  // .. the rest of the class

这些源列表是“正在进行中的”,所以它们在书的源代码中没有单独的文件。该类的实际源代码将与我们在本章中开发的代码不同。我们也将在本章的后面看到“最终产品”。

JPA 2 标准注解包含在javax.persistence包中,所以我们导入适当的注解。一些 ide 将使用特定的导入,而不是“星形导入”,在这种情况下,一个给定的包中的几个类被导入;通常,一个实体会使用这个包中的相当多的注解,所以在任何情况下都有可能以 star imports 结束。4

@Entity注解将该类标记为实体 bean,因此它必须有一个至少在protected范围内可见的无参数构造函数。 5 Hibernate 最少支持包范围,但是如果你利用了这一点,你就失去了对其他 JPA 实现的可移植性。实体 bean 类的其他 JPA 2 规则是:( a)该类不能是最终的,以及(b)实体 bean 类必须是具体的。JPA 2 实体 bean 类和 Hibernate 持久化对象的许多规则是相同的——一部分是因为 Hibernate 团队在 JPA 2 设计过程中投入了很多,另一部分是因为设计相对不引人注目的对象关系持久化解决方案的方法就这么多。

到目前为止,我们已经添加了实体注解、构造函数和导入语句。POJO 的其余部分被单独留下。

带有@Id@GeneratedValue的主键

每个实体 bean 都必须有一个主键,您可以用@Id注解在类上对其进行注解。通常,主键是单个字段,尽管它也可以是多个字段的组合。

@Id注解的位置决定了 Hibernate 用于映射的默认访问策略。如果注解应用于一个字段,如我们的下一个代码片段所示,那么字段访问将通过反射使用:

import javax.persistence.Entity;
import javax.persistence.Id;

@Entity
public class Sample {
  @Id
  int id;
  public int getId() {
     return this.id;
  }

  public void setId(int id) {
     this.id = id;
  }
  // .. the rest of the class
}

相反,如果注解应用于字段的访问器,如我们的下一个代码片段所示,那么将使用属性访问。属性访问意味着 Hibernate 会调用 mutator,而不是实际直接设置字段;这也意味着 mutator 可以在设置值时改变值,或者改变对象中其他可用的状态。选择哪一个取决于你的喜好和需要;通常实地访问就足够了。 6

为什么不总是使用字段访问呢?考虑一下美国的社会安全号码:它们实际上是三组信息——一个三位数的区号;一个群号,是两位数;还有一个的序列号。虽然这些数字没有绝对的意义,但是您可能希望将它们分隔到一个类的不同属性中,即使它作为单个元素存储在数据库中;通过属性访问,您可以使用一种方法将程序的三个元素分开,同时保持数据库结构不变。

import javax.persistence.Entity;
import javax.persistence.Id;

@Entity
public class Sample {
  int id;

  // note that we've moved the @Id annotation to the accessor
  @Id
  public int getId() {
     return this.id;
  }
  public void setId(int id) {
     this.id = id;
  }
  // .. the rest of the class
}

在这里,您可以看到注解方法的优势之一。因为注解与源代码内嵌在一起,所以可以从代码中映射的上下文中提取信息,从而可以推断出许多映射决策,而不是显式地声明,这有助于进一步减少注解的冗长性。

默认情况下,@Id注解不会创建主键生成策略, 7 ,这意味着作为代码的作者,您需要确定什么是有效的主键。您可以通过使用@GeneratedValue注解让 Hibernate 为您确定主键。这需要一对属性:strategygenerator

strategy属性必须是来自javax.persistence.GenerationType枚举的值。如果不指定生成器类型,默认为AUTOGenerationType上有四种不同类型的主键生成器,如表 6-1 所示。

表 6-1

GenerationType选项

|

战略

|

描述

| | --- | --- | | AUTO | Hibernate 根据数据库对主键生成的支持来决定使用哪种生成器类型。 | | IDENTITY | 数据库负责确定和分配下一个主键。不建议这样做,因为它对事务和批处理有影响。 | | SEQUENCE | 一些数据库支持SEQUENCE列类型。请参阅本章后面的“用@SequenceGenerator生成主键值”一节。 | | TABLE | 这种类型用主键值保存一个单独的表。请参阅本章后面的“用@TableGenerator生成主键值”一节。 |

generator 属性允许使用自定义生成机制。Hibernate 为四种策略中的每一种都提供了命名生成器,还有其他的,比如“hilo、“uuid”和“guid”如果您需要使用 Hibernate 特有的主键生成器,比如hilo,那么您的应用可能会丧失在其他 JPA 2 环境中的可移植性;也就是说,Hibernate 生成器提供了更多的灵活性和控制。

对于 Book 类,我们将使用AUTO密钥生成策略。让 Hibernate 决定使用哪种类型的生成器可以让您的代码在不同的数据库之间移植。

@Id
@GeneratedValue(strategy=GenerationType.AUTO)
int id;

用@SequenceGenerator 生成主键值

正如在@Id标签中提到的,我们可以声明主键属性是由数据库序列生成的。序列是一种数据库对象,可用作主键值的来源。它类似于标识列类型的使用,只是序列独立于任何特定的表,因此可以由多个表使用。

序列表用于多个标识符的能力有用吗?看情况。序列生成器在大量访问的情况下处于最佳状态,因为它以块为单位分配标识符,所以在分配新的键时不会发生冲突,所以在许多情况下这是一个很好的选择。然而,这并不能保证你会得到一系列可预测的标识符,因为一旦分配了一个块,这个数字块就不能再用于任何其他标识符的生成。

要声明要使用的特定序列对象及其属性,必须在带注解的字段中包含@SequenceGenerator注解。这里有一个例子:

@Id
@SequenceGenerator(name="seq1",sequenceName="HIB_SEQ")
@GeneratedValue(strategy=SEQUENCE,generator="seq1")
int id;

这里声明了一个名为seq1的序列生成注解。这指的是名为HIB_SEQ的数据库序列对象。然后名称seq1被引用为@GeneratedValue注解的generator属性。

只有序列生成器名称是必需的;其他属性将采用合理的默认值,但是作为一种良好的实践,您应该为sequenceName属性提供一个显式值。如果没有指定,要使用的sequenceName值由持久化提供者(在本例中是 Hibernate)选择。其他(可选)属性为initialValue(生成器以此编号启动)和allocationSize(一次预留的序列中 id 的数量);initialValue默认为 1,新分配的块大小默认为 50,这对大多数应用来说都很好。如果您的应用最终在几毫秒内生成超过 50 个标识符,使用更大的分配大小可能不是一个坏主意,因为 Hibernate 会在需要更多标识符时生成一个查询来获取新的块。

用@TableGenerator 生成主键值

@TableGenerator注解的使用方式与@SequenceGenerator注解非常相似,但是因为@TableGenerator操作一个标准的数据库表来获得它的主键值,而不是使用一个特定于供应商的序列对象,所以它保证了在数据库平台之间的可移植性。

为了获得最佳的可移植性和最佳的性能,您不应该指定使用表生成器,而应该使用@GeneratorValue(strategy=GeneratorType.AUTO)配置,它允许持久化提供者为正在使用的数据库选择最合适的策略。

与序列生成器一样,@TableGenerator的名称属性是必需的,其他属性是可选的,表的详细信息由持久化提供者选择:

@Id
@TableGenerator(name="tablegen",
                table="ID_TABLE",
                pkColumnName="ID",
                valueColumnName="NEXT_ID")
@GeneratedValue(strategy=TABLE,generator="tablegen")
int id;

可选属性如表 6-2 所示。

表 6-2

@TableGenerator可选属性

|

属性名

|

意义

| | --- | --- | | allocationSize | 允许根据性能调整一次留出的主键数量。默认为 50。当 Hibernate 需要分配一个主键时,它会从键表中抓取一个键“块”,并按顺序分配键,直到这个块被使用,所以它会在每次allocationSize分配时更新这个块。 | | catalog | 允许指定表所在的数据库目录。 | | indexes | 这是一个javax.persistence.Index注解列表,表示表的显式索引,这些索引不能从@Column说明符派生,通常是复合索引。 | | initialValue | 允许指定起始主键值。默认为 1。 | | pkColumnName | 允许标识表的主键列。该表可以包含为多个实体生成主键值所需的详细信息。 | | pkColumnValue | 允许标识包含主键生成信息的行的主键。 | | schema | 允许指定表所在的模式。 | | table | 包含主键值的表的名称。 | | uniqueConstraints | 允许将附加约束应用于表以生成架构。 | | valueColumnName | 允许标识包含当前实体的主键生成信息的列。 |

因为该表可用于包含各种条目的主键值,所以每个使用它的实体可能都有一个单独的行。因此,它需要自己的主键(pkColumnName),以及一个包含下一个主键值的列(pkColumnValue),用于任何从它那里获取主键的实体。

用@Id、@IdClass 或@EmbeddedId 组合主键

尽管出于各种原因,使用单列代理键是有利的,但有时您可能会被迫使用业务键。当这些包含在一个单独的列中时,您可以使用@Id而无需指定生成策略(强制用户在实体被持久化之前分配一个主键值)。但是,当主键由多个列组成时,您需要采用不同的策略将这些列组合在一起,以允许持久化引擎将键值作为单个对象进行操作。

您必须创建一个类来表示这个主键。当然,它不需要自己的主键,但是它必须是一个公共类,必须有一个默认的构造函数,必须是可序列化的,并且必须实现hashCode()equals()方法,以允许 Hibernate 代码测试主键冲突(也就是说,它们必须用主键值的适当的数据库语义来实现)。

一旦创建了主键类,使用它的三种策略如下:

  1. 将其标记为@Embeddable,并将其添加到您的实体类中,就像它是一个普通属性一样,标记为@Id

  2. 将它添加到您的实体类中,就像它是一个普通属性一样,用@EmbeddableId标记。

  3. 为实体类的所有字段添加属性,用@Id标记它们,用@IdClass标记实体类,提供主键类的类。

所有这些技术都需要使用一个id类,因为在调用 Hibernate 的持久化 API 的各个部分时,必须为 Hibernate 提供一个主键对象。例如,您可以通过调用Session对象的get()方法来检索实体的实例,该方法将表示实体主键的单个可序列化对象作为其参数。

对标记为@Embeddable的类使用@Id,如我们接下来的清单所示,是最自然的方法。无论如何,@Embeddable注解可以用于非主键可嵌入值(@Embeddable将在本章后面详细讨论)。它允许您将复合主键视为单个属性,并允许在其他表中重用@Embeddable类。

嵌入的主键类必须是可序列化的(即,它们必须实现java.io.Serializable,尽管也可以使用java.io.Externalizable8)。

首先,让我们看一下密钥本身,一个 ISBN,或国际标准书号。ISBNs 作为书籍的自然唯一标识符是有意义的,因为这就是它们的真正用途;它们在所有文学作品中都是独一无二的,因此每一种印刷和封面类型的书都有自己的 ISBN。(换句话说,不仅每本书的不同版本都有自己的 ISBN,你正在阅读的书的电子版和平装版都有自己独特的 ISBN。)

package chapter06.compoundpk;

import javax.persistence.Column;
import javax.persistence.Embeddable;
import java.io.Serializable;

@Embeddable
public class ISBN implements Serializable {
  @Column(name = "group_number")
  int group;
  int publisher;
  int title;
  int checkDigit;

  public ISBN() {
  }

  public int getGroup() {
    return group;
  }

  public void setGroup(int group) {
    this.group = group;
  }

  public int getPublisher() {
    return publisher;
  }

  public void setPublisher(int publisher) {
    this.publisher = publisher;
  }

  public int getTitle() {
    return title;
  }

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

  public int getCheckDigit() {
    return checkDigit;
  }

  public void setCheckDigit(int checkdigit) {
    this.checkDigit = checkdigit;
  }

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

    ISBN isbn = (ISBN) o;

    if (checkDigit != isbn.checkDigit) return false;
    if (group != isbn.group) return false;
    if (publisher != isbn.publisher) return false;
    if (title != isbn.title) return false;

    return true;
  }

  @Override
  public int hashCode() {
    int result = group;
    result = 31 * result + publisher;
    result = 31 * result + title;
    result = 31 * result + checkDigit;
    return result;
  }

}

Listing 6-4src/main/java/chapter06/compoundpk/ISBN.java

这里的@Embeddable注解意味着,确切地说,它应该作为一个实体嵌入到包含类中;对于ISBNgrouppublishertitlecheckDigit都作为包含类的列导出。

group字段实际上对数据库列使用了不同的名称,因为group是 SQL 中的保留字。这里,我们更喜欢我们自己到group_number的映射,而不是数据库方言可能喜欢的任何映射。

使用这个可嵌入键CPKBook——其中CPK表示“复合主键”——的类看起来可能如清单 6-5 所示。

package chapter06.compoundpk;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;

@Entity

public class CPKBook {
  @Id
  ISBN id;
  @Column
  String name;

  public CPKBook() {
  }

  public ISBN getId() {
    return id;
  }

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

  public String getName() {
    return name;
  }

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

Listing 6-5src/main/java/chapter06/compoundpk/CPKBook.java

当然,作为一个简单的例子,这个Book非常稀疏。但是假设它有一个可嵌入的标识符类——即ISBN——创建cpkbook表的 SQL 将如清单 6-6 所示。

create table CPKBook (
    checkDigit integer not null,
    group_number integer not null,
    publisher integer not null,
    title integer not null,
    name varchar(255),
    primary key (checkDigit, group_number, publisher, title)
    );

Listing 6-6The Generated DDL for CPKBook

我们的下一个例子使用了一个@EmbeddedId注解来创建与清单 6-6 中完全相同的 DDL,除了表名。嵌入式 id 的主要区别在于嵌入式密钥的可访问性和范围;在这里,我们基本上将EmbeddedISBN描述为一个EmbeddedPKBook范围中的一个类。我们这里的例子是人为设计的,因为 ISBN 有一个普遍接受的含义(当你看到“ISBN”时,你通常会想到书,而不是想知道这个术语来自哪个领域),但是如果你对关键字有一个限定的含义,你就可以使用这个。

清单 6-7 非常短,主要是因为EmbeddedISBN和我们标记为@EmbeddableISBN有着完全相同的类定义。

package chapter06.compoundpk;

import javax.persistence.Column;
import javax.persistence.EmbeddedId;
import javax.persistence.Entity;
import java.io.Serializable;

@Entity

public class EmbeddedPKBook {
  @EmbeddedId
  EmbeddedISBN id;
  @Column
  String name;

  static class EmbeddedISBN implements Serializable {
    // source matches the listing for ISBN.java
  }
}

Listing 6-7src/main/java/chapter06/compoundpk/EmbeddedPKBook.java

我们的最后一个例子使用了@IdClass,它与@EmbeddedId的例子非常相似。有了@IdClass,实体就有了匹配id 类定义的字段,这些字段在实体中都标有@Id;key 类用于像get()load()这样的Session方法,这些方法需要单个对象通过键来查找类。

这个类的 DDL 看起来与CPKBookEmbeddedPKBook完全相同,除了表名。

package chapter06.compoundpk;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.IdClass;
import java.io.Serializable;

@Entity

@IdClass(IdClassBook.EmbeddedISBN.class)
public class IdClassBook {
  @Id
  @Column(name = "group_number")
  int group;
  @Id
  int publisher;
  @Id
  int title;
  @Id
  int checkdigit;
  String name;

  public IdClassBook() {
  }

  static class EmbeddedISBN implements Serializable {
    int group;
    int publisher;
    int title;
    int checkdigit;

    public EmbeddedISBN() {
    }
    // source matches the listing for ISBN.java
  }
}

Listing 6-8src/main/java/chapter06/compoundpk/IdClassBook.java

@IdClass注解使用了EmbeddedISBN类的完全作用域名称,其简单名称与来自EmbeddedPKBookEmbeddedISBN类相匹配。这是一个正在讨论的类名作用域的例子;键类的完全限定名是不同的,即使简单名不是。

关键类字段必须与它们所应用的实体相匹配;这里不能使用不同的名称或类型。(然而,这些字段的顺序并不相关。)实体类的字段用@Id标记,这在创建复合主键时感觉有些自然。

那么,当有更多的规则适用于使用 id 类时,为什么还要使用它呢?主要是自然物体参照物。id 类的存在不会影响实体定义,因为像publisher这样的字段是包含实体的顶级属性,而不是 ISBN 的一部分。是否是你的对象模型的一个想要的属性取决于你。 9

对于哪一个是“最好的”,有什么偏好吗简短的回答是“是”,真正的回答是“否”。根据轶事经验,@Embeddable方法感觉最自然,但是每个应用和问题领域的特定需求确实有助于确定您的对象模型;很容易让键由与@IdClass的外键关系组成,并且@IdClass@EmbeddedId都允许您限制键类型的有效可见性,因为当您想要一个 ISBN 用作主键时,您有另一个东西,您可以设法调用一个 ISBN 类型,它在同一个包中有其他用途或含义。

永远做最适合你的代码的事情。

@Table@SecondaryTable映射数据库表

默认情况下,表名源自实体名。因此,给定一个带有简单的@Entity注解的类Book,表名将是“book”,根据数据库的配置进行调整。

如果实体名称由于某种原因被更改(通过在@Entity注解中提供一个不同的名称,比如@Entity("BookThing"),新名称将被用于实体名称。(查询需要使用实体名称;从用户的角度来看,表名是不相关的。)

可以进一步定制表名,并且可以通过@Table注解配置其他与数据库相关的属性。该注解允许您指定表的许多细节,这些细节将用于在数据库中持久存储实体。正如已经指出的,如果您省略注解,Hibernate 将默认使用类名作为表名,由数据库方言规范化,因此如果您想要覆盖该行为,只需要提供这个注解。

@Table注解提供了四个属性,允许您覆盖表的名称、目录和模式,并对表中的列实施惟一约束。通常,您只需提供一个替代表名,即@Table(name="ORDER_HISTORY")。如果数据库模式是从带注解的类生成的,那么唯一约束将被应用,并将补充任何特定于列的约束(参见本章后面对@Column@JoinColumn的讨论)。它们不会被强制执行。

@SecondaryTable注解提供了一种对跨几个不同数据库表持久化的实体 bean 建模的方法。 10 这里,除了为主数据库表提供一个@Table注解外,您的实体可以有一个@SecondaryTable注解,或者一个@SecondaryTables注解,依次包含零个或多个@SecondaryTable注解。@SecondaryTable注解采用与@Table注解相同的基本属性,并增加了pkJoinColumns属性。pkJoinColumns属性定义了主数据库表的连接列。它接受一组javax.persistence.PrimaryKeyJoinColumn对象。如果您省略了pkJoinColumns属性,那么将假设这些表是在同名的主键列上连接的。

当从二级表中提取实体中的属性时,必须用@Column标注进行标记,用表属性标识相应的表;否则,它将从“主”数据库表中取出。清单 6-9 展示了如何从以这种方式映射的第二个表中提取一个Customer实体的属性。

package chapter06.twotables;

import javax.persistence.*;

@Entity
@Table(
    name = "customer",
    uniqueConstraints = {@UniqueConstraint(columnNames = "name")}
)
@SecondaryTable(name = "customer_details")
public class Customer {
  @Id

  public int id;
  public String name;
  @Column(table = "customer_details")
  public String address;

  public Customer() {
  }
}

Listing 6-9src/main/java/chapter06/twotables/Customer.java

该类可以在 H2 数据库中使用以下 SQL 建模:

create table customer

   (id integer not null, name varchar(255), primary key (id));
create table customer_details
   (address varchar(255), id integer not null, primary key (id));
alter table if exists customer
   add constraint UKcrkjmjk1oj8gb6j6t5kt7gcxm unique (name);
alter table if exists customer_details
   add constraint FK4g7jhj0n6g33lh0ar8ii6c9to foreign key (id) references customer;

通过向@Table@SecondaryTableuniqueConstraints属性添加一个或多个适当的@UniqueConstraint注解,可以将主表或辅助表中的列标记为在它们的表中具有唯一值,如name所示。您也可以使用@Column属性上的唯一属性在字段级别设置唯一性。

@Basic持久化基本类型

默认情况下,POJO 中的属性和实例变量是持久的;Hibernate 会为你存储它们的值。因此,最简单的映射是针对“基本”类型的。这些包括基元、基元包装器、基元或包装器的数组、枚举以及任何实现Serializable但本身不是映射实体的类型。这些都是隐式映射的——不需要注解。默认情况下,这些字段被映射到一个单独的列,并使用渴望获取来检索它们(即,当从数据库中检索实体时,所有的基本字段和属性都被检索 11 )。此外,当字段或属性不是原语时,它可以作为一个null值存储和检索。

通过对适当的类成员应用@Basic注解,可以覆盖这个默认行为。该注解带有两个可选属性,并且本身是完全可选的。第一个属性被命名为optional,并带有一个Boolean。默认设置为true,可以设置为false来提示模式生成(或者模式使用,如果你不让 Hibernate 管理模式的话)应该创建相关的列NOT NULL12 第二个被命名为fetch并接受一个枚举成员FetchType。默认情况下为EAGER,但可以设置为LAZY以允许在访问该值时加载。

惰性加载意味着当对象被Session加载时,引用的值实际上不一定被初始化。这具有潜在的性能优势——您可以查看对象是否被持久化,而不必设置其所有属性——但这也意味着您的对象可能在任何给定时刻都没有完全初始化。(当你真正开始访问数据时,它被初始化。)这意味着对于延迟加载的数据,当您访问数据时,发起的Session必须是活动的,否则您将得到一个LazyInitializationException异常。

当您真正从数据库加载关系时,惰性初始化最有价值。如果你有一个PublishingHouse对象,已经出版了成千上万本书,你不需要仅仅因为你正在使用PublishingHouse引用就加载所有的书。因此,您可能希望书籍被延迟加载(因为它们可能有自己对Author对象的引用,并且那些作者可能有多本书,等等,如此循环往复)。

通常省略@Basic属性,使用@Column注解的nullable属性,否则@Basic注解的可选属性可能用于提供NOT NULL行为。

用@Transient 省略持久化

一些字段,如计算值,可能只在运行时使用,当实体保存到数据库中时,它们应该从对象中丢弃。JPA 规范为这些瞬态字段提供了@Transient注解。@Transient 注解没有任何属性——您只需根据实体 bean 的属性访问策略将它添加到实例变量或 getter 方法中。

@Transient注解强调了在 Hibernate 中使用注解和使用 XML 映射文档之间的一个重要区别。有了注解,Hibernate 将默认保存映射对象上的所有字段。当使用 XML 映射文档时,Hibernate 要求您明确地告诉它哪些字段将被持久化。 13

对于我们的例子,如果我们想要添加一个名为publicationDateDate字段,而不是存储在我们的 Book 类的数据库中,我们可以这样标记这个字段:

@Transient
Date publicationDate;

如果我们对我们的Book类使用属性访问策略,我们将需要在访问器上放置@Transient注解。

用@Column 映射属性和字段

@Column注解用于指定字段或属性将被映射到的列的细节。有些细节是与模式相关的,因此只有当模式是从带注解的文件生成时才适用。其他的由 Hibernate(或者 JPA 2 持久化引擎)在运行时应用和执行。它是可选的,具有一组适当的默认行为,但在覆盖默认行为或需要将对象模型放入预先存在的模式中时通常很有用。它比类似的@Basic注解更常用,表 6-3 中的属性通常被覆盖。

表 6-3

@Column属性

|

属性

|

描述

| | --- | --- | | name | 这允许显式指定列的名称——默认情况下,这将是属性的名称。但是,如果默认行为会导致 SQL 关键字被用作列名(例如,usergroup,两者都是 SQL 关键字,那么通常有必要覆盖默认行为;例如,你可以用user_name代替user。 | | length | length允许显式定义用于映射值(尤其是String值)的列的大小。列大小默认为 255,否则可能会导致截断的String数据。 | | nullable | 这控制了列的可空性。如果 schema 是 Hibernate 生成的,那么该列会被标记为NOT NULL;否则,此处的值会影响对象的验证。默认情况下,应该允许字段为null;然而,当一个字段是或者应该是强制的时,通常会覆盖它。 | | unique | 这将标记只包含唯一值的字段。这默认为 false,但通常会为一个可能不是主键但如果重复仍会导致问题的值设置(如 username)。如果 Hibernate 不管理模式,这几乎没有影响。 | | table | 当所属实体已经跨一个或多个辅助表映射时,使用该属性。默认情况下,假设该值来自主表,但是在这里可以替换其中一个辅助表的名称(参见本章前面的@SecondaryTable注解示例)。 | | insertable | 这个值控制 Hibernate 是否会为这个字段创建值。它默认为 true,但是如果设置为 false,Hibernate 生成的 insert 语句将省略注解字段(例如,Hibernate 最初不会保存它,但是可能会更新它;参见下面的updatable属性。) | | updatable | 这默认为 true,但是如果设置为 false,Hibernate 生成的 update 语句将省略注解字段(也就是说,一旦它被持久化,就不会被更改)。 | | columnDefinition | 该值可以设置为在数据库中生成列时使用的适当的 DDL 片段。这只能在从带注解的实体生成模式的过程中使用,如果可能的话应该避免,因为这可能会降低应用在数据库方言之间的可移植性。 | | precision | precision允许为模式生成指定十进制数字列的精度,并且在保留非十进制值时将被忽略。给定的值代表数字中的位数(通常要求最小长度为n+1,其中n是刻度,见下表)。 | | scale | 这允许为模式生成指定十进制数字列的小数位数,并且在保留非十进制值的情况下将被忽略。给定的值代表小数点后的位数。 |

下面是一个(简短的)例子,说明如何将这些属性应用到一个Book类的title字段:

@Column(name="working_title",length=200,nullable=false)
String title;

同样,对于一个表示十进制数的类,您可能会得到如下结果:

@Column(scale=2,precision=5,nullable=false)
double royalty;

如果数据库支持十进制值的微调精度,这将把类似于10.2385的数字作为10.24保存。这里的推论相当明显:您的数据库可能不支持这个特性。例如,即使设置了precisionscale,H2 也会为royalty存储一个全浮点值。

建模实体关系

自然,注解也允许您对实体之间的关联进行建模。JPA 2 支持一对一、一对多、多对一和多对多的关联。每一个都有相应的注解。

我们在第五章的表格中讨论了建立这些映射的各种方法。在这一节中,我们将展示如何使用注解来请求各种映射。

映射嵌入式(组件)一对一关联

当一个实体的所有字段都与另一个实体维护在同一个表中时,被包含的实体在 Hibernate 中被称为组件。JPA 标准将这种实体称为嵌入式

这也适用于 id 类,并且使用的命名非常相似。

@Embedded@Embeddable属性用于管理这种关系。在本章的主键示例中,我们以这种方式将一个ISBN类与一个Book类相关联。

ISBN类标有@Embeddable注解。一个可嵌入的实体必须完全由基本字段和属性组成。一个可嵌入的实体只能使用@Basic@Column@Lob@Temporal@Enumerated注解。它不能用@Id注解维护自己的主键,因为它的主键是封闭实体的主键。

@Embeddable注解本身纯粹是一个标记注解,它没有附加属性,如下所示。通常,可嵌入实体的字段和属性不需要进一步的标记。

@Embeddable
public class AuthorAddress {
...
}

然后,封闭实体在实体中标记适当的字段或 getters,利用带有@Embedded注解的可嵌入类,如下所示:

@Embedded
AuthorAddress address;

@Embedded注解从嵌入类型中提取它的列信息,但是允许用@AttributeOverride@AttributeOverrides注解覆盖特定的一列或多列(如果多个列被覆盖,后者包含前者的一个数组)。例如,这里我们看到如何用名为ADDRNATION的列覆盖默认的列名AuthorAddress的 address 和 country 属性,这两个名称肯定是由稍有恶意的数据库分析师选择的:

@Embedded
@AttributeOverrides({
    @AttributeOverride(name="address",column=@Column(name="ADDR")),
    @AttributeOverride(name="country",column=@Column(name="NATION"))
})
AuthorAddress address;

Hibernate 和 JPA 标准都不支持跨多个表映射一个嵌入对象。在实践中,如果您希望您的嵌入式实体具有这种持久化,您通常会更好地让它成为一个一级实体(即,非嵌入式的),拥有自己的@Entity标记和@Id注解,然后通过传统的一对一关联对其进行映射,这将在下一节中解释。 14

映射传统的一对一关联

如果一个实体不是另一个实体的组件(即嵌入到另一个实体中),那么在两个实体之间映射一对一的关联本质上没有任何问题。然而,这种关系往往有些可疑。在使用@OneToOne注解之前,您应该考虑使用前面描述的嵌入技术。

那你为什么想要一对一的联系呢?好吧,考虑这样一种情况,你有密切相关的实体,但仍然保留被独立访问的能力:一个Home,可能有一个地址,一个Homeowner,实体的名字在那个Home的契约上。

您可以通过一对一的关联建立双向关系。一方需要拥有关系,并负责用另一方的外键更新连接列。非拥有方需要使用 mappedBy 属性来指示拥有该关系的实体。

假设您决心以这种方式声明关联(可能是因为您期望在可预见的将来将其转换为一对多或多对一的关系),那么应用注解就非常简单——所有的属性都是可选的。下面是声明这种关系的简单方式:

@OneToOne
Address address;

@OneToOne注解允许指定可选属性(表 6-4 )。

表 6-4

OneToOne标注属性

|

属性

|

描述

| | --- | --- | | targetEntity | 这可以设置为存储关联的实体的类。如果未设置,将从字段类型或属性 getter 的返回类型中推断出适当的类型。 | | cascade | 这个值可以设置为javax.persistence.CascadeType枚举的任何成员。默认设置为无。有关这些值的讨论,请参见“级联操作”一节。 | | fetch | 这可以设置为FetchTypeEAGERLAZY成员。(默认为EAGER。) | | optional | 这表示正在映射的值是否可以是null。 | | orphanRemoval | 此属性表明,如果被映射的值被删除,此实体也将被删除。 | | mappedBy | 该值指示双向一对一关系由命名实体拥有。 15 所属实体包含下属实体的主键。 |

映射多对一或一对多的关联

多对一关联和一对多关联分别从拥有实体和从属实体的角度来看是相同的关联。

维护两个实体之间多对一关系的最简单方法是,将一对多关系中“一”端实体的外键作为“多”实体表中的一列进行管理。

@OneToMany注解可以应用于代表关联的映射“多”端的集合或数组的字段或属性值:

@OneToMany(cascade = ALL,mappedBy = "publisher")
Set<Book> books;

这种关系的多对一端用类似于一对多端的术语来表示,如下所示:

@ManyToOne
@JoinColumn(name = "publisher_id")
Publisher publisher;

@ManyToOne注解采用了一组与@OneToMany相似的属性。表 6-5 中的列表描述了这些属性,这些属性都是可选的。

表 6-5

@ManyToOne属性

|

属性

|

描述

| | --- | --- | | cascade | 这表示对关联操作的适当级联策略;默认为无。 | | fetch | 此属性指示要使用的提取策略;默认为LAZY。 | | optional | 这指示该值是否可以为空;默认为true。 | | targetEntity | 该值指示存储主键的实体-这通常是从字段或属性的类型(在前面的示例中为 Publisher)中推断出来的。 |

我们还提供了可选的@JoinColumn属性来命名关联所需的外键列,而不是默认的(publisher)——这不是必需的,但它说明了注解的用法。(如果没有指定,Hibernate 将从“所属类型”派生一个外键列名。)

当要形成单向的一对多关联时,可以使用链接表来表达这种关系。这是通过添加@JoinTable注解实现的,如下图: 16

@OneToMany(cascade = ALL)
@JoinTable
Set<Book> books;

@JoinTable注解提供了允许控制链接表各个方面的属性。这些属性如表 6-6 所示。

表 6-6

@JoinTable属性

|

属性

|

描述

| | --- | --- | | name | 这是用于表示关联的连接表的名称。 | | catalog | 这是包含连接表的目录的名称。 | | schema | 这是包含连接表的模式的名称。 | | joinColumns | 这个引用是一个由@JoinColumn属性组成的数组,代表关联“一”端实体的主键。如果“一”端有一个复合主键,您将使用多个值。 | | inverseJoinColumns | 这是一个由@JoinColumn属性组成的数组,代表关联“多”端实体的主键。 |

在这里,我们看到了@JoinTable 注解的一个非常典型的应用,它将连接表的名称及其外键指定到关联的实体中:

@OneToMany(cascade = ALL)
@JoinTable(
  name="PublishedBooks",
  joinColumns = { @JoinColumn( name = "publisher_id") },
  inverseJoinColumns = @JoinColumn( name = "book_id")
)

Set<Book> books;

映射多对多关联

当多对多关联不涉及连接关系双方的一级实体时,必须使用链接表来维护关系。这可以自动生成,也可以通过与本章前面的“映射多对一或一对多关联”一节中描述的链接表相同的方式来建立详细信息。

合适的注解自然是@ManyToMany,并采用表 6-7 中所示的属性。

表 6-7

@ManyToMany属性

|

属性

|

描述

| | --- | --- | | mappedBy | 这是指拥有关系的字段,只有在关联是双向的情况下才需要。如果一个实体提供了这个属性,那么关联的另一端就是这个关联的所有者,这个属性必须命名这个实体的一个字段或者属性。 | | targetEntity | 这是作为关联目标的实体类。同样,这可以从泛型或数组声明中推断出来,只有在这种推断不可能时才需要指定。(如果 Hibernate 不能完全推断出模式,那么在模式生成时会出现错误。) | | cascade | 这表示关联的级联行为,默认为无。 | | fetch | 这表示关联的获取行为,默认为 LAZY。 |

这个例子维护了Book实体和Author实体之间的多对多关联。Book 实体拥有关联,因此它的getAuthors()方法必须用适当的@ManyToMany属性标记,如下所示:

@ManyToMany(cascade = ALL)
Set<Author> authors;

这里的Author实体是由即Book实体管理的*。链接表不是显式管理的,因此,如下面的代码片段所示,我们用一个@ManyToMany注解对其进行标记,并指出外键由关联的Book实体的作者属性管理:*

@ManyToMany(mappedBy = "authors")
Set<Book> books;

或者,我们可以指定完整的链接表:

@ManyToMany(cascade = ALL)
@JoinTable(
   name="Books_to_Author",
   joinColumns={@JoinColumn(name="book_ident")},
   inverseJoinColumns={@JoinColumn(name="author_ident")}
)
Set<Author> authors;

级联操作

当建立两个实体之间的关联时(比如人和宠物之间的一对一关联,或者客户和订单之间的一对多关联),通常希望对一个实体的某些持久化操作也应用到它所链接的实体。以下面的代码为例:

Human dave = new Human("dave");
Pet cat = new PetCat("Tibbles");
dave.setPet(cat);
session.save(dave);

在最后一行,我们可能想要保存与人类对象相关联的Pet对象。在一对一的关系中,我们通常期望对拥有实体的所有操作通过——也就是说,级联到——依赖实体。在其他关联中,这是不正确的,即使在一对一的关系中,我们也可能有特殊的原因想要免除依赖实体的删除操作(可能是出于审计的原因)。

因此,我们能够使用cascade属性指定应该通过关联级联到另一个实体的操作类型,该属性接受一个由CascadeType枚举成员组成的数组。这些成员与用于 EJB 3 持久化的EntityManager类的关键方法的名称相对应,并与实体上的操作有如下粗略的对应关系:

  • ALL要求将所有操作级联到相关实体。这与包含MERGEPERSISTREFRESHDETACHREMOVE相同。

  • MERGE级联更新数据库中实体的状态(即UPDATE …)。

  • PERSIST级联数据库中实体状态的初始存储(即INSERT …)。

  • REFRESH从数据库级联更新实体的状态(即SELECT …)。

  • 从托管持久化上下文中级联删除实体。

  • REMOVE从数据库中级联删除实体(即DELETE …)。

  • 如果未指定级联类型,则不会通过关联级联任何操作。

根据这些选项,发布者与其地址之间关系的适当注解如下:

@OneToOne(cascade=CascadeType.ALL)
Address address;

集合排序

使用@OrderColumn注解来维护集合的顺序,可以在 Hibernate 或 JPA 2 中持久化有序集合。您还可以通过@OrderBy注解在检索时对集合进行排序。例如,如果您要检索一个按书籍名称升序排序的列表,您可以注解一个合适的方法。

以下代码片段指定了有序集合的检索顺序:

@OneToMany(cascade = ALL, mappedBy = "publisher")
@OrderBy("name ASC")
List<Book> books;

为什么books不是一个Set,既然你想在一个给定的收藏中只看一次给定的书?这是因为在 Java 中,Set是无序且唯一的,而List是有序的(且不是唯一的)。我们依赖于数据库的唯一性(通过假设,因为我们不能从这里的数据模型中判断),但是我们肯定想要有序的结果;Java 中当然有有序的Set类型,但是如果你想访问一个集合中的第五本书,比如说,你需要一个List。您选择哪种类型在很大程度上取决于您如何使用数据。

@OrderBy注解的值是作为排序依据的字段名的有序列表,每个字段名可选地附加 ASC(升序,如前面的代码所示)或 DESC(降序)。如果 ASC 或 DESC 都没有附加到其中一个字段名称,则顺序将默认为升序。@OrderBy可应用于任何集值关联。

遗产

JPA 2 标准和 Hibernate 都支持三种将继承层次映射到数据库的方法。这些措施如下:

  • 单个表(SINGLE_TABLE):每个类层次结构一个表

  • Joined ( JOINED):每个子类一个表(包括接口和抽象类)

  • 每个类一个表(TABLE_PER_CLASS):每个具体的类实现一个表

通过继承相关的持久实体必须用@Inheritance 注解来标记。这需要一个策略属性,该属性被设置为对应于SINGLE_TABLEJOINEDTABLE_PER_CLASS的三个javax.persistence.InheritanceType枚举值之一。

单一表格

单表方法为超类及其所有子类型管理一个数据库表。超类的每个映射字段或属性以及派生类型的每个不同字段或属性都有列。按照这种策略,当层次结构中的任何字段或属性名称发生冲突时,您需要确保适当地重命名列。

package chapter06.single;

import javax.persistence.Entity;

@Entity(name="SingleCBook")
public class ComputerBook extends Book {
  String primaryLanguage;
}

Listing 6-11A Derived Entity in a SINGLE_TABLE Inheritance Tree

package chapter06.single;

import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Inheritance;
import javax.persistence.InheritanceType;

@Entity(name="SingleBook")
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
public class Book {
  // contents common to all Books go here
  @Id
  Long bookId;
  String title;
  // imagine many more
}

Listing 6-10The Root of a SINGLE_TABLE Inheritance Tree

这个结构将创建一个名为SingleBook的表,包含以下字段:bookIdtitleDTYPEprimaryLanguage,而primaryLanguage留在NULL处,用于存放不可分配给ComputerBook的书籍。这是 H2 数据库的样子:

create table SingleBook (
   DTYPE varchar(31) not null,
   bookId bigint not null,
   title varchar(255),
   primaryLanguage varchar(255),
   primary key (bookId)
   );

连接表

整体单表方法的一种替代方法是类似的连接表方法。这里使用了一个鉴别器列,但是各种派生类型的字段存储在不同的表中。(换句话说,你得到一个具有公共属性的“主表”,但是属于子类的属性得到它们自己的表。)除了不同的策略,这个继承类型以相同的方式指定(如清单 6-12 所示)。

package chapter06.joined;

import javax.persistence.Entity;

@Entity(name="JoinedCBook")
public class ComputerBook extends Book{
  String primaryLanguage;
}

Listing 6-13A Leaf on a JOINED Inheritance Tree

package chapter06.joined;

import javax.persistence.*;

@Entity(name="JoinedBook")
@Inheritance(strategy = InheritanceType.JOINED)
public class Book {
  // contents common to all Books go here
  @Id
  Long bookId;
  String title;
  // imagine many more
}

Listing 6-12The Root of a JOINED Inheritance Tree

使用 H2,可以通过下面的 SQL 看到这种结构:

create table JoinedBook
   (bookId bigint not null, title varchar(255), primary key (bookId));
create table JoinedCBook
   (primaryLanguage varchar(255), bookId bigint not null, primary key (bookId));
alter table if exists JoinedCBook
   add constraint FK62rdg2vgeqlpviherbmj5b1su foreign key (bookId) references JoinedBook;

在这种情况下,如果我们假设ComputerBook看起来一样,我们有两个表:JoinedBookJoinedCBookJoinedBook表有bookIdtitle,而JoinedCBook表会有bookId(这就是它如何知道数据是如何关联的)和primaryLanguage。Hibernate 将查询这两个表,以确定检索时合适的实体类型。

每类表格

最后,还有每类一个表的方法,在这种方法中,继承层次结构中每种类型的所有字段都存储在不同的表中。由于实体和它的表之间的紧密对应关系,@DiscriminatorColumn注解不适用于这种继承策略。清单 6-14 展示了如何以这种方式映射我们的 Book 类。

package chapter06.perclass;

import javax.persistence.Entity;

@Entity(name="PerClassCBook")
public class ComputerBook extends Book {
  String primaryLanguage;
}

Listing 6-15A Leaf on a TABLE_PER_CLASS Inheritance Tree

package chapter06.perclass;

import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Inheritance;
import javax.persistence.InheritanceType;

@Entity(name="PerClassBook")
@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
public class Book {
  // contents common to all Books go here
  @Id
  Long bookId;
  String title;
  // imagine many more
}

Listing 6-14The Root of a TABLE_PER_CLASS Inheritance Tree

这里我们(再次)有两个表,PerClassBookPerClassCBook,但是PerClassBookbookIdtitle,而PerClassCBook现在有三个列:bookIdtitle(就像PerClassBook)和primaryLanguage。在每个类一个表的策略中,每个实体都完全存储在自己的表中,与层次结构中的其他表没有数据库强制关系。

下面是为 H2 生成的 SQL:

create table PerClassBook (
   bookId bigint not null,
   title varchar(255),
   primary key (bookId)
   );
create table PerClassCBook (
   bookId bigint not null,
   title varchar(255),
   primaryLanguage varchar(255),
   primary key (bookId)
   );

建模继承时在继承类型之间选择

这些不同的继承类型各有利弊。当创建模拟类层次结构的数据库模式时,必须权衡性能和数据库可维护性,以决定使用哪种继承类型。

当使用连接表方法时,维护数据库是最容易的。如果在类层次结构中的任何类中添加或删除字段,只需修改一个数据库表来反映这些变化。此外,向类层次结构添加新类只需要添加一个新表,消除了向大型数据集添加数据库列的性能问题。使用每类一个表的方法,对父类中列的更改要求在所有子表中进行列更改。单表方法可能会很混乱,导致表中的许多列并不是在每一行中都使用,以及一个快速水平增长的表。

单表方法的读取性能最好。对于层次结构中的任何类,select 查询将只从一个表中读取,不需要连接,只需要一个表。如果您只处理类层次结构中的叶节点(也就是说,如果您专门处理ComputerBook实体,而不是Book类型),则每类表类型具有很好的性能。任何与父类相关的查询都需要连接多个表才能得到结果。连接表方法还需要连接任何选择查询,因此这会影响性能。连接的数量与类层次结构的大小有关——大而深的类层次结构可能不适合连接表方法。

我们建议使用连接表方法,除非由于数据集的大小和类层次结构的深度而导致性能成为问题,但是这个决定完全基于您正在处理的数据的类型和数量。测量。

其他 JPA 2 持久化注解

虽然我们现在已经介绍了大多数核心 JPA 2 持久化注解,但是还有一些其他注解您会经常遇到。我们将在下面的章节中顺便介绍其中的一些。

时态数据

具有java.util.Datejava.util.Calendar类型的实体的字段或属性表示临时数据。默认情况下,这些将存储在数据类型为TIMESTAMP的列中,但是这个默认行为可以用@Temporal注解覆盖。

该注解接受来自javax.persistence.TemporalType枚举的单值属性。这提供了三个可能的值:DATETIMETIMESTAMP。这些分别对应于java.sql.Datejava.sql.Timejava.sql.Timestamp。表列在模式生成时被赋予适当的数据类型。下一个清单展示了一个将java.util.Date属性映射为TIME类型的示例——java.sql.Datejava.sql.Time类都是从java.util.Date类派生而来的,因此,令人困惑的是,两者都能够表示数据库中的日期和时间!(java.sql.Date类只公开与日期相关的信息,所以没有时间,而java.sql.Time类只表示时间,没有日期,而Timestamp更类似于旧的java.util.Date。)

@Temporal(TemporalType.TIME)
java.util.Date startingTime;

敏锐的读者还会想到java.time包,它表示时态数据,尽管比相当粗糙的java.util.Date类要详细得多。java.time类实际上天生就被支持为时态类型,不需要@Temporal注解来指定字段的性质;默认情况下,Hibernate 会适当地映射它们,所以您需要做的就是允许它们被持久化。

其中,LocalDate映射到一个 SQL DATE类型,LocalTimeOffsetTime映射到TIME类型,InstantLocalDateTimeOffsetDateTimeZonedDateTime都映射到TIMESTAMP

如果可以的话,对于日期和时间,使用java.time类而不是java.util类。它们比旧的java.util.Datejava.util.Calendar类定义和规定得更好,因此涉及的关于实际值是什么以及如何将其转换成其他类型的假设也少得多。

元素集合

除了使用一对多映射来映射集合之外,JPA 2 还引入了一个用于映射基本或可嵌入类集合的@ElementCollection注解,比如ListSet。您可以使用@ElementCollection注解来简化您的映射。清单 6-16 展示了一个使用@ElementCollection注解来映射字符串对象集合java.util.List的例子。

package chapter06.embedded;

import javax.persistence.ElementCollection;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import java.util.List;

@Entity
public class User {
  @Id
  @GeneratedValue
  Long id;
  String name;
  // this is... not wise from a security perspective
  String password;
  @ElementCollection
  List<String> passwordHints;
}

Listing 6-16src/main/java/chapter06/embedded/User.java

这个类实际上创建了两个表。对于 H2,DDL 看起来是这样的:

create table User (
   id bigint not null,
   name varchar(255),
   password varchar(255),
   primary key (id)
   );
create table User_passwordHints (
   User_id bigint not null,
   passwordHints varchar(255)
   );

User正在从前面的 DDL 中的一个hibernate_sequence表中获取它的主键。

不要使用 User 这样的类没有太多的谨慎!这里使用password意味着实际密码以明文形式存储在表中。在现实生活中,这基本上是邀请您的用户认证被黑客攻击。你需要使用散列密码,或者编码密码,或者证书,或者……除了明文以外的任何密码。在那个类定义中没有说代码不散列密码,但是也没有说散列密码。

您也可以嵌入更复杂的类型,在类型上使用@Embeddable注解。将复杂类型映射为实际实体比映射为嵌入类型可能更有用,但这是可行的。(如果该类型足够复杂,可以被专门查询,那么它可能希望是一个实体,而如果您只关心拥有实体的上下文中的数据,那么嵌入类型可能是合适的。)下面是一个EBook(用于“嵌入的书”,以防止在本章中重复使用“书”40 次)和一个嵌入的Author类型的例子。

@ElementCollection注解上有两个属性:targetClassfetchtargetClass属性告诉 Hibernate 哪个类存储在集合中。如果在集合中使用泛型,就不需要指定 targetClass,因为 Hibernate 会推断出正确的类。17fetch属性取一个枚举成员,FetchType。默认情况下为EAGER,但可以设置为LAZY以允许在访问该值时加载。

package chapter06.embedded;

import javax.persistence.ElementCollection;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import java.util.Set;

@Entity
public class EBook {
  @Id
  @GeneratedValue
  Long id;
  String name;
  @ElementCollection
  Set<Author> authors;
}

Listing 6-17src/main/java/chapter06/embedded/EBook.java

package chapter06.embedded;

import javax.persistence.Embeddable;
import java.time.LocalDate;

@Embeddable
public class Author {
  String name;
  LocalDate dateOfBirth;
}

Listing 6-18src/main/java/chapter06/embedded/Author.java

生成的 SQL(同样针对 H2)如下所示:

create table EBook (
   id bigint not null,
   name varchar(255),
   primary key (id)
);
create table EBook_authors (
   EBook_id bigint not null,
   dateOfBirth date,
   name varchar(255)
);

注意一个给定的作者是如何在一本给定的书中出现多次的(从数据库的角度来看,对给定的Author的任何属性都没有限制18)——同样,@ElementCollection并不是要取代@OneToMany或其同类,如果数据有任何复杂性,考虑使用关系注解而不是嵌入注解是完全合理的。

大型物体

通过应用@Lob 注解,可以将持久化属性或字段标记为数据库支持的大型对象类型。

该注解不带任何属性,但是要使用的底层大型对象类型将从字段或参数的类型中推断出来。String -基于字符的类型将存储在一个合适的基于字符的类型中。所有其他对象,如byte[],将存储在一个 BLOB 中。在这里,我们看到一个String——某种标题19——映射到一个大对象列类型:

@Lob
String title; // a very, very long title indeed

@Lob注解可与@Basic@ElementCollection注解结合使用。如何在特定数据库中引用该类型在很大程度上取决于所使用的数据库方言。

映射超类

当层次结构的根本身不是一个持久的实体,但从它派生的各种类是时,继承的一个特例就发生了。这样的类可以是抽象的,也可以是具体的。@MappedSuperclass 注解允许您利用这种情况。

标有@MappedSuperclass的类不是一个实体,也不可查询(它不能被传递给期望在SessionEntityManager对象中有实体的方法)。它不能是关联的目标。

超类的列的映射信息将存储在与派生类的细节相同的表中(这样,注解类似于使用带有SINGLE_TABLE策略的@Inheritance注解)。

在其他方面,超类可以被映射为一个普通的实体,但是映射将只应用于派生类(因为超类本身在数据库中没有相关的表)。当一个派生类需要偏离超类的行为时,可以使用@AttributeOverride注解(就像使用一个可嵌入实体一样)。

例如,如果BookComputerBook的超类,但是Book对象本身从未被直接持久化,那么Book可以被标记为@MappedSuperclass,如清单 6-19 所示。

package chapter06.mapped;

import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.MappedSuperclass;

@MappedSuperclass
public class Book {
    @Id
    @GeneratedValue
    Integer id;
    String name;

    public Book() {
    }

    public Integer getId() {
        return id;
    }

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

    public String getName() {
        return name;
    }

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

Listing 6-19src/main/java/chapter06/mapped/Book.java

值得注意的是,超类也可以标记为abstract;它没有具体。超类还需要为子类指定标识符。

下面是从一个Book(列表 6-20 )派生出来的东西。

package chapter06.mapped;

import javax.persistence.Entity;

@Entity
public class ComputerBook extends Book {
    String language;

    public ComputerBook() {
    }

    public String getLanguage() {
        return language;
    }

    public void setLanguage(String language) {
        this.language = language;
    }
}

Listing 6-20src/main/java/chapter06/mapped/ComputerBook.java

使用这种结构,可以创建一个表:

create table ComputerBook (
   id integer not null,
   name varchar(255),
   language varchar(255),
   primary key (id)
   );

Book派生的ComputerBook实体的字段将被存储在ComputerBook实体类的表中。

映射的超类不会而不是将它们的子类标记为实体。直接从Book派生的类,但是没有映射为实体,比如一个假想的MarketingBook类,是不可持久的。仅在这一方面,映射超类方法的行为不同于传统的使用SINGLE_TABLE策略的@Inheritance方法。

使用@OrderColumn 对集合进行排序

虽然@OrderBy允许在从数据库中检索到数据后对其进行排序,但是 JPA 2 还提供了一个注解,允许在数据库中维护适当的集合类型(例如List)的排序,而不是在检索时对进行排序;它通过维护一个 order 列来表示该订单。这里有一个例子:

@OneToMany
@OrderColumn(
   name="employeeNumber"
)
List<Employee> employees;

这里,我们声明一个employeeNumber列将维护一个值,从零开始,随着每个条目被添加到列表中而递增。默认起始值可以被基本属性覆盖。默认情况下,该列可以包含 null(无序)值。可空性可以通过将nullable属性设置为 false 来覆盖。默认情况下,当从注解生成模式时,列被假定为整数类型;然而,这可以通过提供一个指定不同列定义字符串的columnDefinition属性来覆盖。(还有更多@OrderColumn的选项,但是不经常用。)

从语义上来说,在这里使用一个Set没有什么意义;一个Set天生无序。

命名查询(HQL 或 JPQL)

@NamedQuery@NamedQueries允许一个或多个 Hibernate 查询语言或 Java 持久化查询语言(JPQL)查询与一个实体相关联。所需的属性如下:

  • name是检索查询的名称。

  • query是与名称相关联的 JPQL(或 HQL)查询。

清单 6-21 显示了一个将命名查询与Author实体相关联的例子。该查询将按名称检索Author实体,因此很自然地将其与该实体相关联;然而,并没有实际的需求要求一个命名的查询以这种方式与它所关注的实体相关联。(Hibernate 构建了一个命名查询的列表,它们返回的是*,而不是与声明它们的位置相关联的*。)

@Entity
@NamedQuery(
        name="findAuthorsByName",
        query="from Author where name = :author"
)
public class Author {
...
}

Listing 6-21A JPQL Named Query Annotation

还有一个hints属性,带有一个QueryHint注解名称/值对,允许应用缓存模式、超时值和各种其他特定于平台的调整(这也可以用于注解查询生成的 SQL)。

您不需要直接将查询与声明它所针对的实体相关联,但是这样做是正常的。如果一个查询与任何实体声明都没有自然的关联,那么可以在包级别进行@NamedQuery注解。 20

没有放置包级注解的自然位置,所以 Java 注解允许一个名为package-info.java的特定文件来包含它们。 21 清单 6-22 给出了这样一个例子。

@javax.annotations.NamedQuery(
   name="findBooksByAuthor",
   query="from Book b where b.author.name = :author"
)
package chapter06.annotations;

Listing 6-22A package-info.java File

Hibernate 的Session允许直接访问命名查询,如清单 6-23 所示。

Query query = session.getNamedQuery("findBooksByAuthor", Book.class);
query.setParameter("author", "Dave");
List<Book> booksByDave = query.list();
System.out.println("There is/are " + booksByDave.size()

   + " books by Dave in the catalog");

Listing 6-23Invoking a Named Query via the Session

如果你有多个@NamedQuery注解应用于一个实体,它们可以作为@NamedQueries注解的值的数组提供,不管在哪里声明了@NamedQueries

命名本机查询(SQL)

Hibernate 还允许使用数据库的本地查询语言(通常是 SQL 的一种方言)来代替 HQL 或 JPQL。如果您使用特定于数据库的特性,您可能会失去可移植性,但是只要您选择合理的通用 SQL,您应该没问题。@NamedNativeQuery注解的声明方式几乎与@NamedQuery注解完全相同。以下代码块显示了一个简单的命名本机查询声明示例:

@NamedNativeQuery(
  name="nativeFindAuthorNames",
  query="select name from author"
)

所有查询都以相同的方式使用;唯一的区别是它们是如何被访问的,是通过Session.getNamedQuery()Session.createQuery()还是Session.createSQLQuery();可以通过Query.list()List的形式检索结果,或者通过Query.scroll()访问可滚动的结果集,Query.iterate()提供了Iterator(惊喜!),如果Query只返回一个对象,就可以使用Query.uniqueResult()

多个@NamedNativeQuery注解可以与@NamedNativeQueries注解组合在一起。

配置带注解的类

一旦有了带注解的类,就需要将该类提供给应用的 Hibernate 配置,就像它是一个 XML 映射一样。有了注解,您可以使用通过类路径访问的hibernate.cfg.xml XML 配置文档中的声明性配置,或者以编程方式将带注解的类添加到 Hibernate 的org.hibernate.cfg.AnnotationConfiguration对象中。您的应用可以在同一配置中同时使用带注解的实体和 XML 映射的实体。

为了提供一个声明性的映射,我们使用一个普通的hibernate.cfg.xml XML 配置文件,并使用 mapping 元素将带注解的类添加到映射中(参见清单 6-24 )。请注意,我们已经将带注解的类的名称指定为映射。该文件位于相对于项目根目录的src/main/resourcessrc/test/resources中;这种情况下是chapter06/src/test/resources/hibernate.cfg.xml

<?xml version="1.0"?>
<!DOCTYPE hibernate-configuration PUBLIC
    "-//Hibernate/Hibernate Configuration DTD 3.0//EN"
    "http://www.hibernate.org/dtd/hibernate-configuration-3.0.dtd">
<hibernate-configuration>
  <session-factory>

    <!--  Database connection settings  -->
    <property name="connection.driver_class">org.h2.Driver</property>
    <property name="connection.url">jdbc:h2:./db6</property>
    <property name="connection.username">sa</property>
    <property name="connection.password"/>
    <property name="dialect">org.hibernate.dialect.H2Dialect</property>
    <!-- set up c3p0 for use -->
    <property name="c3p0.max_size">10</property>
    <!--  Echo all executed SQL to stdout  -->
    <property name="show_sql">true</property>
    <!--  Drop and re-create the database schema on startup  -->
    <property name="hbm2ddl.auto">create</property>

    <mapping class="chapter06.primarykey.after.Book"/>
    <mapping class="chapter06.compoundpk.CPKBook"/>
    <mapping class="chapter06.compoundpk.EmbeddedPKBook"/>
    <mapping class="chapter06.compoundpk.IdClassBook"/>
    <mapping class="chapter06.twotables.Customer"/>

    <mapping class="chapter06.mapped.ComputerBook"/>

    <mapping class="chapter06.naturalid.Employee"/>
    <mapping class="chapter06.naturalid.SimpleNaturalIdEmployee"/>

    <mapping class="chapter06.embedded.User"/>
    <mapping class="chapter06.embedded.EBook"/>
    <mapping class="chapter06.embedded.Author"/>

    <mapping class="chapter06.single.Book"/>

    <mapping class="chapter06.single.ComputerBook"/>

    <mapping class="chapter06.joined.Book"/>
    <mapping class="chapter06.joined.ComputerBook"/>

    <mapping class="chapter06.perclass.Book"/>
    <mapping class="chapter06.perclass.ComputerBook"/>
  </session-factory>
</hibernate-configuration>

Listing 6-24A Hibernate XML Configuration File

您还可以通过编程方式将带注解的类添加到 Hibernate 配置中。注解工具集附带了一个 org . hibernate . CFG . annotation Configuration 对象,该对象扩展了基本 Hibernate 配置对象以添加映射。AnnotationConfiguration 上用于将带注解的类添加到配置中的方法如下:

addAnnotatedClass(Class persistentClass) throws MappingException
addAnnotatedClasses(List<Class> classes)
addPackage(String packageName) throws MappingException

使用这些方法,您可以添加一个带注解的类、一列带注解的类或者一个完整的带注解的类包(按名称)。与 Hibernate XML 配置文件一样,带注解的实体可以与 XML 映射的实体进行互操作。 22

特定于 Hibernate 的持久化注解

Hibernate 有各种注解,它们扩展了标准的持久化注解。它们可能非常有用,但是你应该记住,它们的使用会限制你的应用进入休眠状态;这不会影响我们到目前为止编写的任何代码,因为大部分代码已经使用了特定于 Hibernate 的类。

提示:可移植性的重要性怎么强调都不为过——大多数定制的应用从未部署到他们最初开发的环境之外的环境中。作为一个成熟的产品,除了基本的 JPA 2 规范之外,Hibernate 还提供了许多特性。您不应该浪费太多时间去尝试实现一个比这些专有特性更好的可移植解决方案,除非您对可移植性有明确的要求。如果您需要这些功能,请使用它们。

@不可变

@org.hibernate.annotations.Immutable注解将实体标记为不可变的。这对于您的实体表示引用数据的情况很有用——比如状态、性别或其他很少变化的数据的列表。

因为像州(或国家)这样的东西往往很少更改,所以通常有人通过 SQL 或管理应用手动更新数据。Hibernate 可以主动缓存这些数据,这一点需要考虑在内;如果引用数据发生变化,您需要确保使用它的应用得到通知或以某种方式重新启动。

注解告诉 Hibernate 的是,对不可变实体的任何更新都不应该传递给数据库。它是一个“安全”的物体;一个人可能不应该经常更新它,即使只是为了避免混乱。

@Immutable可以放在一个收藏里;在这种情况下,对集合的更改(添加或删除)将导致抛出一个HibernateException

自然身份证

本章的第一部分花了很多页讨论主键,包括生成的值。生成的值被称为“人工主键”,强烈建议将 23 作为给定行的一种简写引用。

然而,除了人工或复合主键之外,还有“自然 ID”的概念,它提供了引用实体的另一种便捷方式。

一个例子可能是美国的社会保险号或税务识别号。一个实体(一个人或一个公司)可能有一个由 Hibernate 生成的人工主键,但是它也可能有一个惟一的税务标识符。这可以用@Column(unique=true, nullable=false, updatable=false)来注解,这将创建一个唯一的、不可变的索引, 24 但是一个自然的 ID 也提供了一个可加载的机制,这在我们以前的代码中还没有见过,加上一个实际的优化。

该会话提供了加载器机制的概念,称为“加载访问”Hibernate 中包含三个加载器:能够按 ID 加载、自然 ID 和简单自然 ID。

按 ID 加载是指给定实例的内部引用。例如,如果一个 ID 为 1 的对象已经被 Hibernate 引用,Hibernate 就不需要去数据库加载该对象了——它可以通过 ID 查找该对象并返回引用。

自然本我是本我的另一种形式;在税务标识符的情况下,系统可以通过实际的对象 ID(在大多数情况下是一个人工键)或税务 ID 号本身来查找它——如果税务 ID 是一个“自然 ID”,那么库就能够在内部查找该对象,而不是为数据库构建一个查询。

正如简单标识符和复合标识符分别由单个字段和多个字段组成一样,自然 ID 也有两种形式,同样由单个字段或多个字段组成。

在简单 ID 的情况下,加载过程提供了一个简单的load()方法,所讨论的 ID 是参数。如果不存在具有该 ID 的实例,load()将返回null。加载器还提供了一个替代方法,一个getReference()方法,如果数据库中没有具有该自然 ID 的对象,该方法将抛出一个异常。

对于自然 id,有两种形式的加载机制;一个使用简单的自然 ID(其中自然 ID 是一个且只有一个字段),另一个使用命名属性作为复合自然 ID 的一部分。

现在让我们看一些实际的代码。首先,让我们创建一个表示清单 6-25 中的雇员的类;我们的员工将有一个名字(每个人都有一个名字),一个由数据库分配的人工 ID(一个员工编号),以及一个自然 ID,代表手动分配的工卡编号。

package chapter06.naturalid;

import org.hibernate.annotations.NaturalId;

import javax.persistence.*;

@Entity
public class SimpleNaturalIdEmployee {
  @Id
  @GeneratedValue(strategy = GenerationType.AUTO)
  Integer id;
  @NaturalId
  Integer badge;
  String name;
  @Column(scale=2,precision=5,nullable=false)
  double royalty;

  public SimpleNaturalIdEmployee() {
  }
  // extra housekeeping not echoed here
}

Listing 6-25A SimpleNaturalIdEmployee Class

简单的自然 ID 是通过用@NaturalId注解单个字段badge来声明的。这使我们能够使用byNaturalId()来获取具有该字段的实体。

要使用加载器机制,您可以通过使用Session.byId()Session.byNaturalId()Session.bySimpleNaturalId()获得一个引用,并传递实体的类型。简单加载器(对于 ID 和简单自然 ID)遵循相同的形式:获取加载器,然后加载或获取引用,使用键值作为参数。让我们看看那会是什么样子。首先,我们创建一个测试基类,让我们能够轻松地创建测试数据。

package chapter06.naturalid;

import com.autumncode.hibernate.util.SessionUtil;
import org.hibernate.Session;
import org.hibernate.Transaction;

public class IdTestBase {
  protected SimpleNaturalIdEmployee createSimpleEmployee(
      String name, int badge
  ) {
    SimpleNaturalIdEmployee employee = new SimpleNaturalIdEmployee();
    employee.setName(name);
    employee.setBadge(badge);
    employee.setRoyalty(10.2385);

    try (Session session = SessionUtil.getSession()) {
      Transaction tx = session.beginTransaction();
      session.save(employee);
      tx.commit();
    }
    return employee;
  }

  protected Employee createEmployee(
      String name,
      int section,
      int department
  ) {
    Employee employee = new Employee();
    employee.setName(name);
    employee.setDepartment(department);
    employee.setSection(section);
    try (Session session = SessionUtil.getSession()) {
      Transaction tx = session.beginTransaction();
      session.save(employee);
      tx.commit();
    }

    return employee;
  }

}

Listing 6-26IdTestBase.java

现在让我们看一个使用bySimpleNaturalId()方法的测试。

package chapter06.naturalid;

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

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

public class IdTestSimple extends IdTestBase {
  @Test
  public void testSimpleNaturalId() {
    Integer id = createSimpleEmployee("Sorhed", 5401).getId();

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

      SimpleNaturalIdEmployee employee =
          session
              .byId(SimpleNaturalIdEmployee.class)
              .load(id);
      assertNotNull(employee);
      SimpleNaturalIdEmployee badgedEmployee =
          session
              .bySimpleNaturalId(SimpleNaturalIdEmployee.class)
              .load(5401);
      assertEquals(badgedEmployee, employee);

      tx.commit();
    }
  }

}

Listing 6-27IdTestSimple.java

这个代码创建一个新的雇员,有一个特定的工卡号码(5401)。然后,它使用Session.byId(SimpleNaturalIdEmployee.class)为实体获取一个加载器,并调用load(id),使用由createSimpleEmployee()方法返回的 ID。

然而,这里发生了一件有趣的事情,代码演示了不一定从代码级别显而易见的情况。

当我们运行这个方法时,我们实际上加载了两个引用——否则等价性测试没有任何意义。 25 然而,如果我们查看在Session中执行的实际 SQL,我们会看到只发出了一个调用。

这是因为 Hibernate 会将自然 id 缓存在它在会话中加载的对象中。当我们在 load 访问器中使用 natural ID 时,Hibernate 会在会话缓存中查找并找到那个 natural ID——并且知道这是我们正在请求的引用。它不需要去数据库,因为它已经在内存中了。

这有助于使类更加自文档化,也稍微更有效率;这意味着,如果我们有一个来自真实世界的关于一个人的数据,API 会更有效。我们可以通过使用自然索引的工号而不是依赖于其他索引来找到给定的雇员,即使在数据库级别其他索引确实起了作用。

一个具有复合自然 ID 的实体仅仅有更多用@NaturalId标注的字段。让我们创建一个雇员,其部门是自然 ID, 26 ,如清单 6-28 所示。

package chapter06.naturalid;

import org.hibernate.annotations.NaturalId;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;

@Entity
public class Employee {
  @Id
  @GeneratedValue(strategy = GenerationType.AUTO)
  Integer id;
  @NaturalId
  Integer section;
  @NaturalId
  Integer department;
  String name;

  public Employee() {
  }
  // extra housekeeping not echoed here
}

Listing 6-28An Employee Class with a Compound Natural Id

接下来,让我们看一个演示自然 ID 加载器使用的测试,如清单 6-29 所示。 27

package chapter06.naturalid;

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

import static org.testng.Assert.*;

public class NaturalIdTest extends IdTestBase {
  @Test
  public void testSimpleNaturalId() {
    Integer id = createSimpleEmployee("Sorhed", 5401).getId();

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

      SimpleNaturalIdEmployee employee =
          session
              .byId(SimpleNaturalIdEmployee.class)
              .load(id);
      assertNotNull(employee);
      SimpleNaturalIdEmployee badgedEmployee =
          session
              .bySimpleNaturalId(SimpleNaturalIdEmployee.class)
              .load(5401);
      assertEquals(badgedEmployee, employee);

      tx.commit();
    }
  }

  @Test
  public void testLoadByNaturalId() {
    Employee initial = createEmployee("Arrowroot", 11, 291);
    try (Session session = SessionUtil.getSession()) {
      Transaction tx = session.beginTransaction();

      Employee arrowroot = session
          .byNaturalId(Employee.class)
          .using("section", 11)
          .using("department", 291)
          .load();
      assertNotNull(arrowroot);
      assertEquals(initial, arrowroot);

      tx.commit();
    }
  }

  @Test
  public void testGetByNaturalId() {
    Employee initial = createEmployee("Eorwax", 11, 292);
    try (Session session = SessionUtil.getSession()) {
      Transaction tx = session.beginTransaction();

      Employee eorwax = session
          .byNaturalId(Employee.class)
          .using("section", 11)
          .using("department", 292)
          .getReference();
      System.out.println(initial.equals(eorwax));
      assertEquals(initial, eorwax);

      tx.commit();
    }
  }

  @Test
  public void testLoadById() {
    Integer id = createEmployee("Legolam", 10, 289).getId();
    try (Session session = SessionUtil.getSession()) {
      Transaction tx = session.beginTransaction();

      Employee boggit = session.byId(Employee.class).load(id);
      assertNotNull(boggit);

         /*
        load successful, let's delete it for the second half of the test
        */
      session.delete(boggit);

      tx.commit();
    }

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

      Employee boggit = session.byId(Employee.class).load(id);
      assertNull(boggit);

      tx.commit();
    }
  }

  @Test
  public void testGetById() {
    Integer id = createEmployee("Eorache", 10, 290).getId();
    try (Session session = SessionUtil.getSession()) {
      Transaction tx = session.beginTransaction();

      Employee boggit = session.byId(Employee.class)
          .getReference(id);
      assertNotNull(boggit);

      /*
       * load successful, let's delete it for the second half of the test
       */
      session.delete(boggit);

      tx.commit();
    }

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

      try {
        Employee boggit = session.byId(Employee.class)
            .getReference(id);

        // trigger object initialization - which, with a nonexistent object,
        // will blow up.
        boggit.getDepartment();
        fail("Should have had an exception thrown!");
      } catch (ObjectNotFoundException ignored) {
      }

      tx.commit();
    }
  }
}

Listing 6-29The Natural Id Loader in Action

testLoadByNaturalId()中,我们看到了与我们之前的自然 ID 使用测试非常相似的东西:我们创建一个雇员,然后搜索 ID。由Session.byNaturalId()返回的对象有一个using()方法,它接受一个字段名和字段值,而不是使用一个对标识符的引用。如果我们不包括组成自然 ID 的每个字段,我们将得到一个异常。

注意,我们使用的是load()方法;如果数据库中不存在自然 ID,load()将返回一个 null 信号值。

NaturalIdTest中的另一种测试方法是testGetByNaturalId()。如果 id 值不存在,这个使用getReference()的函数将抛出一个异常,所以我们不需要检查null

摘要

在这一章中,我们使用 JPA 2 注解来为 Hibernate 的 POJOs 添加元数据,并且我们看到了一些特定于 Hibernate 的注解,这些注解可以以降低可移植性为代价来增强这些功能。

在下一章,我们将讨论 Hibernate 的 JPA 配置,更多的对象生命周期和数据验证。

Footnotes 1

大多数 ide 可以为您生成 XML 映射;另外,关于其他可能性,请参见 JBoss Tools ( https://tools.jboss.org/ )、XDoclet ( http://xdoclet.sourceforge.net/xdoclet/index.html )和 MyEclipse ( www.genuitec.com/products/myeclipse/ )。也就是说,大多数人更喜欢注解,理由很充分。

  2

为您的对象模型创建测试可能是有价值的,人们希望这样可以消除这种顾虑,但是为其本身创建额外的工作是没有意义的。

  3

JPA 配置很大程度上源自 Hibernate 的配置规范,它先于 JPA。

  4

当从一个给定的包中导入的类的数量大于 5 时,我的 IDE 切换到使用星形导入,而不是单独的类。这通常在每个 IDE 中都是可配置的,是否需要完全取决于读者。

  5

自然,有一种方法可以解决这个问题。通常的要求是有一个无参数的构造函数;拦截器允许你不必这样做。

  6

“字段访问就足够了”背后的推理是,如果您需要某个东西成为对象状态的一部分,您可以将它包含在数据库中,这意味着在对象被实例化时您不需要设置它。也就是说,你的里程可能会有所不同;做对你有用的事。

  7

实际上,默认生成器是“赋值”生成器,这意味着应用负责在调用save()之前分配主键。这最终会产生与没有指定密钥生成相同的效果。

  8

Serializable 依靠 Java 的自省来序列化和反序列化数据。可外部化迫使作者实现显式序列化机制。可外部化可以快得多,但在这种情况下,这不会给你带来任何真正的好处。

  9

让属性尽可能接近它们的定义实体可能是更明智的做法。如果一个publisher引用是 ISBN 的一部分,那么最好是有一个包含publisherISBN类,而不是让publisher成为书本身的一部分。然而,也可以提出这样的论点,作为 ISBN 的一部分使用的出版商关键字可以作为Publisher表的自然主键。

  10

如果您首先在 Hibernate 中构建一个对象模型,这种情况并不常见;当您从一个预先存在的数据库中构建一个对象模型时,由于某种原因,该数据库在多个表中有一个实体,这种情况通常会发生。当然,有个原因与数据库分析师和系统管理员比应用程序员更关心的问题有关——比如表在磁盘上的分配位置——但它们并不常见。

  11

注意,即使已经从数据库中检索了数据,类的属性可能还没有初始化。当从会话中加载实例时,可能已经急切地检索到了数据,但是直到请求了对象中的某些内容,才会初始化该对象。这可以产生一些“有趣”的行为,其中一些在前面的章节中已经介绍过了。

  12

在模式中指明非空性对于验证也是有用的,即使您不使用它来生成模式。

  13

这并不是对 XML 配置的认可。用@Transient代替。

  14

您也可以使用这种情况——您希望一个嵌入的对象分布在多个表中——作为一种迹象,表明您可能不打算对这些实体使用对象关系映射。但事实是,你可能没有正确地映射它。大概吧。

  15

如果每个实体都维护一个属性或字段来表示它在同一关系中的端点,那么关联就是双向的。例如,如果我们的 Address 类维护一个对位于那里的发布者的引用,而 Publisher 类维护一个对其地址的引用,那么关联将是双向的。

  16

当使用连接表时,外键关系在连接表本身中维护——因此,将@OneToMany 注解的 mappedBy 属性与@JoinTable 注解结合使用是不合适的。

  17

寓意一如既往:在集合中使用泛型。完全没有理由不这么做。

  18

您可以使用一个List<Author>来代替一个Set<Author>,但是Set的语义是由 Java 执行的,而不是由数据库执行的。数据库模式看起来是一样的。

  19

在你认为这个例子完全是人为的之前,有些标题可能会出奇的长。查看 https://bookstr.com/list/5-books-with-hilariously-long-titles/ 中一些可能不适合传统尺寸列的例子。

  20

请注意,可能的事情与更好的事情是非常不同的。您的作者知道在生产中使用过的这个特性的实例为零;这并不意味着它从来没有被使用过,但是它并不常见。

  21

嗯,从字面上讲,这里的放置包级注解的地方:毕竟是package-info.java

  22

同样,这并不是对 Hibernate XML 配置的认可。这也不完全是谴责,但是…

  23

注意,对此有不同的看法。大多数轶事数据会建议人工键作为主键,因为它们很短并且本质上是不可变的;然而,您可以找到许多提倡自然键的人,因为它们自然地映射到数据模型,而不是添加到数据模型中。也就是说,在一些面向数据的应用(例如数据仓库)中,人工键是不受反对的。考虑到这一点,建议是:使用人工密钥。你的数据可能会在某个时候入库。

  24

本章前面提到的@UniqueConstraints 注解可以对复合索引做同样的事情。也就是说,我们正试图寻找一种更好的方法来至少做一些排序和索引。

  25

请假设我们通常是有意义的。

  26

在前面的脚注中,我们说我们通常有意义。这是一个很好的例子,说明我们什么时候真的不知道;这是可怕的设计,在实际项目中会遭到严厉的指责。也就是说,代码是有效的,并且很好地展示了这个概念。

  27

这里的“员工名字”是无耻地——嗯,大部分是无耻地——从哈佛讽刺小说《厌倦了指环》一书中摘取的。