Hibernate ORM 中文操作指南

3. Entities

entity 是一个 Java 类,它表示关系数据库表中的数据。我们说实体 mapsmaps to 表。不太常见的是,一个实体可能会合并多个表中的数据,但我们将在 later 中实现该目标。

实体具有 attributes(属性或字段),它们映射到表中的列。特别是,每个实体都必须具有一个 identifierid,该实体映射到表的主键。id 允许我们唯一地将表的行与 Java 类的实例相关联,至少在给定的 persistence context 中是这样。

我们将在 later 中探讨持久性上下文的概念。现在,可以将其视为标识和实体实例的一对一映射。

Java 类的实例无法超越其所属的虚拟机继续存在。但我们可以认为实体实例具有超越内存中特定实例化的生命周期。通过向 Hibernate 提供其 ID,只要关联行存在于数据库中,我们就可以在新的持久性上下文中重新实体化该实例。因此,可以将 persist()remove() 操作视为界定了实体生命周期的开始和结束,至少与持久性有关。

因此,ID 表示实体的 persistent identity,一种超越内存中特定实例化的标识。并且这是实体类本身与其属性值之间的一个重要区别——实体具有持久性标识,并且具有相对于持久性而言明确定义的生命周期,而表示其属性值之一的 StringList 则没有。

实体通常与其他实体有联系。通常,两个实体之间的关联映射到数据库表之一的外键。一群相互关联的实体经常被称为 domain model,虽然 data model 也是一个很好的术语。

3.1. Entity classes

实体必须:

  1. be a non-final class,

  2. 使用没有参数的非-_private_构造函数。

另一方面,实体类可以是具体类或 abstract,并且可以拥有任意数量的其他构造函数。

实体类可能是 static 内部类。

每个实体类都必须注释 @Entity

@Entity
class Book {
    Book() {}
    ...
}

或者,可以通过为类提供基于 XML 的映射来将类标识为实体类型。

我们不会在这篇简介中对基于 XML 的映射做更多的说明,因为这并不是我们首选的方式。

3.2. Access types

每个实体类都有一个默认 access type,即:

  1. direct field access, or

  2. property access.

Hibernate 根据属性级注解的位置自动确定访问类型。具体而言:

  1. 如果字段采用注解 @Id,则使用字段访问,或

  2. 如果 getter 方法采用注解 @Id,则使用属性访问。

在 Hibernate 还只是个婴儿的时候,属性访问在 Hibernate 社区非常流行。然而,如今,字段访问 much 更常见。

可以使用 @Access 注解明确指定默认访问类型,但我们强烈不建议这样做,因为它很丑陋且没有必要。

应始终如一地放置映射标注:

如果 @Id 注释字段,其他映射注释也应该应用于字段,或

如果 @Id 注释 getter,其他映射注释应该应用于 getter。

原则上,可以使用显式 @Access 标注在属性级别混合字段和属性访问。我们不建议这样做。

Book 这样的实体类(它不扩展任何其他实体类)被称为 root entity。每个根实体都必须声明一个标识符属性。

3.3. Entity class inheritance

一个实体类可以 extend 另一个实体类。

@Entity
class AudioBook extends Book {
    AudioBook() {}
    ...
}

子类实体继承其扩展的每个实体的每个持续性属性。

根实体还可以扩展另一个类并从其他类继承映射的属性。但在这种情况下,声明映射的属性的类必须使用 @MappedSuperclass 注释。

@MappedSuperclass
class Versioned {
    ...
}

@Entity
class Book extends Versioned {
    ...
}

根实体类必须声明一个带 @Id 注释的属性,或者从 @MappedSuperclass 中继承一个属性。子类实体总是继承根实体的标识符属性。它可能不会声明其自己的 @Id 属性。

3.4. Identifier attributes

标识符属性通常是字段:

@Entity
class Book {
    Book() {}

    @Id
    Long id;

    ...
}

但它可能是一个属性:

@Entity
class Book {
    Book() {}

    private Long id;

    @Id
    Long getId() { return id; }
    void setId(Long id) { this.id = id; }

    ...
}

标识符属性必须带 @Id@EmbeddedId 注释。

标识符值可能为:

  1. 由应用程序分配,即由你的 Java 代码分配,或

  2. 由 Hibernate 生成并分配。

我们将首先讨论第二个选项。

3.5. Generated identifiers

标识符通常是系统生成的,在这种情况下,应该带 @GeneratedValue 注释:

@Id @GeneratedValue
Long id;

系统生成标识符,或 surrogate keys 使得演化或重构关系数据模型变得更加简单。如果您有自由定义关系模式,我们建议使用代理键。另一方面,如果您正在使用预先存在的数据库模式(更为常见),您可能没有这个选项。

JPA 根据 GenerationType 中的定义为生成 ID 提供了以下策略:

表 12. 标准 id 生成策略

Strategy

Java type

Implementation

GenerationType.UUID

UUID or String

A Java UUID

GenerationType.IDENTITY

Long or Integer

一个标识列或自增列

GenerationType.SEQUENCE

Long or Integer

A database sequence

GenerationType.TABLE

Long or Integer

A database table

GenerationType.AUTO

Long or Integer

基于标识符类型和数据库功能选择 SEQUENCETABLEUUID

例如,此 UUID 是在 Java 代码中生成的:

