Hibernate ORM 中文操作指南

4. Object/relational mapping

给定一个域模型——即一个实体类的集合(这些类在上一章中装饰了所有花哨的注释)——Hibernate 将乐于执行以下操作:推断一个完整的相关架构,甚至在您礼貌地请求后 export it to your database 它。

最终的架构将十分合理稳定,虽然如果您仔细观察,您会发现一些缺陷。例如,每个 VARCHAR 列将具有相同的长度 VARCHAR(255)

但我刚才描述的过程,我们称之为 top down 映射,根本不适用于 O/R 映射最常见的场景。Java 类先于关联模式的情况很少见。通常是 we already have a relational schema,我们在模式周围构建我们的域模型。这称为 bottom up 映射。

开发人员通常将已存在的关联数据库称为“遗留”数据。这往往会联想到用 COBOL 或其他语言编写的糟糕旧“遗留应用程序”。但遗留数据是有价值的,学习如何使用它很重要。

尤其是在从下往上映射时,我们常常需要自定义推断的对象/关系映射。这是一个有点枯燥的话题,所以我们不想在这上面花太多笔墨。相反,我们将会快速浏览一下最重要的映射注解。

4.1. Mapping entity inheritance hierarchies

Entity class inheritance 中,我们看到实体类可能存在于继承层次结构中。有三种基本策略可将实体层次结构映射到关系表。我们把它们放在一张表格中,这样我们就可以更容易地比较它们之间的不同点。

表 21. 实体继承映射策略

Strategy

Mapping

Polymorphic queries

Constraints

Normalization

When to use it

SINGLE_TABLE

将层次结构中的每个类映射到同一张表,并使用 discriminator column 的值确定每一行表示哪个具体类。

若要检索给定类的实例,我们只需要查询一个表即可。

由子类声明的属性会映射到没有 NOT NULL 约束的列 💀 任何关联都可以有 FOREIGN KEY 约束 🤓

子类数据是反范式的 🧐

在子类声明少量或不声明任何其他属性时效果很好。

JOINED

将层次结构中的每个类映射到一个单独的表中,但每个表只映射该类本身声明的属性。可以选择使用歧视符列。

为了检索特定类的实例,我们必须使用以下内容 JOIN 映射到其超类的所有表和映射到其子类的所有表,映射到该类的表。

任何属性都可以映射到带有 NOT NULL 约束的列 🤓 任何关联都可以有 FOREIGN KEY 约束 🤓

这些表是经过标准化的 🤓

在我们非常关注约束和标准化时最佳选择。

TABLE_PER_CLASS

将层次结构中的每个具体类映射到一个单独的表中,但将所有继承的属性反范式到表中。

为了检索特定类的实例,我们必须在映射到该类的表和映射到其子类的表中进行 UNION

针对超类的关联在数据库中不能有相应的 FOREIGN KEY 约束 💀 💀 任何属性都可以映射到带有 NOT NULL 约束的列 🤓

超类数据是反范式的 🧐

不太流行。从某种程度上来说,与 @MappedSuperclass 存在竞争。

三个映射策略由 InheritanceType 枚举。我们使用 @Inheritance 注释指定继承映射策略。

对于有 discriminator column 的映射,我们应该:

  1. specify the discriminator column name and type by annotating the root entity @DiscriminatorColumn, and

  2. specify the values of this discriminator by annotating each entity in the hierarchy @DiscriminatorValue.

对于单表继承我们总是需要一个判别器:

@Entity
@DiscriminatorColumn(discriminatorType=CHAR, name="kind")
@DiscriminatorValue('P')
class Person { ... }

@Entity
@DiscriminatorValue('A')
class Author { ... }

我们不需要明确指定 @Inheritance(strategy=SINGLE_TABLE),因为那是默认设置。

对于 JOINED 继承我们不需要一个判别器:

@Entity
@Inheritance(strategy=JOINED)
class Person { ... }

@Entity
class Author { ... }

但是,我们可以在需要时添加一个discriminator列,在这种情况下,为多态查询生成的 SQL 将略微简单一些。

类似地,对于 TABLE_PER_CLASS 继承,我们有:

@Entity
@Inheritance(strategy=TABLE_PER_CLASS)
class Person { ... }

