Hibernate ORM 中文操作指南

8. Advanced Topics

在本简介的最后一章,我们转向了一些实际上不属于简介中内容的主题。在这里,我们考虑一些问题和解决方案,如果您是 Hibernate 新手,那么您可能不会立即遇到这些问题。但是,我们确实希望您了解 about 这些问题,这样一来当时候一到,您就知道该使用哪种工具。

In the last chapter of this Introduction, we turn to some topics that don’t really belong in an introduction. Here we consider some problems, and solutions, that you’re probably not going to run into immediately if you’re new to Hibernate. But we do want you to know about them, so that when the time comes, you’ll know what tool to reach for.

8.1. Filters

Filters 是 Hibernate 里最友好且未被充分利用的功能之一,我们对此深感自豪。过滤器是对数据的一种命名、全局定义且带参数的限制,而在给定的会话中可以看到这些数据。

Filters are one of the nicest and under-usedest features of Hibernate, and we’re quite proud of them. A filter is a named, globally-defined, parameterized restriction on the data that is visible in a given session.

定义良好的过滤器的示例可能包括:

Examples of well-defined filters might include:

  1. a filter that restricts the data visible to a given user according to row-level permissions,

  2. a filter which hides data which has been soft-deleted,

  3. in a versioned database, a filter that displays versions which were current at a given instant in the past, or

  4. a filter that restricts to data associated with a certain geographical region.

过滤器必须在某个地方声明。对于 @FilterDef 来说,包描述符就像任何地方一样好:

A filter must be declared somewhere. A package descriptor is as good a place as any for a @FilterDef:

@FilterDef(name = "ByRegion",
           parameters = @ParamDef(name = "region", type = String.class))
package org.hibernate.example;

此过滤器有一个参数。原则上,更复杂的过滤器可能有许多参数,尽管我们承认这种情况很罕见。

This filter has one parameter. Fancier filters might in principle have multiple parameters, though we admit this must be quite rare.

如果您向包描述符添加注释,并且使用 Configuration 配置 Hibernate,请确保调用 Configuration.addPackage() 使 Hibernate 知道包描述符带有注释。

If you add annotations to a package descriptor, and you’re using Configuration to configure Hibernate, make sure you call Configuration.addPackage() to let Hibernate know that the package descriptor is annotated.

Typically,但不一定是 @FilterDef,指定了默认限制:

Typically, but not necessarily, a @FilterDef specifies a default restriction:

@FilterDef(name = "ByRegion",
           parameters = @ParamDef(name = "region", type = String.class),
           defaultCondition = "region = :region")
package org.hibernate.example;

这个限制必须包含对过滤器参数的引用,而引用的指定方式使用的是带名称参数的常规语法。

The restriction must contain a reference to the parameter of the filter, specified using the usual syntax for named parameters.

受过滤器影响的任何实体或集合都必须用 @Filter 注释:

Any entity or collection which is affected by a filter must be annotated @Filter:

@Entity
@Filter(name = example_.BY_REGION)
class User {

    @Id String username;

    String region;

    ...
}

这里,像往常一样,example.BY_REGION_ 由元模型生成器生成,它只是一个带有值 "ByRegion" 的常量。

Here, as usual, example.BY_REGION_ is generated by the Metamodel Generator, and is just a constant with the value "ByRegion".

如果 @Filter 注释没有明确指定限制,那么 @FilterDef 给出的默认限制将应用于实体。但是,实体可以自由地覆盖默认条件。

If the @Filter annotation does not explicitly specify a restriction, the default restriction given by the @FilterDef will be applied to the entity. But an entity is free to override the default condition.

@Entity
@Filter(name = example_.FILTER_BY_REGION, condition = "name = :region")
class Region {

    @Id String name;

    ...
}

请注意,由 conditiondefaultCondition 指定的限制是一个本机 SQL 表达式。

Note that the restriction specified by the condition or defaultCondition is a native SQL expression.

表 59. 用于定义过滤器的注释

Table 59. Annotations for defining filters

Annotation

Purpose

@FilterDef

Defines a filter and declares its name (exactly one per filter)

@Filter

Specifies how a filter applies to a given entity or collection (many per filter)

过滤器 condition 不得指定与其他表的连接,但可以包含一个子查询。

A filter condition may not specify joins to other tables, but it may contain a subquery.

@Filter(name="notDeleted" condition="(select r.deletionTimestamp from Record r where r.id = record_id) is not null") @Filter(name="notDeleted" condition="(select r.deletionTimestamp from Record r where r.id = record_id) is not null") 只有此示例中的不合格列名(如 record_id )才被解释为属于过滤实体的表。

@Filter(name="notDeleted" condition="(select r.deletionTimestamp from Record r where r.id = record_id) is not null") @Filter(name="notDeleted" condition="(select r.deletionTimestamp from Record r where r.id = record_id) is not null") Only unqualified column names like record_id in this example are interpreted as belonging to the table of the filtered entity.

默认情况下,每个新会话都随每个过滤器禁用附带提供。可以通过调用 enableFilter() 并使用 Filter 的返回实例将参数分配给过滤器的参数,在给定会话中显式启用过滤器。你应该在会话的 start 正确执行此操作。

By default, a new session comes with every filter disabled. A filter may be explicitly enabled in a given session by calling enableFilter() and assigning arguments to the parameters of the filter using the returned instance of Filter. You should do this right at the start of the session.

sessionFactory.inTransaction(session -> {
    session.enableFilter(example_.FILTER_BY_REGION)
        .setParameter("region", "es")
        .validate();

    ...
});

现在,在会话中执行的任何查询都将应用过滤器限制。注释 @Filter 的集合也将正确地过滤其成员。

Now, any queries executed within the session will have the filter restriction applied. Collections annotated @Filter will also have their members correctly filtered.

另一方面,过滤器不应用于 @ManyToOne 关联,也不应用于 find()。这完全是设计使然,绝不是缺陷。

On the other hand, filters are not applied to @ManyToOne associations, nor to find(). This is completely by design and is not in any way a bug.

在给定的会话中,可能会启用多个过滤器。

More than one filter may be enabled in a given session.

或者,自 Hibernate 6.5 起,可以在每个会话中将过滤器声明为 autoEnabled。在这种情况下,必须从 Supplier 中获取过滤器参数的自变量。

Alternatively, since Hibernate 6.5, a filter may be declared as autoEnabled in every session. In this case, the argument to a filter parameter must be obtained from a Supplier.

@FilterDef(name = "ByRegion",
           autoEnabled = true,
           parameters = @ParamDef(name = "region", type = String.class,
                                  resolver = RegionSupplier.class),
           defaultCondition = "region = :region")
package org.hibernate.example;

对于声明为 autoEnabled = true 的过滤器,没有必要调用 enableFilter()

It’s not necessary to call enableFilter() for a filter declared autoEnabled = true.

