Hibernate ORM 中文操作指南

4. Object/relational mapping

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

Given a domain model—that is, a collection of entity classes decorated with all the fancy annotations we just met in the previous chapter—Hibernate will happily go away and infer a complete relational schema, and even export it to your database if you ask politely.

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

The resulting schema will be entirely sane and reasonable, though if you look closely, you’ll find some flaws. For example, every VARCHAR column will have the same length, VARCHAR(255).

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

But the process I just described—which we call top down mapping—simply doesn’t fit the most common scenario for the use of O/R mapping. It’s only rarely that the Java classes precede the relational schema. Usually, we already have a relational schema, and we’re constructing our domain model around the schema. This is called bottom up mapping.

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

Developers often refer to a pre-existing relational database as "legacy" data. This tends to conjure images of bad old "legacy apps" written in COBOL or something. But legacy data is valuable, and learning to work with it is important.

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

Especially when mapping bottom up, we often need to customize the inferred object/relational mappings. This is a somewhat tedious topic, and so we don’t want to spend too many words on it. Instead, we’ll quickly skim the most important mapping annotations.

4.1. Mapping entity inheritance hierarchies

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

In Entity class inheritance we saw that entity classes may exist within an inheritance hierarchy. There’s three basic strategies for mapping an entity hierarchy to relational tables. Let’s put them in a table, so we can more easily compare the points of difference between them.

表 21. 实体继承映射策略

Table 21. Entity inheritance mapping strategies

Strategy

Mapping

Polymorphic queries

Constraints

Normalization

When to use it

SINGLE_TABLE

Map every class in the hierarchy to the same table, and uses the value of a discriminator column to determine which concrete class each row represents.

To retrieve instances of a given class, we only need to query the one table.

Attributes declared by subclasses map to columns without NOT NULL constraints. 💀Any association may have a FOREIGN KEY constraint. 🤓

Subclass data is denormalized. 🧐

Works well when subclasses declare few or no additional attributes.

JOINED

Map every class in the hierarchy to a separate table, but each table only maps the attributes declared by the class itself.Optionally, a discriminator column may be used.

To retrieve instances of a given class, we must JOIN the table mapped by the class with:all tables mapped by its superclasses andall tables mapped by its subclasses.

Any attribute may map to a column with a NOT NULL constraint. 🤓Any association may have a FOREIGN KEY constraint. 🤓

The tables are normalized. 🤓

The best option when we care a lot about constraints and normalization.

TABLE_PER_CLASS

Map every concrete class in the hierarchy to a separate table, but denormalize all inherited attributes into the table.

To retrieve instances of a given class, we must take a UNION over the table mapped by the class and the tables mapped by its subclasses.

Associations targeting a superclass cannot have a corresponding FOREIGN KEY constraint in the database. 💀💀Any attribute may map to a column with a NOT NULL constraint. 🤓

Superclass data is denormalized. 🧐

Not very popular.From a certain point of view, competes with @MappedSuperclass.

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

The three mapping strategies are enumerated by InheritanceType. We specify an inheritance mapping strategy using the @Inheritance annotation.

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

For mappings with a discriminator column, we should:

  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.

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

For single table inheritance we always need a discriminator:

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

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

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

We don’t need to explicitly specify @Inheritance(strategy=SINGLE_TABLE), since that’s the default.

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

For JOINED inheritance we don’t need a discriminator:

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

@Entity
class Author { ... }

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

However, we can add a discriminator column if we like, and in that case the generated SQL for polymorphic queries will be slightly simpler.

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

Similarly, for TABLE_PER_CLASS inheritance we have:

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

@Entity
class Author { ... }

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

Hibernate doesn’t allow discriminator columns for TABLE_PER_CLASS inheritance mappings, since they would make no sense, and offer no advantage.

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

Notice that in this last case, a polymorphic association like:

@ManyToOne Person person;

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

is a bad idea, since it’s impossible to create a foreign key constraint that targets both mapped tables.

4.2. Mapping to tables

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

The following annotations specify exactly how elements of the domain model map to tables of the relational model:

表 22. 用于映射表的注释

Table 22. Annotations for mapping tables

Annotation

Purpose

@Table

Map an entity class to its primary table

@SecondaryTable

Define a secondary table for an entity class

@JoinTable

Map a many-to-many or many-to-one association to its association table

@CollectionTable

Map an @ElementCollection to its table

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

The first two annotations are used to map an entity to its primary table and, optionally, one or more secondary tables.