@Entity
class Author { ... }

Hibernate 不允许为 TABLE_PER_CLASS 继承映射指定discriminator列,因为它们毫无意义,也没有任何优势。

请注意,在最后一种情况下,多态关联类似:

@ManyToOne Person person;

是不好的主意,因为不可能创建同时针对两个已映射表的外部键约束。

4.2. Mapping to tables

以下注释确切地指定了域模型的元素如何映射到关系模型的表:

表 22. 用于映射表的注释

Annotation

Purpose

@Table

将实体类映射到其主键表中

@SecondaryTable

为实体类定义一个次表

@JoinTable

将多对多或多对一关联映射到其关联表

@CollectionTable

@ElementCollection 映射到其表

前两个注释用于将实体映射到它的 primary table,并且可选地将一个或多个 secondary tables 映射到它。

4.3. Mapping entities to tables

默认情况下,一个实体映射到单个表,该表可以使用 @Table 指定:

@Entity
@Table(name="People")
class Person { ... }

然而,@SecondaryTable 注释允许我们将它的属性分布到多个 secondary tables 中。

@Entity
@Table(name="Books")
@SecondaryTable(name="Editions")
class Book { ... }

@Table 注释不仅仅可以指定名称:

表 23. @Table 注释成员

Annotation member

Purpose

name

已映射表的名称

schema 💀

该表归属的架构

catalog 💀

该表归属的目录

uniqueConstraints

一或多个 @UniqueConstraint 注释声明多列唯一约束

indexes

一或多个 @Index 注释,每个都声明一个索引

如果域模型分布在多个架构中,则显式指定注释中的 schema 才合理。

否则,在 @Table 注释中硬编码架构(或编目)是一个坏主意。相反:

set the configuration property hibernate.default_schema (or hibernate.default_catalog), or

simply specify the schema in the JDBC connection URL.

@SecondaryTable 注释更加有趣:

表 24. @SecondaryTable 注释成员

Annotation member

Purpose

name

已映射表的名称

schema 💀

该表归属的架构

catalog 💀

该表归属的目录

uniqueConstraints

一或多个 @UniqueConstraint 注释声明多列唯一约束

indexes

一或多个 @Index 注释,每个都声明一个索引

pkJoinColumns

指定 primary key column mappings的一个或很多 _@PrimaryKeyJoinColumn_注释

foreignKey

一个 @ForeignKey 注释,指定 @PrimaryKeyJoinColumn_s 上 _FOREIGN KEY 约束的名称

SINGLE_TABLE 实体继承层次结构中的子类上使用 @SecondaryTable 为我们提供了一种 SINGLE_TABLEJOINED 继承相结合的方式。

4.4. Mapping associations to tables

@JoinTable 注释指定一个 association table,即包含两个关联实体的外键的表。这个注释通常与 @ManyToMany 关联一起使用:

@Entity
class Book {
    ...

    @ManyToMany
    @JoinTable(name="BooksAuthors")
    Set<Author> authors;

    ...
}

但是,它还可以用于将 @ManyToOne@OneToOne 关联映射到关联表。

@Entity
class Book {
    ...

    @ManyToOne(fetch=LAZY)
    @JoinTable(name="BookPublisher")
    Publisher publisher;

    ...
}

此处,应在关联表的一列中添加 UNIQUE 约束。

@Entity
class Author {
    ...

    @OneToOne(optional=false, fetch=LAZY)
    @JoinTable(name="AuthorPerson")
    Person author;

    ...
}

此处,应在关联表的 both 列中添加 UNIQUE 约束。

表 25. @JoinTable 注释成员

Annotation member

Purpose

name

已映射关联表的名称

schema 💀

该表归属的架构

catalog 💀

该表归属的目录

uniqueConstraints

一或多个 @UniqueConstraint 注释声明多列唯一约束

indexes

一或多个 @Index 注释,每个都声明一个索引

joinColumns

指定 foreign key column mappings至拥有侧表的 _@JoinColumn_注释

inverseJoinColumns

指定 foreign key column mappings至未拥有侧表的 _@JoinColumn_注释

foreignKey

一个 @ForeignKey 注解,指定在 joinColumns_s 上的 _FOREIGN KEY 约束的名称