当我们只需要按无参数的静态条件过滤行时,我们不需要过滤器,因为 @SQLRestriction 提供了一种更简单的方法。

When we only need to filter rows by a static condition with no parameters, we don’t need a filter, since @SQLRestriction provides a much simpler way to do that.

我们已经提到可以使用过滤器来实现版本控制并提供 historical 的数据视图。作为如此通用目的的构造,过滤器在此提供了很大的灵活性。但如果您想要更专注/有主见地解决此问题,您一定应该查看 Envers

We’ve mentioned that a filter can be used to implement versioning, and to provide historical views of the data. Being such a general-purpose construct, filters provide a lot of flexibility here. But if you’re after a more focused/opinionated solution to this problem, you should definitely check out Envers.

历史上,过滤器经常用于实现软删除。但是,自 6.4 以来,Hibernate 现在内置了软删除。

Historically, filters where often used to implement soft-delete. But, since 6.4, Hibernate now comes with soft-delete built in.

8.2. Soft-delete

即使我们不需要完整的历史版本控制,我们也经常更愿意使用 SQL update 将一行标记为过时来“删除”它,而不是执行实际的 SQL delete 并完全从数据库中删除该行。

Even when we don’t need complete historical versioning, we often prefer to "delete" a row by marking it as obsolete using a SQL update, rather than by executing an actual SQL delete and removing the row from the database completely.

@SoftDelete 注释控制此操作的工作方式:

The @SoftDelete annotation controls how this works:

@Entity
@SoftDelete(columnName = "deleted",
            converter = TrueFalseConverter.class)
class Draft {

    ...
}

columnName 指定用于保存删除状态的列, converter 负责将 Java Boolean 转换为该列的类型。在此示例中, TrueFalseConverter 将该列最初设置为字符 'F' ,在删除行时将其设置为 'T' 。此处可以使用 Java Boolean 类型的任何 JPA AttributeConverter 。内置选项包括 NumericBooleanConverterYesNoConverter

The columnName specifies a column holding the deletion status, and the converter is responsible for converting a Java Boolean to the type of that column. In this example, TrueFalseConverter sets the column to the character 'F' initially, and to 'T' when the row is deleted. Any JPA AttributeConverter for the Java Boolean type may be used here. Built-in options include NumericBooleanConverter and YesNoConverter.

User Guide中提供了有关软删除的更多信息。

Much more information about soft delete is available in the User Guide.

您可以使用过滤器的另一个特性是多重租户,但现在不需要了。

Another feature that you could use filters for, but now don’t need to, is multi-tenancy.

8.3. Multi-tenancy

multi-tenant 数据库是数据按 tenant 分离的数据库。我们不必实际确定此处“租户”真正表示什么;我们在此抽象级别关心的是,可以通过唯一标识符来区分每个租户。每个会话中均有一个明确定义的 current tenant

A multi-tenant database is one where the data is segregated by tenant. We don’t need to actually define what a "tenant" really represents here; all we care about at this level of abstraction is that each tenant may be distinguished by a unique identifier. And that there’s a well-defined current tenant in each session.

我们在打开会话时可以指定当前租户:

We may specify the current tenant when we open a session:

var session =
        sessionFactory.withOptions()
            .tenantIdentifier(tenantId)
            .openSession();

或者,在使用 JPA 标准 API 时:

Or, when using JPA-standard APIs:

var entityManager =
        entityManagerFactory.createEntityManager(Map.of(HibernateHints.HINT_TENANT_ID, tenantId));

然而,由于我们通常没有这种级别的会话创建控制权,因此更常见的是为 Hibernate 提供 CurrentTenantIdentifierResolver 的实现。

However, since we often don’t have this level of control over creation of the session, it’s more common to supply an implementation of CurrentTenantIdentifierResolver to Hibernate.

实现多重租户的常用方法有三种:

There are three common ways to implement multi-tenancy:

  • each tenant has its own database,

  • each tenant has its own schema, or

  • tenants share tables in a single schema, and rows are tagged with the tenant id.

从 Hibernate 的角度来看,前两个选项差别不大。Hibernate 需要获取 JDBC 连接,并在当前租户拥有的数据库和架构上获得权限。

From the point of view of Hibernate, there’s little difference between the first two options. Hibernate will need to obtain a JDBC connection with permissions on the database and schema owned by the current tenant.

因此,我们必须实现一个 MultiTenantConnectionProvider ,承担此责任:

Therefore, we must implement a MultiTenantConnectionProvider which takes on this responsibility:

  1. from time to time, Hibernate will ask for a connection, passing the id of the current tenant, and then we must create an appropriate connection or obtain one from a pool, and return it to Hibernate, and

  2. later, Hibernate will release the connection and ask us to destroy it or return it to the appropriate pool.

第三个选择有很大不同。在这种情况下,我们不需要 MultiTenantConnectionProvider,但是我们需要一个专用的列,由我们的每个实体映射的租户 ID 保存。

The third option is quite different. In this case we don’t need a MultiTenantConnectionProvider, but we will need a dedicated column holding the tenant id mapped by each of our entities.

@Entity
class Account {
    @Id String id;
    @TenantId String tenantId;

    ...
}

@TenantId 注解用于指示实体的属性,该属性持有租户 ID。在给定的会话中,我们的数据得到了自动筛选,以便仅在该会话中将标记了当前租户的租户 ID 的行显示出来。

The @TenantId annotation is used to indicate an attribute of an entity which holds the tenant id. Within a given session, our data is automatically filtered so that only rows tagged with the tenant id of the current tenant are visible in that session.

本地 SQL 查询会自动按租户 ID 进行 not 过滤;您必须自己执行该部分。

Native SQL queries are not automatically filtered by tenant id; you’ll have to do that part yourself.

为了使用多租户,我们通常需要设置以下至少一个配置属性:

To make use of multi-tenancy, we’ll usually need to set at least one of these configuration properties:

表 60. 多重租户配置

Table 60. Multi-tenancy configuration

Configuration property name

Purpose

hibernate.tenant_identifier_resolver

Specifies the CurrentTenantIdentifierResolver

hibernate.multi_tenant_connection_provider

Specifies the MultiTenantConnectionProvider

User Guide中可能会找到对多租户的更长的讨论。

A longer discussion of multi-tenancy may be found in the User Guide.

8.4. Using custom-written SQL

我们已经讨论过如何运行 queries written in SQL,但有时这还不够。有时(但比您想象的要少得多)我们希望自定义 Hibernate 用于对实体或集合执行基本 CRUD 操作的 SQL。

We’ve already discussed how to run queries written in SQL, but occasionally that’s not enough. Sometimes—but much less often than you might expect—we would like to customize the SQL used by Hibernate to perform basic CRUD operations for an entity or collection.

为此,我们可以使用 @SQLInsert 等:

For this we can use @SQLInsert and friends:

@Entity
@SQLInsert(sql = "insert into person (name, id, valid) values (?, ?, true)",
           verify = Expectation.RowCount.class)
@SQLUpdate(sql = "update person set name = ? where id = ?")
@SQLDelete(sql = "update person set valid = false where id = ?")
@SQLSelect(sql = "select id, name from person where id = ? and valid = true")
public static class Person { ... }

表 61. 用于重写生成的 SQL 的注释

Table 61. Annotations for overriding generated SQL

Annotation

Purpose

@SQLSelect

Overrides a generated SQL select statement

@SQLInsert

Overrides a generated SQL insert statement

@SQLUpdate

Overrides a generated SQL update statement

@SQDelete

Overrides a generated SQL delete statement a single rows

@SQDeleteAll

Overrides a generated SQL delete statement for multiple rows

@SQLRestriction

Adds a restriction to generated SQL

@SQLOrder

Adds an ordering to generated SQL

如果自定义 SQL 应通过 CallableStatement 执行,只需指定 callable=true

If the custom SQL should be executed via a CallableStatement, just specify callable=true.

这些注释之一指定的任何 SQL 语句必须具有 Hibernate 所期望的 JDBC 参数数量,也就是说,对于实体映射的每一列,必须有一个,并且按照 Hibernate 所期望的确切顺序进行。尤其是,主键列必须放在最后。

Any SQL statement specified by one of these annotations must have exactly the number of JDBC parameters that Hibernate expects, that is, one for each column mapped by the entity, in the exact order Hibernate expects. In particular, the primary key columns must come last.

不过,@Column 注释确实在这里提供了一些灵活性:

However, the @Column annotation does lend some flexibility here:

  1. if a column should not be written as part of the custom insert statement, and has no corresponding JDBC parameter in the custom SQL, map it @Column(insertable=false), or

  2. if a column should not be written as part of the custom update statement, and has no corresponding JDBC parameter in the custom SQL, map it @Column(updatable=false).

这些注释的 verify 成员指定了一个实现 Expectation 的类,该类允许针对通过 JDBC 执行的操作成功检查自定义的逻辑。有三种内置实现:

The verify member of these annotations specifies a class implementing Expectation, allowing customized logic for checking the success of an operation executed via JDBC. There are three built-in implementations:

  1. Expectation.None, which performs no checks,

  2. Expectation.RowCount, which is what Hibernate usually uses when executing its own generated SQL,

  3. and Expectation.OutParameter, which is useful for checking an output parameter of a stored procedure.

如果这些选项都不合适,您可以编写自己的 Expectation 实现。

You can write your own implementation of Expectation if none of these options is suitable.

如果您需要自定义 SQL,但针对的是多种 SQL 方言,则可以使用 DialectOverride 中定义的注解。例如,此注解使我们仅针对 PostgreSQL 覆盖自定义 insert 语句:

If you need custom SQL, but are targeting multiple dialects of SQL, you can use the annotations defined in DialectOverride. For example, this annotation lets us override the custom insert statement just for PostgreSQL:

@DialectOverride.SQLInsert(dialect = PostgreSQLDialect.class, override = @SQLInsert(sql="insert into person (name,id) values (?,gen_random_uuid())")) @DialectOverride.SQLInsert(dialect = PostgreSQLDialect.class, override = @SQLInsert(sql="insert into person (name,id) values (?,gen_random_uuid())")) 甚至还可以针对数据库的特定 versions 覆盖自定义 SQL。

@DialectOverride.SQLInsert(dialect = PostgreSQLDialect.class, override = @SQLInsert(sql="insert into person (name,id) values (?,gen_random_uuid())")) @DialectOverride.SQLInsert(dialect = PostgreSQLDialect.class, override = @SQLInsert(sql="insert into person (name,id) values (?,gen_random_uuid())")) It’s even possible to override the custom SQL for specific versions of a database.

有时自定义 insertupdate 语句会在数据库中执行语句时为映射字段分配一个值。例如,可以通过调用 SQL 函数获取值:

Sometimes a custom insert or update statement assigns a value to a mapped column which is calculated when the statement is executed on the database. For example, the value might be obtained by calling a SQL function:

@SQLInsert(sql = "insert into person (name, id) values (?, gen_random_uuid())")

但是,表示正在插入或更新的行实体实例不会自动填充为该值。这样,我们的持久化上下文会与数据库失去同步。在这种情况中,我们可能使用 @Generated 注释告诉 Hibernate 在每次 insertupdate 之后重新读取实体状态。

But the entity instance which represents the row being inserted or updated won’t be automatically populated with that value. And so our persistence context loses synchronization with the database. In situations like this, we may use the @Generated annotation to tell Hibernate to reread the state of the entity after each insert or update.

8.5. Handling database-generated columns

有时,数据库中发生的一些事件会为列值赋值或对其进行变更,而这些事件对 Hibernate 来说是不可见的。例如:

Sometimes, a column value is assigned or mutated by events that happen in the database, and aren’t visible to Hibernate. For example:

  1. a table might have a column value populated by a trigger,

  2. a mapped column might have a default value defined in DDL, or

  3. a custom SQL insert or update statement might assign a value to a mapped column, as we saw in the previous subsection.

应对这种情况的一个方法是在适当的时机显式调用 refresh(),强制会话重新读取实体状态。但这很烦人。

One way to deal with this situation is to explicitly call refresh() at appropriate moments, forcing the session to reread the state of the entity. But this is annoying.

@Generated 注解让我们不必显式调用 refresh() 。它指定注解实体属性的值由数据库生成,并且应使用 SQL returning 子句或在生成后使用单独的 select 自动检索生成的值。

The @Generated annotation relieves us of the burden of explicitly calling refresh(). It specifies that the value of the annotated entity attribute is generated by the database, and that the generated value should be automatically retrieved using a SQL returning clause, or separate select after it is generated.

一个有用的示例是以下映射:

A useful example is the following mapping:

@Entity
class Entity {
    @Generated @Id
    @ColumnDefault("gen_random_uuid()")
    UUID id;
}

生成的 DDL 如下:

The generated DDL is:

create table Entity (
    id uuid default gen_random_uuid() not null,
    primary key (uuid)
)

因此,此处 _id_的值是由列默认子句通过调用 PostgreSQL 函数 _gen_random_uuid()_定义的。

So here the value of id is defined by the column default clause, by calling the PostgreSQL function gen_random_uuid().

当某列值在更新期间生成时,请使用 @Generated(event=UPDATE)。当某值由插入和更新都生成时,请使用 @Generated(event={INSERT,UPDATE})

When a column value is generated during updates, use @Generated(event=UPDATE). When a value is generated by both inserts and updates, use @Generated(event={INSERT,UPDATE}).

对于应使用 SQL generated always as 子句生成的列,请使用 @GeneratedColumn 注解,以便 Hibernate 自动生成正确的 DDL。