4.3. Mapping entities to tables

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

By default, an entity maps to a single table, which may be specified using @Table:

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

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

However, the @SecondaryTable annotation allows us to spread its attributes across multiple secondary tables.

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

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

The @Table annotation can do more than just specify a name:

表 23. @Table 注释成员

Table 23. @Table annotation members

Annotation member

Purpose

name

The name of the mapped table

schema 💀

The schema to which the table belongs

catalog 💀

The catalog to which the table belongs

uniqueConstraints

One or more @UniqueConstraint annotations declaring multi-column unique constraints

indexes

One or more @Index annotations each declaring an index

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

It only makes sense to explicitly specify the schema in annotations if the domain model is spread across multiple schemas.

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

Otherwise, it’s a bad idea to hardcode the schema (or catalog) in a @Table annotation. Instead:

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

simply specify the schema in the JDBC connection URL.

@SecondaryTable 注释更加有趣:

The @SecondaryTable annotation is even more interesting:

表 24. @SecondaryTable 注释成员

Table 24. @SecondaryTable annotation members

Annotation member

Purpose

name

The name of the mapped table

schema 💀

The schema to which the table belongs

catalog 💀

The catalog to which the table belongs

uniqueConstraints

One or more @UniqueConstraint annotations declaring multi-column unique constraints

indexes

One or more @Index annotations each declaring an index

pkJoinColumns

One or more @PrimaryKeyJoinColumn annotations, specifying primary key column mappings

foreignKey

A @ForeignKey annotation specifying the name of the FOREIGN KEY constraint on the _@PrimaryKeyJoinColumn_s

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

Using @SecondaryTable on a subclass in a SINGLE_TABLE entity inheritance hierarchy gives us a sort of mix of SINGLE_TABLE with JOINED inheritance.

4.4. Mapping associations to tables

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

The @JoinTable annotation specifies an association table, that is, a table holding foreign keys of both associated entities. This annotation is usually used with @ManyToMany associations:

@Entity
class Book {
    ...

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

    ...
}

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

But it’s even possible to use it to map a @ManyToOne or @OneToOne association to an association table.

@Entity
class Book {
    ...

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

    ...
}

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

Here, there should be a UNIQUE constraint on one of the columns of the association table.

@Entity
class Author {
    ...

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

    ...
}

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

Here, there should be a UNIQUE constraint on both columns of the association table.

表 25. @JoinTable 注释成员

Table 25. @JoinTable annotation members

Annotation member

Purpose

name

The name of the mapped association table

schema 💀

The schema to which the table belongs

catalog 💀

The catalog to which the table belongs

uniqueConstraints

One or more @UniqueConstraint annotations declaring multi-column unique constraints

indexes

One or more @Index annotations each declaring an index

joinColumns

One or more @JoinColumn annotations, specifying foreign key column mappings to the table of the owning side

inverseJoinColumns

One or more @JoinColumn annotations, specifying foreign key column mappings to the table of the unowned side

foreignKey

A @ForeignKey annotation specifying the name of the FOREIGN KEY constraint on the _joinColumns_s

inverseForeignKey

A @ForeignKey annotation specifying the name of the FOREIGN KEY constraint on the _inverseJoinColumns_s

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

To better understand these annotations, we must first discuss column mappings in general.

4.5. Mapping to columns

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

These annotations specify how elements of the domain model map to columns of tables in the relational model:

表 26. 用于映射列的注释

Table 26. Annotations for mapping columns

Annotation

Purpose

@Column

Map an attribute to a column

@JoinColumn

Map an association to a foreign key column

@PrimaryKeyJoinColumn

Map the primary key used to join a secondary table with its primary, or a subclass table in JOINED inheritance with its root class table

@OrderColumn

Specifies a column that should be used to maintain the order of a List.

@MapKeyColumn

Specified a column that should be used to persist the keys of a Map.

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

We use the @Column annotation to map basic attributes.

4.6. Mapping basic attributes to columns

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

The @Column annotation is not only useful for specifying the column name.

表 27. _@Column_注释成员

Table 27. @Column annotation members

Annotation member

Purpose

name

The name of the mapped column

table

The name of the table to which this column belongs

length

The length of a VARCHAR, CHAR, or VARBINARY column type

precision

The decimal digits of precision of a FLOAT, DECIMAL, NUMERIC, or TIME, or TIMESTAMP column type

scale