@Id @GeneratedValue UUID id;  // AUTO strategy selects UUID based on the field type

此 id 映射到 SQL identityauto_incrementbigserial 列:

@Id @GeneratedValue(strategy=IDENTITY) Long id;

@SequenceGenerator@TableGenerator 注释分别允许进一步控制 SEQUENCETABLE 生成。

考虑此序列生成器:

@SequenceGenerator(name="bookSeq", sequenceName="seq_book", initialValue = 5, allocationSize=10)

值是使用如下定义的数据库序列生成的:

create sequence seq_book start with 5 increment by 10

请注意,每次需要新标识符时,Hibernate 无需都转到数据库。相反,给定的进程会获取一个大小为 allocationSize 的 id 块,并且仅在块用尽时才需要访问数据库。当然,缺点是生成的标识符不连续。

如果您让 Hibernate 导出您的数据库模式,序列定义将拥有正确的 start withincrement 值。但如果您正在处理在 Hibernate 外部管理的数据库模式,请确保 initialValueallocationSize@SequenceGenerator 成员匹配 DDL 中指定的 start withincrement

任何标识符属性现在都可以使用 bookSeq 中指定的生成器:

@Id
@GeneratedValue(strategy=SEQUENCE, generator="bookSeq")  // reference to generator defined elsewhere
Long id;

实际上,将 @SequenceGenerator 注释放到使用它的 @Id 属性上非常常见:

@Id
@GeneratedValue(strategy=SEQUENCE, generator="bookSeq")  // reference to generator defined below
@SequenceGenerator(name="bookSeq", sequenceName="seq_book", initialValue = 5, allocationSize=10)
Long id;

JPA id 生成器可以在实体之间共享。@SequenceGenerator@TableGenerator 必须有一个名称,并且可以在多个 id 属性之间共享。这与注释使用生成器的 @Id 属性的普遍实践有些不合拍!

正如您所见,JPA 为系统生成 ID 的最常见策略提供了非常充足的支持。但注释本身比它们应有的更具入侵性,并且没有明确定义的方法来扩展此框架以支持用于生成 ID 的自定义策略。在未注释为 @Id 的属性上也不能使用 @GeneratedValue。由于自定义 ID 生成是相当普遍的要求,Hibernate 提供了一个非常精细设计的框架,用于用户定义的 _ 生成器_,我们将在 User-defined generators 中予以讨论。

3.6. Natural keys as identifiers

并非每个标识符属性都映射到(系统生成的)代理键。对系统用户来说有意义的主键被称为 natural keys

当表的主键是自然键时,我们不会使用 @GeneratedValue 注释标识符属性,并且由应用程序代码负责为标识符属性分配一个值。

@Entity
class Book {
    @Id
    String isbn;

    ...
}

特别令人感兴趣的是由多个数据库列组成的自然键,并且此类自然键称为 composite keys

3.7. Composite identifiers

如果你的数据库使用复合键,则需要多个标识符属性。有两种方法可以在 JPA 中映射复合键:

  1. using an @IdClass, or

  2. using an @EmbeddedId.

将这段文字用 Gemini 翻译成中文可能最直接的自然方式是在实体类中用多个 @Id 注释的字段表示,例如:

@Entity
@IdClass(BookId.class)
class Book {
    Book() {}

    @Id
    String isbn;

    @Id
    int printing;

    ...
}

但这种方法有一个问题:我们可以使用什么对象标识一个 Book,并将其传递给接受标识符的 find() 等方法?

解决方案是编写一个单独的类,其字段与实体的标识符属性相匹配。每个此类 ID 类必须覆盖 equals()hashCode()。当然,满足这些要求最简单的方法是将 ID 类声明为 record

record BookId(String isbn, int printing) {}

Book 实体的 @IdClass 注释将 BookId 标识为用于该实体的 ID 类。

这不是我们推荐的方法。相反,我们建议将 BookId 类声明为 @Embeddable 类型:

@Embeddable
record BookId(String isbn, int printing) {}

我们将在下面详细了解有关 Embeddable objects 的情况。

现在,实体类可以使用 @EmbeddedId 重用此定义,并且 @IdClass 注释不再是必需的:

@Entity
class Book {
    Book() {}

    @EmbeddedId
    BookId bookId;

    ...
}

第二种方法消除了部分重复代码。

无论哪种方式,我们现在都可以使用 BookId 来获取 Book 的实例:

Book book = session.find(Book.class, new BookId(isbn, printing));

3.8. Version attributes

实体可能具有由 Hibernate 用于乐观锁检查的属性。版本属性通常为 IntegerShortLongLocalDateTimeOffsetDateTimeZonedDateTimeInstant 类型。

@Version
LocalDateTime lastUpdated;

当实体持久化时,版本属性会由 Hibernate 自动分配,并在每次更新实体时自动递增或更新。

如果实体没有版本号(映射旧数据时常常会发生此情况),我们仍然可以执行乐观锁定。 @OptimisticLocking 标注使我们能够指定通过验证 ALL 字段值或仅验证实体的 DIRTY 字段值来检查乐观锁。 @OptimisticLock 标注使我们能够有选择地将某些字段排除在乐观锁定之外。

我们已经见过的 @Id@Version 属性只是 basic attributes 的专门示例。

3.9. Natural id attributes