For columns which should be generated using a SQL generated always as clause, prefer the @GeneratedColumn annotation, so that Hibernate automatically generates the correct DDL.

实际上,@Generated@GeneratedColumn 注释是根据一个更通用的可用户扩展的框架进行定义的,该框架用于处理在 Java 中或由数据库生成的属性值。所以,让我们降低一个层级,看看它是如何工作的。

Actually, the @Generated and @GeneratedColumn annotations are defined in terms of a more generic and user-extensible framework for handling attribute values generated in Java, or by the database. So let’s drop down a layer, and see how that works.

8.6. User-defined generators

JPA 并未定义一种扩展 ID 生成策略集的标准方式,但 Hibernate 定义了:

JPA doesn’t define a standard way to extend the set of id generation strategies, but Hibernate does:

  1. the Generator hierarchy of interfaces in the package org.hibernate.generator lets you define new generators, and

  2. the @IdGeneratorType meta-annotation from the package org.hibernate.annotations lets you write an annotation which associates a Generator type with identifier attributes.

此外, @ValueGenerationType 元注解允许您编写将 Generator 类型与非 @Id 属性关联起来的注解。

Furthermore, the @ValueGenerationType meta-annotation lets you write an annotation which associates a Generator type with a non-@Id attribute.

这些 API 在 Hibernate 6 中是新的,并且取代了旧版本 Hibernate 中经典的 IdentifierGenerator 接口和 @GenericGenerator 注释。但是,较旧的 API 仍然可用,为旧版本 Hibernate 编写的自定义 _IdentifierGenerator_s 在 Hibernate 6 中仍然有效。

These APIs are new in Hibernate 6, and supersede the classic IdentifierGenerator interface and @GenericGenerator annotation from older versions of Hibernate. However, the older APIs are still available and custom _IdentifierGenerator_s written for older versions of Hibernate continue to work in Hibernate 6.

Hibernate 有一系列内置生成器,它们是根据这个新框架定义的。

Hibernate has a range of built-in generators which are defined in terms of this new framework.

表 62. 内置生成器

Table 62. Built-in generators

Annotation

Implementation

Purpose

@Generated

GeneratedGeneration

Generically handles database-generated values

@GeneratedColumn

GeneratedAlwaysGeneration

Handles values generated using generated always

@CurrentTimestamp

CurrentTimestampGeneration

Generic support for database or in-memory generation of creation or update timestamps

@CreationTimestamp

CurrentTimestampGeneration

A timestamp generated when an entity is first made persistent

@UpdateTimestamp

CurrentTimestampGeneration

A timestamp generated when an entity is made persistent, and regenerated every time the entity is modified

@UuidGenerator

UuidGenerator

A more flexible generator for RFC 4122 UUIDs

此外,对 JPA 标准 ID 生成策略的支持也根据此框架来定义。

Furthermore, support for JPA’s standard id generation strategies is also defined in terms of this framework.

作为一个例子,让我们看看 @UuidGenerator 是如何定义的:

As an example, let’s look at how @UuidGenerator is defined:

@IdGeneratorType(org.hibernate.id.uuid.UuidGenerator.class)
@ValueGenerationType(generatedBy = org.hibernate.id.uuid.UuidGenerator.class)
@Retention(RUNTIME)
@Target({ FIELD, METHOD })
public @interface UuidGenerator { ... }

@UuidGenerator 同时通过元注解 @IdGeneratorType@ValueGenerationType 进行元注解,因为它可能用来生成 ID 和常规属性的值。无论哪种情况, this Generator class 都完成了艰苦的工作:

@UuidGenerator is meta-annotated both @IdGeneratorType and @ValueGenerationType because it may be used to generate both ids and values of regular attributes. Either way, this Generator class does the hard work:

public class UuidGenerator
        // this generator produced values before SQL is executed
        implements BeforeExecutionGenerator {

    // constructors accept an instance of the @UuidGenerator
    // annotation, allowing the generator to be "configured"

    // called to create an id generator
    public UuidGenerator(
            org.hibernate.annotations.UuidGenerator config,
            Member idMember,
            GeneratorCreationContext creationContext) {
        this(config, idMember);
    }

    // called to create a generator for a regular attribute
    public UuidGenerator(
            org.hibernate.annotations.UuidGenerator config,
            Member member,
            GeneratorCreationContext creationContext) {
        this(config, idMember);
    }

    ...

    @Override
    public EnumSet<EventType> getEventTypes() {
        // UUIDs are only assigned on insert, and never regenerated
        return INSERT_ONLY;
    }

    @Override
    public Object generate(SharedSessionContractImplementor session, Object owner, Object currentValue, EventType eventType) {
        // actually generate a UUID and transform it to the required type
        return valueTransformer.transform( generator.generateUuid( session ) );
    }
}

您可以通过 @IdGeneratorTypeorg.hibernate.generator 的 Javadoc 了解有关自定义生成器的更多信息。

You can find out more about custom generators from the Javadoc for @IdGeneratorType and for org.hibernate.generator.

8.7. Naming strategies

在使用预先存在的关联模式时,通常会发现模式中使用的列和表命名约定与 Java 的命名约定不匹配。

When working with a pre-existing relational schema, it’s usual to find that the column and table naming conventions used in the schema don’t match Java’s naming conventions.

当然,@Table@Column 注解允许我们显式指定映射的表或列名称。但我们更希望避免在整个领域模型中分散这些注解。

Of course, the @Table and @Column annotations let us explicitly specify a mapped table or column name. But we would prefer to avoid scattering these annotations across our whole domain model.

因此,Hibernate 允许我们定义 Java 命名约定与关联模式命名约定之间的映射。这样的一个映射称为 naming strategy

Therefore, Hibernate lets us define a mapping between Java naming conventions, and the naming conventions of the relational schema. Such a mapping is called a naming strategy.

首先,我们需要了解 Hibernate 如何分配和处理名称。

First, we need to understand how Hibernate assigns and processes names.

  1. Logical naming is the process of applying naming rules to determine the logical names of objects which were not explicitly assigned names in the O/R mapping. That is, when there’s no @Table or @Column annotation.

  2. Physical naming is the process of applying additional rules to transform a logical name into an actual "physical" name that will be used in the database. For example, the rules might include things like using standardized abbreviations, or trimming the length of identifiers.

因此,命名策略有两种不同的目的,具有略微不同的责任。Hibernate 附带了这些接口的默认实现:

Thus, there’s two flavors of naming strategy, with slightly different responsibilities. Hibernate comes with default implementations of these interfaces:

Flavor

Default implementation

An ImplicitNamingStrategy is responsible for assigning a logical name when none is specified by an annotation

A default strategy which implements the rules defined by JPA

A PhysicalNamingStrategy is responsible for transforming a logical name and producing the name used in the database

A trivial implementation which does no processing