inverseForeignKey

一个 @ForeignKey 注解,指定在 inverseJoinColumns_s 上的 _FOREIGN KEY 约束的名称

为了更好地理解这些注释,我们首先必须讨论列映射的总体情况。

4.5. Mapping to columns

这些注释指定了域模型的元素如何映射到关系模型中的表的列:

表 26. 用于映射列的注释

Annotation

Purpose

@Column

将属性映射到列

@JoinColumn

将关联映射到外键列

@PrimaryKeyJoinColumn

映射主要键,用于将辅助表与其主键,或 JOINED 继承中的子类表与其根类表连接

@OrderColumn

指定应用于维持 List 顺序的列。

@MapKeyColumn

指定应用于持久化 Map 键的列。

我们使用 @Column 注释来映射基本属性。

4.6. Mapping basic attributes to columns

_@Column_注释不仅可用于指定列名。

表 27. _@Column_注释成员

Annotation member

Purpose

name

映射列的名称

table

此列所属的表名称

length

VARCHARCHARVARBINARY 列类型的长度

precision

FLOATDECIMALNUMERICTIMETIMESTAMP 列类型的精度小数位数

scale

DECIMALNUMERIC 列类型的精度,即小数点右侧的精度位数

unique

列是否有 UNIQUE 约束

nullable

列是否有 NOT NULL 约束

insertable

列是否应该出现在生成的 SQL INSERT 语句中

updatable

列是否应该出现在生成的 SQL UPDATE 语句中

columnDefinition 💀

一个 DDL 片段,用于声明一个列

由于它导致不可移植的 DDL,因此我们不再推荐使用 columnDefinition。Hibernate 有更好的方法可以使用在不同数据库中产生可移植行为的技术来自定义所生成的 DDL。

这里我们看到了使用 @Column 注解的四种不同方式:

@Entity
@Table(name="Books")
@SecondaryTable(name="Editions")
class Book {
    @Id @GeneratedValue
    @Column(name="bookId") // customize column name
    Long id;

    @Column(length=100, nullable=false) // declare column as VARCHAR(100) NOT NULL
    String title;

    @Column(length=17, unique=true, nullable=false) // declare column as VARCHAR(17) NOT NULL UNIQUE
    String isbn;

    @Column(table="Editions", updatable=false) // column belongs to the secondary table, and is never updated
    int edition;
}

我们不使用 _@Column_来映射关联。

4.7. Mapping associations to foreign key columns

_@JoinColumn_注释用于自定义外键列。

表 28. _@JoinColumn_注释成员

Annotation member

Purpose

name

映射的外键列的名称

table

此列所属的表名称

referencedColumnName

映射外键列引用的列的名称

unique

列是否有 UNIQUE 约束

nullable

列是否有 NOT NULL 约束

insertable

列是否应该出现在生成的 SQL INSERT 语句中

updatable

列是否应该出现在生成的 SQL UPDATE 语句中

columnDefinition 💀

一个 DDL 片段,用于声明一个列

foreignKey

一个 @ForeignKey 注解,指定 FOREIGN KEY 约束的名称

外键列不必引用被引用表的键。外键引用被引用实体的任何其他唯一键是可以接受的,甚至是二级表的唯一键。

这里,我们将看到如何使用 _@JoinColumn_定义将外键列映射到引用 _Book_的 _@NaturalId_的 _@ManyToOne_关联:

@Entity
@Table(name="Items")
class Item {
    ...

    @ManyToOne(optional=false)  // implies nullable=false
    @JoinColumn(name = "bookIsbn", referencedColumnName = "isbn",  // a reference to a non-PK column
                foreignKey = @ForeignKey(name="ItemsToBooksBySsn")) // supply a name for the FK constraint
    Book book;

    ...
}

若此令人困惑:

  1. bookIsbn is the name of the foreign key column in the Items table,

  2. it refers to a unique key isbn in the Books table, and

  3. it has a foreign key constraint named ItemsToBooksBySsn.

请注意,_foreignKey_成员是完全可选的,且只会影响 DDL 生成。