即使实体具有替代键,也应该始终能够写下唯一标识实体实例的字段组合,从系统用户的角度来看。字段组合就是自然键。在上面,我们 considered 讨论了自然键与主键重叠的情况。此处,自然键是实体的第二个唯一键,与其替代主键不同。

如果您无法识别自然键,这可能表明您需要更仔细地思考您的数据模型的某些方面。如果一个实体没有有意义的唯一键,则不可能说出它在程序外部的“现实世界”中代表什么事件或对象。

由于基于自然键检索实体是 extremely 常见的做法,因此 Hibernate 有一种方法可以标记组成自然键的实体的属性。每个属性都必须使用 @NaturalId 进行标注。

@Entity
class Book {
    Book() {}

    @Id @GeneratedValue
    Long id; // the system-generated surrogate key

    @NaturalId
    String isbn; // belongs to the natural key

    @NaturalId
    int printing; // also belongs to the natural key

    ...
}

Hibernate 会为由注释字段映射的列自动生成一个 UNIQUE 约束。

考虑使用自然 ID 属性来实现 equals() and hashCode()

正如我们将在 much later 中看到,完成这项额外工作的好处在于,我们可以利用优化的自然 ID 查找,充分利用二级缓存。

请注意,即使您已标识出自然键,我们仍建议在外部键中使用生成的代理键,因为这可以使您的数据模型 much 更易于更改。

3.10. Basic attributes

实体的 basic 属性是映射到关联数据库表单个列的字段或属性。JPA 规范定义了一组非常有限的基本类型:

表 13. JPA 标准基本属性类型

Classification

Package

Types

Primitive types

boolean, int, double, etc

Primitive wrappers

java.lang

Boolean, Integer, Double, etc

Strings

java.lang

String

Arbitrary-precision numeric types

java.math

BigInteger, BigDecimal

Date/time types

java.time

LocalDateLocalTimeLocalDateTimeOffsetDateTimeInstant

Deprecated date/time types 💀

java.util

Date, Calendar

弃用的 JDBC 日期/时间类型 💀

java.sql

Date, Time, Timestamp

Binary and character arrays

byte[], char[]

UUIDs

java.util

UUID

Enumerated types

Any enum

Serializable types

implements _java.io.Serializable_的任何类型

我们在恳求您使用 java.time 的包中的类型,而不是继承 java.util.Date 的任何东西。

将 Java 对象序列化并将其二进制表示形式存储到数据库通常是错误的。正如我们将在 Embeddable objects 中很快看到的那样,Hibernate 拥有更好地处理复杂 Java 对象的方法。

Hibernate 通过以下类型略微扩展了此列表:

表 14. Hibernate 中的其他基本属性类型

Classification

Package

Types

Additional date/time types

java.time

DurationZoneIdZoneOffsetYear、甚至是 ZonedDateTime

JDBC LOB types

java.sql

Blob, Clob, NClob

Java class object

java.lang

Class

Miscellaneous types

java.util

Currency, URL, TimeZone

@Basic 注释明确指明一个属性是基本属性,但通常不需要它,因为属性默认为基本属性。另一方面,如果一个非基本类型属性不能为 null,则强烈建议使用 @Basic(optional=false)

@Basic(optional=false) String firstName;
@Basic(optional=false) String lastName;
String middleName; // may be null

请注意,基本类型属性默认情况下是 NOT NULL 推断得到的。

3.11. Enumerated types

我们包括 Java enum_s on the list above. An enumerated type is considered a sort of basic type, but since most databases don’t have a native _ENUM 类型,JPA 提供了特殊 @Enumerated 注解,用于指定枚举值在数据库中应如何表示:

  1. 默认情况下,枚举存储为整数,即其_ordinal()_成员的值,但

  2. 如果属性带_@Enumerated(STRING)_注解,则存储为字符串,即其_name()_成员的值。

//here, an ORDINAL encoding makes sense
@Enumerated
@Basic(optional=false)
DayOfWeek dayOfWeek;

//but usually, a STRING encoding is better
@Enumerated(EnumType.STRING)
@Basic(optional=false)
Status status;

在 Hibernate 6 中,带 enum 注解的 @Enumerated(STRING) 映射到:

  1. 大多数数据库中带有_CHECK_约束的_VARCHAR_列类型,或

  2. MySQL中的_ENUM_列类型。

任何其他 enum 都映射到带 CHECK 约束的 TINYINT 列。

JPA 在这里选择了错误的默认值。在大多数情况下,存储 enum 值的整数编码会使关系数据更难解释。

即使考虑到 DayOfWeek ,编码为整数也是模棱两可的。如果你查看 java.time.DayOfWeek ,你会发现 SUNDAY 编码为 6 。但在我的出生地, SUNDAY 是星期 first

因此,我们更喜欢 @Enumerated(STRING) 作为大多数 enum 属性。

PostgreSQL 是一个有趣的特殊情况。Postgres 支持 named ENUM 类型,必须使用 DDL CREATE TYPE 语句声明。遗憾的是,这些 ENUM 类型与语言集成不佳,也不受 Postgres JDBC 驱动程序的充分支持,因此 Hibernate 默认不使用它们。但是,如果您想在 Postgres 上使用命名枚举类型,只需像这样注释您的 enum 属性:

@JdbcTypeCode(SqlTypes.NAMED_ENUM)
@Basic(optional=false)
Status status;