碰巧我们不太喜欢 JPA 定义的命名规则,该规则指定应使用下划线连接混合大小写和驼峰式标识符。我们相信,您肯定能想出一个比这更好的 ImplicitNamingStrategy!(提示:它应该始终生成合法的混合大小写标识符。)

We happen to not much like the naming rules defined by JPA, which specify that mixed case and camel case identifiers should be concatenated using underscores. We bet you could easily come up with a much better ImplicitNamingStrategy than that! (Hint: it should always produce legit mixed case identifiers.)

热门 PhysicalNamingStrategy 生成蛇形大小写标识符。

A popular PhysicalNamingStrategy produces snake case identifiers.

可以使用我们在 Minimizing repetitive mapping information 中提到的配置属性启用自定义命名策略,我们之前已经简要地解释过。

Custom naming strategies may be enabled using the configuration properties we already mentioned without much explanation back in Minimizing repetitive mapping information.

表 63. 命名策略配置

Table 63. Naming strategy configuration

Configuration property name

Purpose

hibernate.implicit_naming_strategy

Specifies the ImplicitNamingStrategy

hibernate.physical_naming_strategy

Specifies the PhysicalNamingStrategy

8.8. Spatial datatypes

Hibernate Spatial 使用一组针对 OGC 空间类型的 Java 映射来扩充 built-in basic types

Hibernate Spatial augments the built-in basic types with a set of Java mappings for OGC spatial types.

  1. Geolatte-geom defines a set of Java types implementing the OGC spatial types, and codecs for translating to and from database-native spatial datatypes.

  2. Hibernate Spatial itself supplies integration with Hibernate.

要使用 Hibernate Spatial,必须将其添加为依赖项,如 Optional dependencies 中所述。

To use Hibernate Spatial, we must add it as a dependency, as described in Optional dependencies.

然后我们可以在实体中立即使用 Geolatte-geom 和 JTS 类型。无需任何特殊的注解:

Then we may immediately use Geolatte-geom and JTS types in our entities. No special annotations are needed:

import org.locationtech.jts.geom.Point;
import jakarta.persistence.*;

@Entity
class Event {
    Event() {}

    Event(String name, Point location) {
        this.name = name;
        this.location = location;
    }

    @Id @GeneratedValue
    Long id;

    String name;

    Point location;

}

生成的 DDL 使用 geometry 作为 location 映射的列类型:

The generated DDL uses geometry as the type of the column mapped by location:

create table Event (
    id bigint not null,
    location geometry,
    name varchar(255),
    primary key (id)
)

有了 Hibernate 空间,我们可以像处理任何内置基本属性类型一样处理空间类型。

Hibernate Spatial lets us work with spatial types just as we would with any of the built-in basic attribute types.

var geometryFactory = new GeometryFactory();
...

Point point = geometryFactory.createPoint(new Coordinate(10, 5));
session.persist(new Event("Hibernate ORM presentation", point));

但是,它的强大之处在于我们可以编写一些非常花哨的查询,其中涉及空间类型的函数:

But what makes this powerful is that we may write some very fancy queries involving functions of spatial types:

Polygon triangle =
        geometryFactory.createPolygon(
                new Coordinate[] {
                        new Coordinate(9, 4),
                        new Coordinate(11, 4),
                        new Coordinate(11, 20),
                        new Coordinate(9, 4)
                }
        );
Point event =
        session.createQuery("select location from Event where within(location, :zone) = true", Point.class)
                .setParameter("zone", triangle)
                .getSingleResult();

在此,within()_是 OpenGIS 规范定义的用于测试空间关系的函数之一。其他此类函数包括 _touches()intersects()distance()、_boundary()_等。并非每个空间关系函数都受每个数据库支持。可以在 User Guide中找到空间关系函数支持的矩阵。

Here, within() is one of the functions for testing spatial relations defined by the OpenGIS specification. Other such functions include touches(), intersects(), distance(), boundary(), etc. Not every spatial relation function is supported on every database. A matrix of support for spatial relation functions may be found in the User Guide.

如果要在 H2 上试用空间函数,请先运行以下代码:

If you want to play with spatial functions on H2, run the following code first:

sessionFactory.inTransaction(session → { session.doWork(connection → { try (var statement = connection.createStatement()) { statement.execute("create alias if not exists h2gis_spatial for \"org.h2gis.functions.factory.H2GISFunctions.load\""); statement.execute("call h2gis_spatial()"); } }); } ); sessionFactory.inTransaction(session → { session.doWork(connection → { try (var statement = connection.createStatement()) { statement.execute("create alias if not exists h2gis_spatial for \"org.h2gis.functions.factory.H2GISFunctions.load\""); statement.execute("call h2gis_spatial()"); } }); } );

sessionFactory.inTransaction(session → { session.doWork(connection → { try (var statement = connection.createStatement()) { statement.execute("create alias if not exists h2gis_spatial for \"org.h2gis.functions.factory.H2GISFunctions.load\""); statement.execute("call h2gis_spatial()"); } }); } ); sessionFactory.inTransaction(session → { session.doWork(connection → { try (var statement = connection.createStatement()) { statement.execute("create alias if not exists h2gis_spatial for \"org.h2gis.functions.factory.H2GISFunctions.load\""); statement.execute("call h2gis_spatial()"); } }); } );

8.9. Ordered and sorted collections and map keys

Java 列表和映射无法非常自然地映射到表之间的外键关系,因此我们倾向于避免使用它们来表示实体类之间的关联。但是如果你觉得 really 需要一个比 Set 结构更花哨的集合,那么 Hibernate 还是提供了选择。

Java lists and maps don’t map very naturally to foreign key relationships between tables, and so we tend to avoid using them to represent associations between our entity classes. But if you feel like you really need a collection with a fancier structure than Set, Hibernate does have options.

前三个选项使我们可以将 List 的索引或 Map 的键映射到列,并且通常与 @ElementCollection 一起使用,或在关联的所有者端使用:

The first three options let us map the index of a List or key of a Map to a column, and are usually used with a @ElementCollection, or on the owning side of an association:

表 64. 用于映射列表和映射的注解

Table 64. Annotations for mapping lists and maps

Annotation

Purpose

JPA-standard

@OrderColumn

Specifies the column used to maintain the order of a list

@ListIndexBase

The column value for the first element of the list (zero by default)

@MapKeyColumn

Specifies the column used to persist the keys of a map (used when the key is of basic type)

@MapKeyJoinColumn

Specifies the column used to persist the keys of a map (used when the key is an entity)

@ManyToMany
@OrderColumn // order of list is persistent
List<Author> authors = new ArrayList<>();
@ElementCollection
@OrderColumn(name="tag_order") @ListIndexBase(1) // order column and base value
List<String> tags;
@ElementCollection
@CollectionTable(name = "author_bios",                 // table name
        joinColumns = @JoinColumn(name = "book_isbn")) // column holding foreign key of owner