如果你不使用 @ForeignKey 提供一个显式名称,Hibernate 会生成一个相当丑陋的名字。原因在于,某些数据库的外键名称最长长度受到极大限制,我们需要避免冲突。公平地说,如果你仅仅将生成的 DDL 用于测试,这是完全可以的。

对于复合外键,我们可能有几个 @JoinColumn 注解:

@Entity
@Table(name="Items")
class Item {
    ...

    @ManyToOne(optional=false)
    @JoinColumn(name = "bookIsbn", referencedColumnName = "isbn")
    @JoinColumn(name = "bookPrinting", referencedColumnName = "printing")
    Book book;

    ...
}

如果我们需要指定 @ForeignKey,这会变得有点乱:

@Entity
@Table(name="Items")
class Item {
    ...

    @ManyToOne(optional=false)
    @JoinColumns(value = {@JoinColumn(name = "bookIsbn", referencedColumnName = "isbn"),
                          @JoinColumn(name = "bookPrinting", referencedColumnName = "printing")},
                 foreignKey = @ForeignKey(name="ItemsToBooksBySsn"))
    Book book;

    ...
}

对于映射到 _@JoinTable_的关联,获取关联需要进行两次连接,因此我们必须声明 _@JoinColumn_s inside the _@JoinTable_注释:

@Entity
class Book {
    @Id @GeneratedValue
    Long id;

    @ManyToMany
    @JoinTable(joinColumns=@JoinColumn(name="bookId"),
               inverseJoinColumns=@joinColumn(name="authorId"),
               foreignKey=@ForeignKey(name="BooksToAuthors"))
    Set<Author> authors;

    ...
}

_foreignKey_成员仍然是可选的。

对于映射 to a primary key@MapsId@OneToOne 关联,Hibernate 允许我们使用 @JoinColumn@PrimaryKeyJoinColumn

_@Entityclass Author { @Id Long id;

@OneToOne(optional=false, fetch=LAZY) @MapsId @PrimaryKeyJoinColumn(name="personId") Person author;

    ...
}_
_@Entity
class Author {
    @Id
    Long id;

@OneToOne(optional=false, fetch=LAZY) @MapsId @PrimaryKeyJoinColumn(name="personId") Person author;

    ...
}_
Arguably, the use of _@PrimaryKeyJoinColumn_ is clearer.

4.8. Mapping primary key joins between tables

_@PrimaryKeyJoinColumn_是用于映射的专用注释:

  1. the primary key column of a @SecondaryTable—which is also a foreign key referencing the primary table, or

  2. the primary key column of the primary table mapped by a subclass in a JOINED inheritance hierarchy—which is also a foreign key referencing the primary table mapped by the root entity.

表 29. _@PrimaryKeyJoinColumn_注释成员

Annotation member

Purpose

name

映射的外键列的名称

referencedColumnName

映射外键列引用的列的名称

columnDefinition 💀

一个 DDL 片段,用于声明一个列

foreignKey

一个 @ForeignKey 注解,指定 FOREIGN KEY 约束的名称

映射子类表的键时,我们将 _@PrimaryKeyJoinColumn_注释放到实体类上:

@Entity
@Table(name="People")
@Inheritance(strategy=JOINED)
class Person { ... }

@Entity
@Table(name="Authors")
@PrimaryKeyJoinColumn(name="personId") // the primary key of the Authors table
class Author { ... }

但要映射二级表的键,_@PrimaryKeyJoinColumn_注释必须在 _@SecondaryTable_注释内部:

@Entity
@Table(name="Books")
@SecondaryTable(name="Editions",
                pkJoinColumns = @PrimaryKeyJoinColumn(name="bookId")) // the primary key of the Editions table
class Book {
    @Id @GeneratedValue
    @Column(name="bookId") // the name of the primary key of the Books table
    Long id;

    ...
}

4.9. Column lengths and adaptive column types

Hibernate 根据 @Column_注释指定的长自动调整生成 DDL 中使用的列类型。因此,我们通常不需要明确指定列的类型应为 _TEXT_或 _CLOB,也不必担心 MySQL 上 TINYTEXTMEDIUMTEXTTEXT、_LONGTEXT_类型的一系列问题,因为在需要适应我们指定的 _length_字符串时,Hibernate 会自动选择其中一种类型。

此类中定义的常量值在此非常有用:

表 30. 预定义的列长度

Constant

Value

Description

DEFAULT

255

当没有明确指定长度时,列 VARCHARVARBINARY 的默认长度

LONG

32600

允许在 Hibernate 支持的每个数据库上使用的 VARCHARVARBINARY 的最大列长度

LONG16

32767

使用 16 位可以表示的最大长度(但对于某些数据库来说,该长度对于 VARCHARVARBINARY 列来说太大了)

LONG32

2147483647

Java 字符串的最大长度

我们可以在 @Column 注释中使用这些常量:

@Column(length=LONG)
String text;

@Column(length=LONG32)
byte[] binaryData;

这通常就是你需要做的一切,就能在 Hibernate 中使用大型对象类型。

4.10. LOBs

JPA 提供了一个 @Lob 注释,该注释指定应将一个字段作为 BLOBCLOB 进行持久化。

Hibernate 以我们认为最合理的方式来解释此注释。在 Hibernate 中,一个带 @Lob 注释的属性将使用 PreparedStatementsetClob()setBlob() 方法写入 JDBC,并将使用 ResultSetgetClob()getBlob() 方法从 JDBC 中读取。

现在,通常不需要使用这些 JDBC 方法!JDBC 驱动程序完全能转换 _String_和 _CLOB_之间或 _byte[]_和 _BLOB_之间。因此,除非你明确需要使用这些 JDBC LOB API,否则你不需要 _@Lob_注释。

相反,正如我们在 Column lengths and adaptive column types 中刚刚看到的,您只需要指定足够大的列 length 来容纳您计划写入该列的数据。

你通常应该这样写:

@Column(length=LONG32) // good, correct column type inferred
String text;

而不是这样:

@Lob // almost always unnecessary
String text;

对于 PostgreSQL 来说,这一点尤其正确。

不幸的是,PostgreSQL 驱动程序不允许 BYTEATEXT 列通过 JDBC LOB API 进行读取。

Postgres 驱动程序的这一限制导致了一大批博客作者和 stackoverflow 问答者煞费苦心地推荐如何 hack Hibernate Dialect for Postgres 以允许将注释为 @Lob 的属性使用 setString() 编写并在 getString() 中读取。

但简单地移除 @Lob 注释具有完全相同的效果。

结论:

on PostgreSQL, @Lob always means the OID type,

@Lob should never be used to map columns of type BYTEA or TEXT, and

please don’t believe everything you read on stackoverflow.

最后,作为一种替代方法,Hibernate 允许你声明 java.sql.Blobjava.sql.Clob 类型的属性。

@Entity
class Book {
    ...
    Clob text;
    Blob coverArt;
    ....
}

其优点是,_java.sql.Clob_或 _java.sql.Blob_原则上可以索引多达 263 个字符或字节,远远多于可以放入 Java _String_或 _byte[]_数组(或你的计算机)中的数据。

要为这些字段分配一个值,我们需要使用 LobHelper 。我们可以从 Session 中获取一个:

LobHelper helper = session.getLobHelper();
book.text = helper.createClob(text);
book.coverArt = helper.createBlob(image);

原则上,BlobClob 对象提供了从服务器读取或流式传输 LOB 数据的高效方式。

Book book = session.find(Book.class, bookId);
String text = book.text.getSubString(1, textLength);
InputStream bytes = book.images.getBinaryStream();

当然,此处的行为在很大程度上取决于 JDBC 驱动程序,所以我们无法保证在你的数据库中这样做有意义。

4.11. Mapping embeddable types to UDTs or to JSON

可以在数据库端使用一些备用方式来表示可嵌入类型。

Embeddables as UDTs

首先,一个很好的选项(至少是 Java 记录类型和支持 user-defined types (UDT) 的数据库),就是定义一个表示记录类型的 UDT。Hibernate 6 使此操作变得非常容易。只需使用新 @Struct 注释,对记录类型或持有对记录类型的引用的属性进行注释:

@Embeddable
@Struct(name="PersonName")
record Name(String firstName, String middleName, String lastName) {}
@Entity
class Person {
    ...
    Name name;
    ...
}

这会导致以下 UDT:

create type PersonName as (firstName varchar(255), middleName varchar(255), lastName varchar(255))