可以通过提供 converter 来略微扩展有限的预定义基本属性类型的集合。

3.12. Converters

JPA AttributeConverter 负责:

  1. 将给定的Java类型转换为上面列出的某个类型,和/或

  2. 在将基本属性值写入或从数据库读出之前,执行其他任何必要的预处理和后处理。

转换器大幅扩展了 JPA 可以处理的属性类型集合。

有两种应用转换器的方法:

  1. 在_@Convert_注解中,将_AttributeConverter_应用于特定实体属性:

  2. 在_@Converter_注解(或也可选的_@ConverterRegistration_注解)中,注册_AttributeConverter_以自动应用于给定类型的全部属性。

例如,以下转换器将自动应用于类型为 BitSet 的任何属性,并负责将 BitSet 持续存储到类型为 varbinary 的列:

@Converter(autoApply = true)
public static class EnumSetConverter
        // converts Java values of type EnumSet<DayOfWeek> to integers for storage in an INT column
        implements AttributeConverter<EnumSet<DayOfWeek>,Integer> {
    @Override
    public Integer convertToDatabaseColumn(EnumSet<DayOfWeek> enumSet) {
        int encoded = 0;
        var values = DayOfWeek.values();
        for (int i = 0; i<values.length; i++) {
            if (enumSet.contains(values[i])) {
                encoded |= 1<<i;
            }
        }
        return encoded;
    }

    @Override
    public EnumSet<DayOfWeek> convertToEntityAttribute(Integer encoded) {
        var set = EnumSet.noneOf(DayOfWeek.class);
        var values = DayOfWeek.values();
        for (int i = 0; i<values.length; i++) {
            if (((1<<i) & encoded) != 0) {
                set.add(values[i]);
            }
        }
        return set;
    }
}

另一方面,如果我们 don’t 设置 autoapply=true,则我们必须通过使用 @Convert 注解显式应用转换器:

@Convert(converter = BitSetConverter.class)
@Basic(optional = false)
BitSet bitset;

所有这些都很不错,但这可能不会让你惊讶,因为 Hibernate 超越了 JPA 所需的内容。

3.13. Compositional basic types

Hibernate 将“基本类型”视为由两个对象组成的婚姻结合:

  1. JavaType,其对某个Java类的语义建模,以及

  2. JdbcType,表示JDBC识别的SQL类型。

映射基本属性时,我们可能明确指定 JavaTypeJdbcType,或同时指定两者。

JavaType

org.hibernate.type.descriptor.java.JavaType 的实例表示特定的 Java 类。它能够:

  1. 比较类的实例以确定该类类型上的属性是否已修改(脏数据),

  2. 为类的实例生成有用的哈希码,

  3. 将值强制转换为其他类型,尤其是

  4. 根据其_JdbcType_合作伙伴请求,将类的实例转换成几个相同的Java表示之一。

例如,IntegerJavaType 知道如何将 Integerint 值转换为 LongBigIntegerString 等类型。

我们可以使用 @JavaType 注释明确指定一种 Java 类型,但是对于内置的 JavaType 来说,这是从不必要的。

@JavaType(LongJavaType.class)  // not needed, this is the default JavaType for long
long currentTimeMillis;

对于用户编写的 JavaType,注释更加有用:

@JavaType(BitSetJavaType.class)
BitSet bitSet;

或者,可以使用 @JavaTypeRegistration 注释来注册 BitSetJavaType 作为 BitSet 的默认值 JavaType

JdbcType

org.hibernate.type.descriptor.jdbc.JdbcType 能够从 JDBC 读写单个 Java 类型和写到 JDBC。

例如,VarcharJdbcType 负责:

  1. 将Java字符串写入JDBC_PreparedStatement_s by calling setString(),以及

  2. 从JDBC_ResultSet_s using _getString()_中读取Java字符串。

通过将 LongJavaTypeVarcharJdbcType 以神圣的婚姻结合,我们产生了映射 Long_s and primitive _longs_s to the SQL type _VARCHAR 的基本类型。

我们可以使用 @JdbcType 注释明确指定一种 JDBC 类型。

@JdbcType(VarcharJdbcType.class)
long currentTimeMillis;

或者,我们可以指定一种 JDBC 类型代码:

@JdbcTypeCode(Types.VARCHAR)
long currentTimeMillis;

@JdbcTypeRegistration 注释可用于注册一个用户编写的 JdbcType 作为给定的 SQL 类型代码的默认值。

AttributeConverter

如果给定的 JavaType 不知道如何将其实例转换为其合作伙伴 JdbcType 所需的类型,我们必须通过提供一个 JPA AttributeConverter 来帮助它执行转换。

例如,要使用 LongJavaTypeTimestampJdbcType 形成基本类型,我们将提供一个 AttributeConverter<Long,Timestamp>

@JdbcType(TimestampJdbcType.class)
@Convert(converter = LongToTimestampConverter.class)
long currentTimeMillis;

在我们将这个基本类型称为“三重列表”之前,让我们放弃我们的类比。

3.14. Embeddable objects

可嵌入对象是一种 Java 类,其状态映射到表的多个列,但没有它自己的持久性标识。也就是说,它是一个具有映射属性的类,但没有 @Id 属性。