@Column(name="bio")                                    // column holding map values
@MapKeyJoinColumn(name="author_ssn")                   // column holding map keys
Map<Author,String> biographies;

对于表示未拥有 @OneToMany 关联的 Map,还必须在所有者端映射列,通常通过目标实体的一个属性。在这种情况下,我们通常使用不同的注解:

For a Map representing an unowned @OneToMany association, the column must also be mapped on the owning side, usually by an attribute of the target entity. In this case we usually use a different annotation:

表 65. 用于将实体属性映射到映射键的注解

Table 65. Annotation for mapping an entity attribute to a map key

Annotation

Purpose

JPA-standard

@MapKey

Specifies an attribute of the target entity which acts as the key of the map

@OneToMany(mappedBy = Book_.PUBLISHER)
@MapKey(name = Book_.TITLE) // the key of the map is the title of the book
Map<String,Book> booksByTitle = new HashMap<>();

现在,让我们引入一个小区别:

Now, let’s introduce a little distinction:

  1. an ordered collection is one with an ordering maintained in the database, and

  2. a sorted collection is one which is sorted in Java code.

这些注解使我们能够指定集合的元素在从数据库中读取时如何排序:

These annotations allow us to specify how the elements of a collection should be ordered as they are read from the database:

表 66. 用于有序集合的注解

Table 66. Annotations for ordered collections

Annotation

Purpose

JPA-standard

@OrderBy

Specifies a fragment of JPQL used to order the collection

@SQLOrder

Specifies a fragment of SQL used to order the collection

另一方面,以下注解指定集合在内存中如何排序,用于 SortedSetSortedMap 类型的集合:

On the other hand, the following annotations specify how a collection should be sorted in memory, and are used for collections of type SortedSet or SortedMap:

表 67. 用于已排序集合的注解

Table 67. Annotations for sorted collections

Annotation

Purpose

JPA-standard

@SortNatural

Specifies that the elements of a collection are Comparable

@SortComparator

Specifies a Comparator used to sort the collection

在底层,Hibernate 使用 TreeSetTreeMap 按照已排序顺序维护集合。

Under the covers, Hibernate uses a TreeSet or TreeMap to maintain the collection in sorted order.

8.10. Any mappings

一个 @Any 映射有点像多态多对一关联,而目标实体类型不是通过通常的实体继承来关联的。目标类型通过保存在关系的 referring 端的鉴别值来区分。

An @Any mapping is a sort of polymorphic many-to-one association where the target entity types are not related by the usual entity inheritance. The target type is distinguished using a discriminator value stored on the referring side of the relationship.

这与 discriminated inheritance 非常不同,其中鉴别符保存在被引用的实体层次结构映射的表中。

This is quite different to discriminated inheritance where the discriminator is held in the tables mapped by the referenced entity hierarchy.

例如,考虑一个包含 Payment 信息的 Order 实体,其中一个 Payment 可能为 CashPaymentCreditCardPayment

For example, consider an Order entity containing Payment information, where a Payment might be a CashPayment or a CreditCardPayment:

interface Payment { ... }

@Entity
class CashPayment { ... }

@Entity
class CreditCardPayment { ... }

在这个例子中,Payment 没有被声明为实体类型,且未加 @Entity 标记。它甚至可能是一个接口,或者最多只是 CashPaymentCreditCardPayment 的映射超类。因此,就对象/关系映射而言,CashPaymentCreditCardPayment 不会被认为参与相同的实体继承层次结构。

In this example, Payment is not be declared as an entity type, and is not annotated @Entity. It might even be an interface, or at most just a mapped superclass, of CashPayment and CreditCardPayment. So in terms of the object/relational mappings, CashPayment and CreditCardPayment would not be considered to participate in the same entity inheritance hierarchy.

另一方面,CashPaymentCreditCardPayment 确实具有相同的标识符类型。这一点很重要。

On the other hand, CashPayment and CreditCardPayment do have the same identifier type. This is important.

@Any 映射将存储鉴别器值,用于识别 Payment 的具体类型,以及关联的 Order 的状态,而不是将它存储在 Payment 映射的表中。

An @Any mapping would store the discriminator value identifying the concrete type of Payment along with the state of the associated Order, instead of storing it in the table mapped by Payment.

@Entity
class Order {
    ...

    @Any
    @AnyKeyJavaClass(UUID.class)   //the foreign key type
    @JoinColumn(name="payment_id") // the foreign key column
    @Column(name="payment_type")   // the discriminator column
    // map from discriminator values to target entity types
    @AnyDiscriminatorValue(discriminator="CASH", entity=CashPayment.class)
    @AnyDiscriminatorValue(discriminator="CREDIT", entity=CreditCardPayment.class)
    Payment payment;

    ...
}

@Any 映射中的“外键”视为由外键和鉴别器共同组成的一个复合值。但请注意,此复合外键只是一种概念,不能被声明为关系数据库表上的物理约束。

It’s reasonable to think of the "foreign key" in an @Any mapping as a composite value made up of the foreign key and discriminator taken together. Note, however, that this composite foreign key is only conceptual and cannot be declared as a physical constraint on the relational database table.

有一些标记对于表达这种复杂且不自然的映射很有用:

There are a number of annotations which are useful to express this sort of complicated and unnatural mapping:

表 68. @Any 映射的标记

Table 68. Annotations for @Any mappings

Annotations

Purpose

@Any

Declares that an attribute is a discriminated polymorphic association mapping

@AnyDiscriminator

Specify the Java type of the discriminator

@JdbcType or @JdbcTypeCode

Specify the JDBC type of the discriminator

@AnyDiscriminatorValue

Specifies how discriminator values map to entity types

@Column or @Formula

Specify the column or formula in which the discriminator value is stored

@AnyKeyJavaType or @AnyKeyJavaClass

Specify the Java type of the foreign key (that is, of the ids of the target entities)

@AnyKeyJdbcType or @AnyKeyJdbcTypeCode

Specify the JDBC type of the foreign key

@JoinColumn

Specifies the foreign key column

当然,除了在非常特殊的情况下,@Any 映射不受欢迎,因为在数据库层级执行引用完整性会更加困难。

Of course, @Any mappings are disfavored, except in extremely special cases, since it’s much more difficult to enforce referential integrity at the database level.

此外,目前在 HQL 中查询 @Any 关联也有一些局限性。以下操作是允许的:

There’s also currently some limitations around querying @Any associations in HQL. This is allowed:

from Order ord
    join CashPayment cash
        on id(ord.payment) = cash.id

目前尚未实现 @Any 映射的多态关联连接。

Polymorphic association joins for @Any mappings are not currently implemented.

进一步的信息可以在 User Guide 中找到。

Further information may be found in the User Guide.

8.11. Selective column lists in inserts and updates