The scale of a DECIMAL or NUMERIC column type, the digits of precision that occur to the right of the decimal point

unique

Whether the column has a UNIQUE constraint

nullable

Whether the column has a NOT NULL constraint

insertable

Whether the column should appear in generated SQL INSERT statements

updatable

Whether the column should appear in generated SQL UPDATE statements

columnDefinition 💀

A DDL fragment that should be used to declare the column

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

We no longer recommend the use of columnDefinition since it results in unportable DDL. Hibernate has much better ways to customize the generated DDL using techniques that result in portable behavior across different databases.

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

Here we see four different ways to use the @Column annotation:

@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_来映射关联。

We don’t use @Column to map associations.

4.7. Mapping associations to foreign key columns

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

The @JoinColumn annotation is used to customize a foreign key column.

表 28. _@JoinColumn_注释成员

Table 28. @JoinColumn annotation members

Annotation member

Purpose

name

The name of the mapped foreign key column

table

The name of the table to which this column belongs

referencedColumnName

The name of the column to which the mapped foreign key column refers

unique

Whether the column has a UNIQUE constraint

nullable

Whether the column has a NOT NULL constraint

insertable

Whether the column should appear in generated SQL INSERT statements

updatable

Whether the column should appear in generated SQL UPDATE statements

columnDefinition 💀

A DDL fragment that should be used to declare the column

foreignKey

A @ForeignKey annotation specifying the name of the FOREIGN KEY constraint

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

A foreign key column doesn’t necessarily have to refer to the primary key of the referenced table. It’s quite acceptable for the foreign key to refer to any other unique key of the referenced entity, even to a unique key of a secondary table.

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

Here we see how to use @JoinColumn to define a @ManyToOne association mapping a foreign key column which refers to the @NaturalId of Book:

@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;

    ...
}

若此令人困惑:

In case this is confusing:

  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 生成。

Note that the foreignKey member is completely optional and only affects DDL generation.

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

If you don’t supply an explicit name using @ForeignKey, Hibernate will generate a quite ugly name. The reason for this is that the maximum length of foreign key names on some databases is extremely constrained, and we need to avoid collisions. To be fair, this is perfectly fine if you’re only using the generated DDL for testing.

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

For composite foreign keys we might have multiple @JoinColumn annotations:

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

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

    ...
}

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

If we need to specify the @ForeignKey, this starts to get a bit messy:

@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_注释:

For associations mapped to a @JoinTable, fetching the association requires two joins, and so we must declare the @JoinColumn_s inside the _@JoinTable annotation:

@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_成员仍然是可选的。

Again, the foreignKey member is optional.

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

For mapping a @OneToOne association to a primary key with @MapsId, Hibernate lets us use either @JoinColumn or @PrimaryKeyJoinColumn.