可嵌入对象只能通过分配给实体的属性才能变得持久。由于可嵌入对象没有它自己的持久性标识,因此它在持久性方面的生命周期完全由它所属的实体的生命周期决定。

可嵌入类必须使用 @Embeddable 注释而不是 @Entity

@Embeddable
class Name {

    @Basic(optional=false)
    String firstName;

    @Basic(optional=false)
    String lastName;

    String middleName;

    Name() {}

    Name(String firstName, String middleName, String lastName) {
        this.firstName = firstName;
        this.middleName = middleName;
        this.lastName = lastName;
    }

    ...
}

可嵌入类必须满足实体类满足的相同要求,但可嵌入类没有 @Id 属性除外。特别是,它必须有一个没有参数的构造函数。

或者,可将可嵌入式类型定义为 Java 记录类型:

@Embeddable
record Name(String firstName, String middleName, String lastName) {}

在这种情况下,对没有参数的构造函数的需求会得到放宽。

我们现在可以将我们的 Name 类(或记录)用作实体属性的类型:

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

    Name name;

    ...
}

可嵌入式类型可以是嵌套的。也就是说,@Embeddable 类可能有一个属性,其类型本身是另一个不同的 @Embeddable 类。

JPA 提供了一个 @Embedded 注解来标识实体的属性,这些属性引用可嵌入类型。该注解是完全可选的,因此我们通常不使用它。

另一方面,对可嵌入类型的引用是 never 多态的。一个 @EmbeddableF 可能继承第二个 @EmbeddableE,但类型为 E 的属性将始终引用该具体类 E 的实例,而永远不会引用 F 的实例。

通常,可嵌入类型将存储为“扁平化”格式。它们的属性会映射到其所属实体表的列。稍后在 Mapping embeddable types to UDTs or to JSON 中,我们将看到几种不同的选项。

可嵌入式类型的属性表示具有持久性标识的 Java 对象与不具有持久性标识的 Java 对象之间的关系。我们可以将其视为整体/部分关系。可嵌入式对象属于实体,不能与其他实体实例共享。而且,它仅在父实体存在的期间内存在。

接下来,我们将讨论另一种关系:具有各自不同的持久性标识和持久性生命周期的 Java 对象之间的关系。

3.15. Associations

association 是实体之间的关系。我们通常根据关联的 multiplicity 对关联进行分类。如果 EF 都是实体类,则:

  1. _one-to-one_关联将至多一个唯一实例_E_与至多一个_F_唯一实例关联起来,

  2. _many-to-one_关联将_E_的零个或多个实例与_F_的唯一实例关联起来,以及

  3. _many-to-many_关联将_E_的零个或多个实例与_F_的零个或多个实例关联起来。

实体类之间的关联可以是:

  1. unidirectional,从_E_指向_F_的可导航,但从_F_指向_E_不可导航,或

  2. bidirectional,且在任何方向均可导航。

在这个示例数据模型中,我们可以看到可能的关联类型:

associations

细心的观察者可能会注意到上图中提出的关系是一个单向一对一关联,可以用 Java 中的分类型合理表示。这是很常见的。在完全归一化的关系模型中,我们通常使用一对一关联实现分类型。它与 JOINED inheritance mapping 策略有关。

用于映射关联的注释有三个:@ManyToOne@OneToMany@ManyToMany。它们共享一些通用的注释成员:

表 15. 定义关联的注解成员

Member

Interpretation

Default value

cascade

应当 cascade于关联实体的持久性操作;CascadeType 的列表

{}

fetch

关联是 eagerly fetched或者可能 proxied

针对 @OneToMany_的 _LAZY_以及针对 _@ManyToOne_的 _@ManyToMany__EAGER💀💀💀

targetEntity

The associated entity class

根据属性类型声明确定

optional

对于 @ManyToOne_或 _@OneToOne_关联,关联可以 _null

true

mappedBy

对于双向关联,映射关联的关联实体的属性

默认情况下,已关联假设为单向

当我们考虑各种类型的关联映射时,我们将解释这些成员的作用。

让我们从最常见的关联多重性开始。

3.16. Many-to-one

多对一关联是我们能想象到的最基本的关联类型。它完全自然地映射到数据库中的外键。域模型中的几乎所有关联都将采用这种形式。

后面,我们将会看到如何将多对一关联映射到一个 association table

@ManyToOne 注释标记关联的“到一”端,因此单向多对一关联如下所示:

class Book {
    @Id @GeneratedValue
    Long id;

    @ManyToOne(fetch=LAZY)
    Publisher publisher;

    ...
}

在这里,Book 表有一个外键列,其中保存着关联的 Publisher 的标识符。

JPA 具有一个非常不幸的缺陷,默认情况下, @ManyToOne 关联被即时获取。这几乎不是我们想要的。几乎所有关联都应该是延迟的。 fetch=EAGER 有意义的唯一情况是我们认为 very 始终有很高的概率 associated object will be found in the second-level cache 。如果并非如此,请记得明确指定 fetch=LAZY

大多数情况下,我们希望能够轻松地在两个方向上导航我们的关联。我们确实需要一种方法来获取给定 BookPublisher,但我们也希望能够获取属于给定发布者的所有 Book

为了使此关联变为双向,我们需要向 Publisher 类中添加集合值属性并对其进行 @OneToMany 注释。