并且 Author 表的 name 列将具有类型 PersonName

Embeddables to JSON

另一个可用选项是将可嵌入类型映射到 JSON(或 JSONB)列。现在,如果您要从头开始定义数据模型,这不是我们准确 recommend 的内容,但至少可用于映射具有 JSON 类型列的预定义表。由于可嵌入类型是可嵌套的,因此我们可以通过这种方式映射一些 JSON 格式,甚至可以使用 HQL 查询 JSON 属性。

此时,不支持 JSON 数组!

若要将嵌入式类型的属性映射到 JSON,我们必须为 @JdbcTypeCode(SqlTypes.JSON) 属性添加注解,而不是为嵌入式类型添加注解。但是,如果我们想要使用 HQL 查询嵌入式类型的属性,那么 Name 嵌入式类型仍然应该被 @Embeddable 注释。

@Embeddable
record Name(String firstName, String middleName, String lastName) {}
@Entity
class Person {
    ...
    @JdbcTypeCode(SqlTypes.JSON)
    Name name;
    ...
}

我们还需要向运行时类路径添加 Jackson 或 JSONB 实现(例如 Yasson)。要使用 Jackson,我们可以将此行添加到 Gradle 构建:

runtimeOnly 'com.fasterxml.jackson.core:jackson-databind:{jacksonVersion}'

现在 Author 表的 name 列将具有类型 jsonb,并且 Hibernate 将自动使用 Jackson 将 Name 序列化为 JSON 格式并从 JSON 格式反序列化为 Name

4.12. Summary of SQL column type mappings

因此,如我们所见,有不少注释会影响 Java 类型在 DDL 中映射到 SQL 列类型的方式。此处,我们在本章的后半部分总结我们刚刚看到的注释,以及我们在前面章节中已经提到的部分内容。

表 31.用于映射 SQL 列类型的注释

Annotation

Interpretation

@Enumerated

指定如何持久化 enum 类型

@Nationalized

使用一个国际化字符类型:NCHARNVARCHAR,或 NCLOB

@Lob 💀

使用 JDBC LOB API 来读取并写入注释属性

@Array

将一个集合映射到一个 SQL ARRAY 类型,具有指定长度

@Struct

将一个可嵌入映射到一个具有给定名称的 SQL UDT

@TimeZoneStorage

指定时区信息应该如何持久化

@JdbcType or @JdbcTypeCode

使用 JdbcType 的一个实现来映射一个任意 SQL 类型

@Collate

为一个列指定一个排序规则

此外,还有一些配置属性会 global 影响基本类型如何映射到 SQL 列类型:

表 32.类型映射设置

Configuration property name

Purpose

hibernate.use_nationalized_character_data

默认启用使用国际化字符类型

hibernate.type.preferred_boolean_jdbc_type

为映射 boolean 指定默认的 SQL 列类型

hibernate.type.preferred_uuid_jdbc_type

为映射 UUID 指定默认的 SQL 列类型

hibernate.type.preferred_duration_jdbc_type

为映射 Duration 指定默认的 SQL 列类型

hibernate.type.preferred_instant_jdbc_type

为映射 Instant 指定默认的 SQL 列类型

hibernate.timezone.default_storage

指定存储时区信息时的默认策略

这些是 global 设置,因此非常笨拙。除非你确实有充分的理由,否则我们建议不要改变这些设置中的任何一项。

在这一章中,我们还要探讨另一个主题。

4.13. Mapping to formulas

Hibernate 允许我们将实体的属性映射到涉及映射表列的 SQL 公式。因此,该属性是一种“派生”值。

表 33.用于映射公式的注释

Annotation

Purpose

@Formula

将属性映射到 SQL 公式

@JoinFormula

将关联映射到 SQL 公式

@DiscriminatorFormula

single table inheritance中使用 SQL 公式作为区分符。

例如:

@Entity
class Order {
    ...
    @Column(name = "sub_total", scale=2, precision=8)
    BigDecimal subTotal;

    @Column(name = "tax", scale=4, precision=4)
    BigDecimal taxRate;

    @Formula("sub_total * (1.0 + tax)")
    BigDecimal totalWithTax;
    ...
}

4.14. Derived Identity