_@Entityclass Author { @Id Long id;

_@Entity class 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_是用于映射的专用注释:

The @PrimaryKeyJoinColumn is a special-purpose annotation for mapping:

  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_注释成员

Table 29. @PrimaryKeyJoinColumn annotation members

Annotation member

Purpose

name

The name of the mapped foreign key column

referencedColumnName

The name of the column to which the mapped foreign key column refers

columnDefinition 💀

A DDL fragment that should be used to declare the column

foreignKey

A @ForeignKey annotation specifying the name of the FOREIGN KEY constraint

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

When mapping a subclass table primary key, we place the @PrimaryKeyJoinColumn annotation on the entity class:

@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_注释内部:

But to map a secondary table primary key, the @PrimaryKeyJoinColumn annotation must occur inside the @SecondaryTable annotation:

@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 会自动选择其中一种类型。

Hibernate automatically adjusts the column type used in generated DDL based on the column length specified by the @Column annotation. So we don’t usually need to explicitly specify that a column should be of type TEXT or CLOB—or worry about the parade of TINYTEXT, MEDIUMTEXT, TEXT, LONGTEXT types on MySQL—because Hibernate will automatically select one of those types if required to accommodate a string of the length we specify.

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

The constant values defined in the class Length are very helpful here:

表 30. 预定义的列长度

Table 30. Predefined column lengths

Constant

Value

Description

DEFAULT

255

The default length of a VARCHAR or VARBINARY column when none is explicitly specified

LONG

32600

The largest column length for a VARCHAR or VARBINARY that is allowed on every database Hibernate supports

LONG16

32767

The maximum length that can be represented using 16 bits (but this length is too large for a VARCHAR or VARBINARY column on for some database)

LONG32

2147483647

The maximum length for a Java string

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

We can use these constants in the @Column annotation:

@Column(length=LONG)
String text;

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

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

This is usually all you need to do to make use of large object types in Hibernate.

4.10. LOBs

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

JPA provides a @Lob annotation which specifies that a field should be persisted as a BLOB or CLOB.

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

Hibernate interprets this annotation in what we think is the most reasonable way. In Hibernate, an attribute annotated @Lob will be written to JDBC using the setClob() or setBlob() method of PreparedStatement, and will be read from JDBC using the getClob() or getBlob() method of ResultSet.

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

Now, the use of these JDBC methods is usually unnecessary! JDBC drivers are perfectly capable of converting between String and CLOB or between byte[] and BLOB. So unless you specifically need to use these JDBC LOB APIs, you don’t need the @Lob annotation.

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

Instead, as we just saw in Column lengths and adaptive column types, all you need is to specify a large enough column length to accommodate the data you plan to write to that column.

你通常应该这样写:

You should usually write this:

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

而不是这样:

instead of this:

@Lob // almost always unnecessary
String text;

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

This is particularly true for PostgreSQL.

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

Unfortunately, the driver for PostgreSQL doesn’t allow BYTEA or TEXT columns to be read via the JDBC LOB APIs.

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

This limitation of the Postgres driver has resulted in a whole cottage industry of bloggers and stackoverflow question-answerers recommending convoluted ways to hack the Hibernate Dialect for Postgres to allow an attribute annotated @Lob to be written using setString() and read using getString().

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

But simply removing the @Lob annotation has exactly the same effect.

结论:

Conclusion:

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 类型的属性。

Finally, as an alternative, Hibernate lets you declare an attribute of type java.sql.Blob or java.sql.Clob.

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

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

The advantage is that a java.sql.Clob or java.sql.Blob can in principle index up to 263 characters or bytes, much more data than you can fit in a Java String or byte[] array (or in your computer).

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

To assign a value to these fields, we’ll need to use a LobHelper. We can get one from the Session:

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

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

In principle, the Blob and Clob objects provide efficient ways to read or stream LOB data from the server.

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

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

Of course, the behavior here depends very much on the JDBC driver, and so we really can’t promise that this is a sensible thing to do on your database.

4.11. Mapping embeddable types to UDTs or to JSON

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

There’s a couple of alternative ways to represent an embeddable type on the database side.

Embeddables as UDTs

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

First, a really nice option, at least in the case of Java record types, and for databases which support user-defined types (UDTs), is to define a UDT which represents the record type. Hibernate 6 makes this really easy. Just annotate the record type, or the attribute which holds a reference to it, with the new @Struct annotation:

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

这会导致以下 UDT:

This results in the following UDT:

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

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

And the name column of the Author table will have the type PersonName.

Embeddables to JSON

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

A second option that’s available is to map the embeddable type to a JSON (or JSONB) column. Now, this isn’t something we would exactly recommend if you’re defining a data model from scratch, but it’s at least useful for mapping pre-existing tables with JSON-typed columns. Since embeddable types are nestable, we can map some JSON formats this way, and even query JSON properties using HQL.

此时,不支持 JSON 数组!

At this time, JSON arrays are not supported!

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

To map an attribute of embeddable type to JSON, we must annotate the attribute @JdbcTypeCode(SqlTypes.JSON), instead of annotating the embeddable type. But the embeddable type Name should still be annotated @Embeddable if we want to query its attributes using HQL.

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

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

We also need to add Jackson or an implementation of JSONB—for example, Yasson—to our runtime classpath. To use Jackson we could add this line to our Gradle build:

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

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

Now the name column of the Author table will have the type jsonb, and Hibernate will automatically use Jackson to serialize a Name to and from JSON format.

4.12. Summary of SQL column type mappings

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

So, as we’ve seen, there are quite a few annotations that affect the mapping of Java types to SQL column types in DDL. Here we summarize the ones we’ve just seen in the second half of this chapter, along with some we already mentioned in earlier chapters.

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

Table 31. Annotations for mapping SQL column types

Annotation

Interpretation

@Enumerated

Specify how an enum type should be persisted

@Nationalized

Use a nationalized character type: NCHAR, NVARCHAR, or NCLOB

@Lob 💀

Use JDBC LOB APIs to read and write the annotated attribute

@Array

Map a collection to a SQL ARRAY type of the specified length

@Struct

Map an embeddable to a SQL UDT with the given name

@TimeZoneStorage

Specify how the time zone information should be persisted

@JdbcType or @JdbcTypeCode

Use an implementation of JdbcType to map an arbitrary SQL type

@Collate

Specify a collation for a column

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

In addition, there are some configuration properties which have a global affect on how basic types map to SQL column types:

表 32.类型映射设置

Table 32. Type mapping settings

Configuration property name

Purpose

hibernate.use_nationalized_character_data

Enable use of nationalized character types by default

hibernate.type.preferred_boolean_jdbc_type

Specify the default SQL column type for mapping boolean

hibernate.type.preferred_uuid_jdbc_type

Specify the default SQL column type for mapping UUID

hibernate.type.preferred_duration_jdbc_type

Specify the default SQL column type for mapping Duration

hibernate.type.preferred_instant_jdbc_type

Specify the default SQL column type for mapping Instant

hibernate.timezone.default_storage

Specify the default strategy for storing time zone information

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

These are global settings and thus quite clumsy. We recommend against messing with any of these settings unless you have a really good reason for it.

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

There’s one more topic we would like to cover in this chapter.

4.13. Mapping to formulas

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

Hibernate lets us map an attribute of an entity to a SQL formula involving columns of the mapped table. Thus, the attribute is a sort of "derived" value.

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

Table 33. Annotations for mapping formulas

Annotation

Purpose

@Formula

Map an attribute to a SQL formula

@JoinFormula

Map an association to a SQL formula

@DiscriminatorFormula

Use a SQL formula as the discriminator in single table inheritance.

例如:

For example:

@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 退化的案例。

An entity has a derived identity if it inherits part of its primary key from an associated "parent" entity. We’ve already met a kind of degenerate case of derived identity when we talked about one-to-one associations with a shared primary key.

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

But a @ManyToOne association may also form part of a derived identity. That is to say, there could be a foreign key column or columns included as part of the composite primary key. There’s three different ways to represent this situation on the Java side of things:

  1. using @IdClass without @MapsId,

  2. using @IdClass with @MapsId, or

  3. using @EmbeddedId with @MapsId.

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

Let’s suppose we have a Parent entity class defined as follows:

@Entity
class Parent {
    @Id
    Long parentId;

    ...
}

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

The parentId field holds the primary key of the Parent table, which will also form part of the composite primary key of every Child belonging to the Parent.

First way

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

In the first, slightly simpler approach, we define an @IdClass to represent the primary key of Child:

class DerivedId {
    Long parent;
    String childId;

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

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

And a Child entity class with a @ManyToOne association annotated @Id:

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

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

    ...
}

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

Then the primary key of the Child table comprises the columns (childId,parentId).

Second way

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

This is fine, but sometimes it’s nice to have a field for each element of the primary key. We may use the @MapsId annotation we met earlier:

@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 属性。

We’re using the approach we saw previously to refer to the parentId property of Child in a typesafe way.

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

Note that we must place column mapping information on the association annotated @MapsId, not on the @Id field.

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

We must slightly modify our @IdClass so that field names align:

class DerivedId {
    Long parentId;
    String childId;

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

Third way

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

The third alternative is to redefine our @IdClass as an @Embeddable. We don’t actually need to change the DerivedId class, but we do need to add the annotation.

@Embeddable
class DerivedId {
    Long parentId;
    String childId;

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

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

Then we may use @EmbeddedId in Child:

@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 可能稍微枯燥一些。

The choice between @IdClass and @EmbeddedId boils down to taste. The @EmbeddedId is perhaps a little DRYer.

4.15. Adding constraints

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

Database constraints are important. Even if you’re sure that your program has no bugs 🧐, it’s probably not the only program with access to the database. Constraints help ensure that different programs (and human administrators) play nicely with each other.

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

Hibernate adds certain constraints to generated DDL automatically: primary key constraints, foreign key constraints, and some unique constraints. But it’s common to need to:

  1. add additional unique constraints,

  2. add check constraints, or

  3. customize the name of a foreign key constraint.

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

We’ve already seen how to use @ForeignKey to specify the name of a foreign key constraint.

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

There’s two ways to add a unique constraint to a table:

  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 { ... }

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

This annotation looks a bit ugly perhaps, but it’s actually useful even as documentation.

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

The @Check annotation adds a check constraint to the table.

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

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

The @Check annotation is commonly used at the field level:

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