Hibernate 需要在运行时 proxy 未获取的关联。因此,多值侧必须使用接口类型(如 SetList )声明,并且从不使用具体类型(如 HashSetArrayList )声明。

为了明确表示这是一个双向关联,并重用已在 Book 实体中指定的任何映射信息,我们必须使用 mappedBy 注释成员引用 Book.publisher

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

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

    ...
}

Publisher.books 字段称为关联的 unowned 端。

现在,我们热情地 hate 将强类型 mappedBy 引用推荐给关联的所有方。幸运的是, Metamodel Generator 为我们提供了一种使其更具类型安全的方法:

@OneToMany(mappedBy=Book_.PUBLISHER)  // get used to doing it this way!
Set<Book> books;

我们将在介绍的剩余部分中使用这种方法。

如果要修改双向关联,我们必须更改 owning side

对关联的非拥有端所做的更改绝不会同步到数据库中。如果我们希望在数据库中更改关联,则必须在拥有端更改关联。在此,我们必须设置 Book.publisher

事实上,经常需要更改双向关联的 both sides 。例如,如果将集合 Publisher.books 存储在二级缓存中,我们还必须修改该集合,以确保二级缓存与数据库保持同步。

也就是说,更新非自有端是 not 一项艰巨的要求,至少如果您确定知道自己在做什么的话。

原则上,Hibernate does 允许您拥有单向多对一,即另一方没有匹配的 @ManyToOne@OneToMany。实际上,这种映射是不自然的,而且并不太好用。避免它。

这里我们使用 Set 作为集合的类型,但 Hibernate 也允许在此处使用 ListCollection,语义几乎没有差别。特别是,List 可能不包含重复元素,其顺序不会持久化。

@OneToMany(mappedBy=Book_.PUBLISHER)
Collection<Book> books;

我们将在 much later 中看到如何映射具有持久顺序的集合。

3.17. One-to-one (first way)

最简单的单一对一关联几乎与 @ManyToOne 关联完全相同,不同之处在于它映射到带有 UNIQUE 约束的外键列。

后面,我们将会看到如何将一对一关联映射到一个 association table

一对一关联必须注释 @OneToOne

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

    @OneToOne(optional=false, fetch=LAZY)
    Person author;

    ...
}

在这里,Author 表有一个外键列,其中包含关联的 Person 的标识符。

一对一关联通常模拟“类型为”关系。在我们的示例中, AuthorPerson 的类型。在 Java 中表示“类型为”关系的另一种(通常更自然)的方法是通过 entity class inheritance

我们可以通过在 Person 实体中添加对 Author 的引用,使此关联变为双向:

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

    @OneToOne(mappedBy = Author_.PERSON)
    Author author;

    ...
}

Person.author 是非拥有端,因为它标有 mappedBy 的一侧。

这并不是一对一关联的唯一类型。

3.18. One-to-one (second way)

一种更优雅的方式来表示这种关系是这两个表之间共享一个主键。

要使用这种方法,必须像这样对 Author 类进行注释:

@Entity
class Author {
    @Id
    Long id;

    @OneToOne(optional=false, fetch=LAZY)
    @MapsId
    Person author;

    ...
}

请注意,与之前的映射相比:

  1. @Id_属性不再是@GeneratedValue_,并且

  2. 而是_author_关联被注解为_@MapsId_。

这可让 Hibernate 了解到与 Person 的关联是 Author 主键值的来源。

此处,Author 表中没有额外的外键列,因为 id 列包含 Person 的标识符。也就是说,Author 表的主键充当了指代 Person 表的外键的双重职责。

Person 类并不改变。如果关联是双向的,我们会像以前一样对非拥有端 @OneToOne(mappedBy = Author.PERSON)_ 进行注释。

3.19. Many-to-many

单向多对多关联表示为集合值属性。它总是映射到数据库中的单独 association table

通常情况下,多对多关联最终会变成伪装的实体。

假设我们从 AuthorBook 之间的简洁干净的多对多关联开始。稍后,我们很可能会发现一些附加信息附加到关联中,因此关联表需要一些额外的列。

例如,假设我们需要报告每位作者在书中所占的百分比贡献。该信息自然属于关联表。我们无法轻松地将它存储为 Book 的属性,也无法将其存储为 Author 的属性。

当这种情况发生时,我们需要更改 Java 模型,通常需引入一个新的实体类来直接映射关联表。在我们示例中,我们可以将此实体命名为 BookAuthorship ,它将包含与 AuthorBook@OneToMany 关联,以及 contribution 属性。

我们可以通过从一开始就避免使用 @ManyToMany 来躲避此类“发现”所造成的干扰。使用中间实体表示每个(或至少是 almost )逻辑多对多关联几乎没有缺点。

多对多关联必须注释 @ManyToMany

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

    @ManyToMany
    Set<Author> authors;

    ...
}

如果关联是双向的,我们向 Book 添加一个非常相似的属性,但这次我们必须指定 mappedBy 以指示这是关联的非拥有端:

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

    @ManyToMany(mappedBy=Author_.BOOKS)
    Set<Author> authors;

    ...
}

请记住,如果我们希望修改集合,我们必须 change the owning side

我们再次使用了 Set_s to represent the association. As before, we have the option to use _CollectionList。但在这种情况下,它 does 会影响关联的语义。