如果实体从关联的“父”实体继承了主键的一部分,则该实体具有 derived identity。当我们讨论 one-to-one associations with a shared primary key 时,我们已经遇到了 derived identity 退化的案例。

@ManyToOne 关联也可能构成派生标识符的一部分。也就是说,可能包括外键列或多列作为复合主键的一部分。可以在 Java 侧以三种不同的方式表示此情况:

  1. using @IdClass without @MapsId,

  2. using @IdClass with @MapsId, or

  3. using @EmbeddedId with @MapsId.

假设我们有一个 Parent 实体类,其定义如下:

@Entity
class Parent {
    @Id
    Long parentId;

    ...
}

parentId 字段保存 Parent 表的主键,该主键还将构成属于 Parent 的每个 Child 的复合主键的一部分。

First way

在第一个稍微简单一些的方法中,我们定义一个 @IdClass 来表示 Child 的主键:

class DerivedId {
    Long parent;
    String childId;

    // constructors, equals, hashcode, etc
    ...
}

并使用 @Id 注释的 @ManyToOne 关联定义一个 Child 实体类 @Id

@Entity
@IdClass(DerivedId.class)
class Child {
    @Id
    String childId;

    @Id @ManyToOne
    @JoinColumn(name="parentId")
    Parent parent;

    ...
}

然后 Child 表的主键包含列 (childId,parentId)

Second way

这很好,但是有时为每个主键元素准备一个字段会很好。我们可以使用我们在 earlier 中遇到的 @MapsId 注释:

@Entity
@IdClass(DerivedId.class)
class Child {
    @Id
    Long parentId;
    @Id
    String childId;

    @ManyToOne
    @MapsId(Child_.PARENT_ID) // typesafe reference to Child.parentId
    @JoinColumn(name="parentId")
    Parent parent;

    ...
}

我们正在使用我们在 previously 中看到的 approach 来以类型安全的方式引用 ChildparentId 属性。

请注意我们必须将列映射信息放在注释为 @MapsId 的联合上,而不是 @Id 字段上。

我们必须稍稍修改一下 @IdClass 让字段名称对齐:

class DerivedId {
    Long parentId;
    String childId;

    // constructors, equals, hashcode, etc
    ...
}

Third way

第三种解决方法是将我们的 @IdClass 重新定义为 @Embeddable。我们实际上并不需要更改 DerivedId 类,但我们需要添加注释。

@Embeddable
class DerivedId {
    Long parentId;
    String childId;

    // constructors, equals, hashcode, etc
    ...
}

然后我们可以在 Child 中使用 @EmbeddedId

@Entity
class Child {
    @EmbeddedId
    DerivedId id;

    @ManyToOne
    @MapsId(DerivedId_.PARENT_ID) // typesafe reference to DerivedId.parentId
    @JoinColumn(name="parentId")
    Parent parent;

    ...
}

@IdClass@EmbeddedId 之间的 choice 最终归结为品味。@EmbeddedId 可能稍微枯燥一些。

4.15. Adding constraints

数据库约束很重要。即使你确定你的程序没有错误 🧐,它可能不是唯一可以访问数据库的程序。约束有助于确保不同的程序(和人工管理员)愉快地共处。

Hibernate 会自动将某些约束添加到生成的 DDL:主键约束、外键约束和一些唯一约束。但通常需要:

  1. add additional unique constraints,

  2. add check constraints, or

  3. customize the name of a foreign key constraint.

我们已经 already seen 如何使用 @ForeignKey 来指定外键约束的名称。

有两种方式可以向表中添加唯一约束:

  1. using @Column(unique=true) to indicate a single-column unique key, or

  2. using the @UniqueConstraint annotation to define a uniqueness constraint on a combination of columns.

@Entity
@Table(uniqueConstraints=@UniqueConstraint(columnNames={"title", "year", "publisher_id"}))
class Book { ... }

此注释看起来可能有点丑陋,但它实际上很有用,甚至用作文档。

@Check 注释会向表中添加一个检查约束。

@Entity
@Check(name="ValidISBN", constraints="length(isbn)=13")
class Book { ... }

@Check 注释通常在字段级别使用:

@Id @Check(constraints="length(isbn)=13")
String isbn;