默认情况下,Hibernate会在自举过程中为每个实体生成 insertupdate 语句,并在每次某个实体实例持久化时重用同一个 insert 语句,并在每次某个实体实例被修改时重用同一个 update 语句。

By default, Hibernate generates insert and update statements for each entity during boostrap, and reuses the same insert statement every time an instance of the entity is made persistent, and the same update statement every time an instance of the entity is modified.

这意味着:

This means that:

  1. if an attribute is null when the entity is made persistent, its mapped column is redundantly included in the SQL insert, and

  2. worse, if a certain attribute is unmodified when other attributes are changed, the column mapped by that attribute is redundantly included in the SQL update.

大多数时候,这个情况并不值得担忧。与数据库交互的成本 usually 主要由往返成本主导,而不是由 insertupdate 中的列数主导。但在确实变得重要的情况下,有两种方法可以更严格地筛选 SQL 中包含的列。

Most of the time, this just isn’t an issue worth worrying about. The cost of interacting with the database is usually dominated by the cost of a round trip, not by the number of columns in the insert or update. But in cases where it does become important, there are two ways to be more selective about which columns are included in the SQL.

JPA 标准方式是通过 @Column 标记静态地指示有资格包含的列。例如,如果一个实体总是创建为不可变的 creationDate,且不带 completionDate,那么我们应该写:

The JPA-standard way is to indicate statically which columns are eligible for inclusion via the @Column annotation. For example, if an entity is always created with an immutable creationDate, and with no completionDate, then we would write:

@Column(updatable=false) LocalDate creationDate;
@Column(insertable=false) LocalDate completionDate;

这种方法在很多情况下都很好用,但对于具有十几个可更新列的实体来说,通常会失败。

This approach works quite well in many cases, but often breaks down for entities with more than a handful of updatable columns.

一个备选方案是要求 Hibernate 在每次执行 insertupdate 时动态生成 SQL。通过标记实体类来执行此操作。

An alternative solution is to ask Hibernate to generate SQL dynamically each time an insert or update is executed. We do this by annotating the entity class.

表 69. 动态 SQL 生成的标记

Table 69. Annotations for dynamic SQL generation

Annotation

Purpose

@DynamicInsert

Specifies that an insert statement should be generated each time an entity is made persistent

@DynamicUpdate

Specifies that an update statement should be generated each time an entity is modified

需要注意的是,虽然 @DynamicInsert 不会影响语义,但更实用的 @DynamicUpdate 标记 does 会带来一些微妙的副作用。

It’s important to realize that, while @DynamicInsert has no impact on semantics, the more useful @DynamicUpdate annotation does have a subtle side effect.

问题在于,如果一个实体没有版本属性,@DynamicUpdate 会为两个乐观事务同时读取并有选择地更新该实体的给定实例提供了可能性。原则上,这可能会导致在两个乐观事务都成功提交后出现列值不一致的行。

The wrinkle is that if an entity has no version property, @DynamicUpdate opens the possibility of two optimistic transactions concurrently reading and selectively updating a given instance of the entity. In principle, this might lead to a row with inconsistent column values after both optimistic transactions commit successfully.

当然,对于具有 @Version 属性的实体来说,不考虑这一因素。

Of course, this consideration doesn’t arise for entities with a @Version attribute.

但有一个解决办法!设计良好的关系模式应当包含 constraints 以确保数据完整性。无论我们采取何种措施在程序逻辑中保持完整性,这都是正确的。我们可以使用 @Check 注解要求 Hibernate 向我们的表中添加 check constraint 。检查约束和外键约束有助于确保一行永不包含不一致的列值。

But there’s a solution! Well-designed relational schemas should have constraints to ensure data integrity. That’s true no matter what measures we take to preserve integrity in our program logic. We may ask Hibernate to add a check constraint to our table using the @Check annotation. Check constraints and foreign key constraints can help ensure that a row never contains inconsistent column values.

8.12. Using the bytecode enhancer

Hibernate 的 bytecode enhancer启用以下功能:

Hibernate’s bytecode enhancer enables the following features:

  1. attribute-level lazy fetching for basic attributes annotated @Basic(fetch=LAZY) and for lazy non-polymorphic associations,

  2. interception-based—instead of the usual snapshot-based—detection of modifications.

要使用字节码增强器,我们必须向 gradle 构建添加 Hibernate 插件:

To use the bytecode enhancer, we must add the Hibernate plugin to our gradle build:

plugins {
    id "org.hibernate.orm" version "7.0.0.Beta1"
}

hibernate { enhancement }

考虑此字段:

Consider this field:

@Entity
class Book {
    ...

    @Basic(optional = false, fetch = LAZY)
    @Column(length = LONG32)
    String fullText;

    ...
}

fullText 字段映射到 clobtext 列(取决于 SQL 方言)。由于检索全文本本书的代价昂贵,因此我们映射了 fetch=LAZY 字段,要求 Hibernate 不要在实际使用该字段之前对其进行读取。

The fullText field maps to a clob or text column, depending on the SQL dialect. Since it’s expensive to retrieve the full book-length text, we’ve mapped the field fetch=LAZY, telling Hibernate not to read the field until it’s actually used.

  1. Without the bytecode enhancer, this instruction is ignored, and the field is always fetched immediately, as part of the initial select that retrieves the Book entity.

  2. With bytecode enhancement, Hibernate is able to detect access to the field, and lazy fetching is possible.

默认情况下,当访问给定实体的任意惰性字段时,Hibernate 将在一单个 select 中同时获取该实体的所有惰性字段。使用 @LazyGroup 注解,可以将字段分配给不同的“获取组”,以便可以独立获取不同的惰性字段。

By default, Hibernate fetches all lazy fields of a given entity at once, in a single select, when any one of them is accessed. Using the @LazyGroup annotation, it’s possible to assign fields to distinct "fetch groups", so that different lazy fields may be fetched independently.

类似地,拦截让我们能够为非多态关联实施延迟提取,而不需要单独的代理对象。但是,如果关联是多态的,也就是说,如果目标实体类型具有子类,则仍然需要代理。

Similarly, interception lets us implement lazy fetching for non-polymorphic associations without the need for a separate proxy object. However, if an association is polymorphic, that is, if the target entity type has subclasses, then a proxy is still required.

基于拦截器变更检测是一项不错的性能优化措施,但其在正确性方面略有成本。

Interception-based change detection is a nice performance optimization with a slight cost in terms of correctness.

  1. Without the bytecode enhancer, Hibernate keeps a snapshot of the state of each entity after reading from or writing to the database. When the session flushes, the snapshot state is compared to the current state of the entity to determine if the entity has been modified. Maintaining these snapshots does have an impact on performance.

  2. With bytecode enhancement, we may avoid this cost by intercepting writes to the field and recording these modifications as they happen.

然而,这种优化并非 completely 透明。

This optimization isn’t completely transparent, however.