表示为 CollectionList 的多对多关联可能包含重复元素。但是,与之前一样,元素的次序不是持久的。也就是说,该集合是 bag,而不是集合。

3.20. Collections of basic values and embeddable objects

我们现在看到了以下类型的实体属性:

Kind of entity attribute

Kind of reference

Multiplicity

Examples

基本类型的单值属性

Non-entity

At most one

@Basic String name

可嵌入类型的单值属性

Non-entity

At most one

@Embedded Name name

Single-valued association

Entity

At most one

@ManyToOne Publisher publisher_@OneToOne 人员 person_

Many-valued association

Entity

Zero or more

@OneToMany Set&lt;Book&gt; books_@ManyToMany 地址 authors_

扫描此分类法,你可能会问:Hibernate 是否具有基本或可嵌入类型的多值属性?

好,实际上我们已经看到了它确实如此,至少在两种特殊情况下。因此,首先,让我们 recall JPA 将 byte[]char[] 数组视为基本类型。Hibernate 会将 byte[]char[] 数组持久保存到 VARBINARYVARCHAR 列(分别)。

但在本节中,我们真正关注的是比这两个特例 other 的情况。那么, apart from _byte[]char[]_ ,Hibernate 是否具有基本类型或可嵌入类型多值属性?

答案仍然是 it does。事实上,有两种不同的方法来处理这样的集合,即通过映射它:

  1. 转换为 SQL _ARRAY_类型的列(假设数据库有_ARRAY_类型),或

  2. to a separate table.

因此,我们可以通过以下方式扩展我们的分类法:

Kind of entity attribute

Kind of reference

Multiplicity

Examples

byte[] and char[] arrays

Non-entity

Zero or more

byte[] image__char[] text

Collection of basic-typed elements

Non-entity

Zero or more

@Array String[] names_@ElementCollection 名称 names_ 的集合

Collection of embeddable elements

Non-entity

Zero or more

@ElementCollection Set<Name> names

实际上,这里有两种新的映射类型:@Array 映射和 @ElementCollection 映射。

对这类映射的使用过度。

在某些情况下,我们认为在我们的实体类中使用基本类型值的集合是合适的。但这种情况很少见。几乎所有多值关系都应映射到独立表之间的外键关联。几乎每个表都应通过实体类映射。

我们在接下来两个子部分中将要了解的功能,初学者使用得比专家多。因此,如果您是初学者,现阶段远离这些功能将节省您相同的麻烦。

我们先来讨论一下 @Array 映射。

3.21. Collections mapped to SQL arrays

让我们考虑在每周某些日期重复的日历事件。我们可能在_Event_ 实体中将其表示为类型为_DayOfWeek[]_ 或_List<DayOfWeek>_ 的属性。由于此数组或列表的元素数上限为 7,因此这是一个使用_ARRAY_ 类型列的合理案例。将此集合存储在单独的表中似乎没什么价值。

不幸的是,JPA 没有定义将 SQL 数组映射到标准方法,但以下是如何在 Hibernate 中进行映射:

@Entity
class Event {
    @Id @GeneratedValue
    Long id;
    ...
    @Array(length=7)
    DayOfWeek[] daysOfWeek;  // stored as a SQL ARRAY type
    ...
}

@Array 注释是可选的,但限制数据库分配给_ARRAY_ 列的存储空间数量非常重要。通过在此处编写_@Array(length=7),我们指定 DDL 应该使用列类型_TINYINT ARRAY[7] 生成。

仅出于好玩,我们在上面的代码中使用了一个枚举类型,但是数组元素时间可能是几乎任何 basic type。例如,Java 数组类型 String[]UUID[]double[]BigDecimal[]LocalDate[]OffsetDateTime[] 通通允许,分别映射到 SQL 类型 VARCHAR(n) ARRAYUUID ARRAYFLOAT(53) ARRAYNUMERIC(p,s) ARRAYDATE ARRAYTIMESTAMP(p) WITH TIME ZONE ARRAY

现在,让我们明白的是:并非每个数据库都有 SQL ARRAY 类型,并且某些 do 具有 ARRAY 类型的数据库不允许将其用作列类型。

具体来讲,DB2 和 SQL Server 都没有数组类型列。在此类数据库中,Hibernate 会退而使用更糟糕的方法:它使用 Java 序列化将数组编码成二进制表示形式,并将二进制流存储在 VARBINARY 列中。很明显,这样做很糟糕。您可以要求 Hibernate 通过注释属性 @JdbcTypeCode(SqlTypes.JSON) 来做些 slightly 不那么糟糕的事情,以便将数组序列化为 JSON 而不是二进制格式。但在此时,最好直接承认失败,转而使用 @ElementCollection

或者,我们可以将该数组或列表存储在单独的表中。

3.22. Collections mapped to a separate table

JPA does 定义了将集合映射到辅助表的标准方法:@ElementCollection 注释。

@Entity
class Event {
    @Id @GeneratedValue
    Long id;
    ...
    @ElementCollection
    DayOfWeek[] daysOfWeek;  // stored in a dedicated table
    ...
}

实际上,我们不应该在此处使用数组,因为数组类型无法 proxied,因此 JPA 规范甚至没有说它们受支持。相反,我们应该使用 SetListMap

@Entity
class Event {
    @Id @GeneratedValue
    Long id;
    ...
    @ElementCollection
    List<DayOfWeek> daysOfWeek;  // stored in a dedicated table
    ...
}

在其中,每个集合元素作为辅助表的单独行存储。默认情况下,此表具有以下定义:

create table Event_daysOfWeek (
    Event_id bigint not null,
    daysOfWeek tinyint check (daysOfWeek between 0 and 6),
    daysOfWeek_ORDER integer not null,
    primary key (Event_id, daysOfWeek_ORDER)
)

这是合适的,但它仍然是我们希望避免的映射。

@ElementCollection 是我们最不喜欢的 JPA 功能之一。即使是注释名称也不好。

上面的代码将生成一个包含三列的表:

a foreign key of the Event table,

a TINYINT encoding the enum, and

an INTEGER encoding the ordering of elements in the array.

它没有替代主键,而是一个复合键,其中包含 Event 的外键和顺序列。

当我们(不可避免地)发现需要向该表添加第四列时,我们的 Java 代码必须完全更改。很可能,我们最终会意识到需要添加一个单独的实体。所以,在我们的数据模型发生小更改的情况下,这种映射并不十分稳健。

我们可以就“元素集合”的多得多进行讨论,但我们不会这么做,因为我们不想让你拿起枪对自己开枪。

3.23. Summary of annotations

让我们停下来,回想起我们到目前为止了解过的注释。

表 16. 声明实体和可嵌入类型

Annotation

Purpose

JPA-standard

@Entity

Declare an entity class

@MappedSuperclass

声明一个非实体类,其中映射的属性继承自一个实体

@Embeddable

Declare an embeddable type

@IdClass

声明一个具有多个_@Id_属性的实体的标识符类

表 17. 声明基本和嵌入式属性

Annotation

Purpose

JPA-standard

@Id

声明一个基本类型标识符属性

@Version

Declare a version attribute

@Basic

Declare a basic attribute

Default

@EmbeddedId

声明一个可嵌入类型标识符属性

@Embedded

Declare an embeddable-typed attribute

Inferred

@Enumerated

声明一个_enum_类型属性,并指定其如何编码

Inferred

@Array

声明一个属性映射到 SQL ARRAY,并指定长度

Inferred

@ElementCollection

声明一个集合映射到一个专用表

表 18. 转换器和复合基本类型

Annotation

Purpose

JPA-standard

@Converter

Register an AttributeConverter

@Convert

将一个转换器应用到一个属性

@JavaType

为一个基本属性明确指定 JavaType 的实现

@JdbcType

为一个基本属性明确指定 JdbcType 的实现

@JdbcTypeCode

明确指定用于确定基本属性_JdbcType_的 JDBC 类型代码

@JavaTypeRegistration

为给定的 Java 类型注册 JavaType

@JdbcTypeRegistration

针对给定的 JDBC 类型代码注册 JdbcType

表 19. 系统生成的标识符

Annotation

Purpose

JPA-standard

@GeneratedValue

指定标识符是由系统生成的

@SequenceGenerator

定义以数据库序列为后盾的 ID 生成

@TableGenerator

定义以数据库表为后盾的 ID 生成

@IdGeneratorType

声明将自定义 Generator 与它注释的每个 @Id 属性关联的注释

@ValueGenerationType

声明将自定义 Generator 与它注释的每个 @Basic 属性关联的注释

表 20. 声明实体关联

Annotation

Purpose

JPA-standard

@ManyToOne

声明一对多关联的单值方(拥有方)

@OneToMany

声明一对多关联的多值方(无拥有方)

@ManyToMany

声明多对多关联的任意一方

@OneToOne

声明一对一关联的任意一方

@MapsId

声明 @OneToOne 关联的拥有方映射了主键列

呼!这已经是许多注释了,我们甚至没有开始 O/R 映射的注释!

3.24. equals() and hashCode()

实体类应覆盖 equals()hashCode(),尤其是在关联 represented as sets 时。

初次使用 Hibernate 或 JPA 的人经常会困惑究竟应将哪些字段包含在 hashCode() 中。以及有更多经验的人经常会十分虔诚地认为一个或另一个方法是唯一正确的途径。事实上,没有一种正确的方法,但存在一些约束。因此,请牢记以下原则:

  1. You should not include a mutable field in the hashcode, since that would require rehashing every collection containing the entity whenever the field is mutated.

  2. It’s not completely wrong to include a generated identifier (surrogate key) in the hashcode, but since the identifier is not generated until the entity instance is made persistent, you must take great care to not add it to any hashed collection before the identifier is generated. We therefore advise against including any database-generated field in the hashcode.

将任何不可变的非生成字段包含在哈希中是没有问题的。

在此示例中,equals() 方法和 hashCode() 方法与 @NaturalId 注释一致:

@Entity
class Book {

    @Id @GeneratedValue
    Long id;

    @NaturalId
    @Basic(optional=false)
    String isbn;

    String getIsbn() {
        return isbn;
    }

    ...

    @Override
    public boolean equals(Object other) {
        return other instanceof Book                   // check type with instanceof, not getClass()
            && ((Book) other).getIsbn().equals(isbn);  // compare natural ids
    }
    @Override
    public int hashCode() {
        return isbn.hashCode();  // hashcode based on the natural id
    }
}

也就是说,基于实体的生成标识符实现 equals()hashCode() 也可用作 if you’re careful