基于拦截的变更检测比基于快照的脏数据检查准确性较低。例如,考虑以下属性:

Interception-based change detection is less accurate than snapshot-based dirty checking. For example, consider this attribute:

byte[] image; byte[] image; 拦截能够检测对 image 字段的写入,即整个数组的替换。它不能检测对数组的 elements 进行的直接修改,所以可能丢失了此类修改。

byte[] image; byte[] image; Interception is able to detect writes to the image field, that is, replacement of the whole array. It’s not able to detect modifications made directly to the elements of the array, and so such modifications may be lost.

8.13. Named fetch profiles

我们已经看到两种不同的方法来覆盖关联的默认 fetching strategy

We’ve already seen two different ways to override the default fetching strategy for an association:

  1. JPA entity graphs, and

  2. the join fetch clause in HQL, or, equivalently, the method From.fetch() in the criteria query API.

第三种方法是定义一个命名的获取配置文件。首先,我们必须通过在类或包上使用注解来声明该配置文件 @FetchProfile

A third way is to define a named fetch profile. First, we must declare the profile, by annotating a class or package @FetchProfile:

@FetchProfile(name = "EagerBook")
@Entity
class Book { ... }

请注意,虽然我们已将此注释放在 Book 实体上,但获取配置文件(不同于实体图)并不“根植”于任何特定实体。

Note that even though we’ve placed this annotation on the Book entity, a fetch profile—unlike an entity graph—isn’t "rooted" at any particular entity.

我们可以使用 @FetchProfile 注释的 fetchOverrides 成员来指定关联获取策略,但坦率地说,它看起来非常混乱,以至于我们不好意思在这里向您展示。

We may specify association fetching strategies using the fetchOverrides member of the @FetchProfile annotation, but frankly it looks so messy that we’re embarrassed to show it to you here.

类似地,可以使用 @NamedEntityGraph 定义 JPA entity graph 。但此注释的格式比 @FetchProfile(fetchOverrides=…​) even worse ,所以我们不建议使用。 💀

Similarly, a JPA entity graph may be defined using @NamedEntityGraph. But the format of this annotation is even worse than @FetchProfile(fetchOverrides=…​), so we can’t recommend it. 💀

更好的方法是使用获取配置文件对关联进行注释,它应该在该配置文件中获取:

A better way is to annotate an association with the fetch profiles it should be fetched in:

@FetchProfile(name = "EagerBook")
@Entity
class Book {
    ...

    @ManyToOne(fetch = LAZY)
    @FetchProfileOverride(profile = Book_.PROFILE_EAGER_BOOK, mode = JOIN)
    Publisher publisher;

    @ManyToMany
    @FetchProfileOverride(profile = Book_.PROFILE_EAGER_BOOK, mode = JOIN)
    Set<Author> authors;

    ...
}
@Entity
class Author {
    ...

    @OneToOne
    @FetchProfileOverride(profile = Book_.PROFILE_EAGER_BOOK, mode = JOIN)
    Person person;

    ...
}

在此,Book.PROFILE_EAGER_BOOK_ 由元模型生成器生成,并且只是一个值 "EagerBook" 为常量。

Here, once again, Book.PROFILE_EAGER_BOOK_ is generated by the Metamodel Generator, and is just a constant with the value "EagerBook".

对于集合,我们甚至可以请求子选择获取:

For collections, we may even request subselect fetching:

@FetchProfile(name = "EagerBook")
@FetchProfile(name = "BookWithAuthorsBySubselect")
@Entity
class Book {
    ...

    @OneToOne
    @FetchProfileOverride(profile = Book_.PROFILE_EAGER_BOOK, mode = JOIN)
    Person person;

    @ManyToMany
    @FetchProfileOverride(profile = Book_.PROFILE_EAGER_BOOK, mode = JOIN)
    @FetchProfileOverride(profile = Book_.BOOK_WITH_AUTHORS_BY_SUBSELECT,
                          mode = SUBSELECT)
    Set<Author> authors;

    ...
}

我们可以根据需要定义许多不同的获取配置文件。

We may define as many different fetch profiles as we like.

表 70. 用于定义获取配置文件的注释

Table 70. Annotations for defining fetch profiles

Annotation

Purpose

@FetchProfile

Declares a named fetch profile, optionally including a list of _@FetchOverride_s

@FetchProfile.FetchOverride

Declares a fetch strategy override as part of the @FetchProfile declaration

@FetchProfileOverride

Specifies the fetch strategy for the annotated association, in a given fetch profile

必须通过调用 enableFetchProfile() 为给定的会话明确启用获取配置文件:

A fetch profile must be explicitly enabled for a given session by calling enableFetchProfile():

session.enableFetchProfile(Book_.PROFILE_EAGER_BOOK);
Book eagerBook = session.find(Book.class, bookId);

那么,我们为什么或何时更喜欢使用已命名的获取配置文件,而不是实体图?好吧,这确实很难说。此功能 exists 很好,如果您喜欢它,那很好。但 Hibernate 提供了替代方案,我们认为在大多数情况下替代方案更吸引人。

So why or when might we prefer named fetch profiles to entity graphs? Well, it’s really hard to say. It’s nice that this feature exists, and if you love it, that’s great. But Hibernate offers alternatives that we think are more compelling most of the time.

提取配置文件唯一的优势在于,它们允许我们非常有选择地请求 subselect fetching. 我们无法通过实体图完成该操作,也不能通过 HQL 完成。

The one and only advantage unique to fetch profiles is that they let us very selectively request subselect fetching. We can’t do that with entity graphs, and we can’t do it with HQL.

有一个名为 org.hibernate.defaultProfile 的特殊内置抓取配置文件,它被定义为已将 @FetchProfileOverride(mode=JOIN) 应用到所有急切的 @ManyToOne@OneToOne 关联的配置文件。如果启用此配置文件:

There’s a special built-in fetch profile named org.hibernate.defaultProfile which is defined as the profile with @FetchProfileOverride(mode=JOIN) applied to every eager @ManyToOne or @OneToOne association. If you enable this profile:

session.enableFetchProfile("org.hibernate.defaultProfile"); session.enableFetchProfile("org.hibernate.defaultProfile"); 然后 outer join_s for such associations will _automatically 将被添加到每个 HQL 或条件查询中。如果你不想费心键入 @ManyToOnejoin fetch_es explicitly. And in principle it even helps partially mitigate the problem of JPA having specified the wrong default for the _fetch 成员的类型,这很不错。

session.enableFetchProfile("org.hibernate.defaultProfile"); session.enableFetchProfile("org.hibernate.defaultProfile"); Then outer join_s for such associations will _automatically be added to every HQL or criteria query. This is nice if you can’t be bothered typing out those join fetch_es explicitly. And in principle it even helps partially mitigate the problem of JPA having specified the wrong default for the _fetch member of @ManyToOne.