Hibernate ORM 中文操作指南

8. Advanced Topics

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

8.1. Filters

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

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

  1. 一个过滤器,根据行级权限限制给定用户可以看到的数据,

  2. 一个过滤器,隐藏已软删除的数据,

  3. 在版本化数据库中,一个过滤器,显示过去特定时间点的当前版本,或者

  4. 一个过滤器,限制一个特定地理区域关联的数据。

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

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

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

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

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

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

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

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

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

    @Id String username;

    String region;

    ...
}

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

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

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

    @Id String name;

    ...
}

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

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

Annotation

Purpose

@FilterDef

定义一个过滤器并声明其名称(每个过滤器一个)

@Filter

指定过滤器如何应用于给定的实体或集合(每个过滤器多个)

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

@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 )才被解释为属于过滤实体的表。

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

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

    ...
});

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

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

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

或者,自 Hibernate 6.5 起,可以在每个会话中将过滤器声明为 autoEnabled。在这种情况下,必须从 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()

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

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

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

8.2. Soft-delete

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

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

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

    ...
}

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

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

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

8.3. Multi-tenancy

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

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

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

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

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

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

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

  • 每个租户有自己的数据库,

  • 每个租户有自己的架构,或者

  • 租户共享单个架构中的表,行用租户 ID 标记。

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

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

  1. 不时,Hibernate 会请求连接,传递当前租户的 ID,然后我们必须创建一个合适的连接,或从池中获取一个连接,并返回它给 Hibernate,

  2. 稍后,Hibernate 将释放连接,并要求我们摧毁它或将其返回给适当的池。

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

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

    ...
}

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

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

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

表 60. 多重租户配置

Configuration property name

Purpose

hibernate.tenant_identifier_resolver

Specifies the CurrentTenantIdentifierResolver

hibernate.multi_tenant_connection_provider

Specifies the MultiTenantConnectionProvider

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

8.4. Using custom-written SQL

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

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

@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 的注释

Annotation

Purpose

@SQLSelect

覆盖生成的 SQL select 语句

@SQLInsert

覆盖生成的 SQL insert 语句

@SQLUpdate

覆盖生成的 SQL update 语句

@SQDelete

覆盖生成的 SQL delete 语句单行

@SQDeleteAll

覆盖生成的 SQL delete 语句多行

@SQLRestriction

向生成的 SQL 添加约束

@SQLOrder

向生成的 SQL 添加排序

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

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

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

  1. 如果一个列不应该作为自定义 insert 语句的一部分来写入,并且在自定义 SQL 中没有对应的 JDBC 参数,用 @Column(insertable=false) 映射它,或者

  2. 如果一个列不应该作为自定义 update 语句的一部分来写入,并且在自定义 SQL 中没有对应的 JDBC 参数,用 @Column(updatable=false) 映射它。

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

  1. Expectation.None,不执行任何检查,

  2. Expectation.RowCount,这是 Hibernate 在执行其自己生成的 SQL 时通常使用的方法,以及

  3. Expectation.OutParameter,用于检查存储过程的输出参数。

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

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

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

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

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

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

8.5. Handling database-generated columns

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

  1. 一个表可能有一个由触发器填充的列值,

  2. 映射列可能有一个在 DDL 中定义的默认值,或者

  3. 自定义 SQL insertupdate 语句可能会给映射列分配一个值,如我们在前一个小节中看到的那样。

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

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

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

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

生成的 DDL 如下:

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

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

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

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

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

8.6. User-defined generators

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

  1. org.hibernate.generator 包中的 Generator 接口层次结构允许您定义新生成器,

  2. org.hibernate.annotations 包中的 @IdGeneratorType 元注解允许您编写将 Generator 类型与标识符属性关联起来的注解。

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

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

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

表 62. 内置生成器

Annotation

Implementation

Purpose

@Generated

GeneratedGeneration

Generically handles database-generated values

@GeneratedColumn

GeneratedAlwaysGeneration

处理使用 generated always 生成的值

@CurrentTimestamp

CurrentTimestampGeneration

对数据库或内存中创建或更新时间戳的生成进行通用支持

@CreationTimestamp

CurrentTimestampGeneration

当实体首次变为持久时生成的 timestamp

@UpdateTimestamp

CurrentTimestampGeneration

实体持久化时生成的一个时间戳,每次修改实体时都会重新生成

@UuidGenerator

UuidGenerator

为 RFC 4122 UUID 生成的更加灵活的生成器

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

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

@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 都完成了艰苦的工作:

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 了解有关自定义生成器的更多信息。

8.7. Naming strategies

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

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

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

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

  1. Logical naming 是将命名规则应用于确定对象 logical names 的过程,这些对象未在 O/R 映射中明确指定名称。也就是说,当没有 @Table@Column 注解时。

  2. _Physical naming_是将额外规则应用于将逻辑名称转换为数据库中使用的实际“物理”名称的过程。例如,这些规则可能包含一些内容,例如使用标准化的缩写或修剪标识符的长度。

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

Flavor

Default implementation

当注释没有指定时,ImplicitNamingStrategy 负责分配逻辑名称

实现 JPA 定义规则的默认策略

PhysicalNamingStrategy 负责转换逻辑名称并生成数据库中使用的名称

不会执行任何处理的一个平凡实现

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

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

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

表 63. 命名策略配置

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

  1. Geolatte-geom定义了一组实现 OGC 空间类型的 Java 类型,以及用于与数据库原生空间数据类型进行转换的编解码器。

  2. Hibernate Spatial自身提供与 Hibernate 的集成。

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

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

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 映射的列类型:

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

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

var geometryFactory = new GeometryFactory();
...

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

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

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中找到空间关系函数支持的矩阵。

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

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 还是提供了选择。

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

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

Annotation

Purpose

JPA-standard

@OrderColumn

指定用于维护列表顺序的列

@ListIndexBase

列的值用于列表的第一个元素(默认为零)

@MapKeyColumn

指定用于持久化 map 键的列(密钥为基本类型时使用)

@MapKeyJoinColumn

指定用于持久化 map 键的列(密钥为实体时使用)

@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,还必须在所有者端映射列,通常通过目标实体的一个属性。在这种情况下,我们通常使用不同的注解:

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

Annotation

Purpose

JPA-standard

@MapKey

指定作为 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<>();

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

  1. 一个 _ordered collection_是在数据库中维护着顺序,而

  2. 一个 _sorted collection_是在 Java 代码中进行排序。

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

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

Annotation

Purpose

JPA-standard

@OrderBy

指定用于对集合进行排序的 JPQL 片段

@SQLOrder

指定用于对集合进行排序的 SQL 片段

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

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

Annotation

Purpose

JPA-standard

@SortNatural

指定集合的元素是 Comparable

@SortComparator

指定用于对集合进行排序的 Comparator

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

8.10. Any mappings

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

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

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

interface Payment { ... }

@Entity
class CashPayment { ... }

@Entity
class CreditCardPayment { ... }

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

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

@Any 映射将存储鉴别器值,用于识别 Payment 的具体类型,以及关联的 Order 的状态,而不是将它存储在 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 映射中的“外键”视为由外键和鉴别器共同组成的一个复合值。但请注意,此复合外键只是一种概念,不能被声明为关系数据库表上的物理约束。

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

表 68. @Any 映射的标记

Annotations

Purpose

@Any

声明一个属性是一个有歧视的多态关联映射

@AnyDiscriminator

指定判别器的 Java 类型

@JdbcType or @JdbcTypeCode

指定判别器的 JDBC 类型

@AnyDiscriminatorValue

指定判别器值如何映射到实体类型

@Column or @Formula

指定存储判别器值的行或公式

@AnyKeyJavaType or @AnyKeyJavaClass

指定外键的 Java 类型(即目标实体的 id)

@AnyKeyJdbcType or @AnyKeyJdbcTypeCode

指定外键的 JDBC 类型

@JoinColumn

指定外键列

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

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

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

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

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

8.11. Selective column lists in inserts and updates

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

这意味着:

  1. 如果在实体变为持久时,一个属性为 null,则其映射的列冗余地包含在 SQL _insert_中,

  2. 更糟糕的是,如果在其他属性更改时某个属性未修改,则该属性映射的列冗余地包含在 SQL _update_中。

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

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

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

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

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

表 69. 动态 SQL 生成的标记

Annotation

Purpose

@DynamicInsert

指定每次使实体持久时都应该生成一个 insert 语句

@DynamicUpdate

指定每次修改一个实体时都应该生成一个 update 语句

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

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

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

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

8.12. Using the bytecode enhancer

Hibernate 的 bytecode enhancer启用以下功能:

  1. _attribute-level lazy fetching_用于带注释的 _@Basic(fetch=LAZY)_基本属性,以及惰性非多态关联,

  2. interception-based— 代替通常的 snapshot-based— 检测修改情况。

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

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

hibernate { enhancement }

考虑此字段:

@Entity
class Book {
    ...

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

    ...
}

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

  1. _Without_字节码增强器,此指令被忽略,并且该字段始终立即获取,作为检索 _Book_实体的初始 _select_的一部分。

  2. _With_字节码增强,Hibernate 能够检测该字段的访问,且可以使用惰性获取。

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

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

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

  1. _Without_字节码增强器,Hibernate 会在从数据库中读取或向数据库写入后保留每个实体状态的快照。当会话刷新时,会将快照状态与实体的当前状态进行比较,以确定实体是否已修改。维护这些快照会对性能产生影响。

  2. _With_字节码增强,我们可以通过拦截写入到该字段并记录这些修改来避免此开销,因为这些修改发生。

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

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

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

8.13. Named fetch profiles

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

  1. JPA entity graphs, and

  2. HQL中的 _join fetch_子句,或等效地,标准查询 API 中的 _From.fetch()_方法。

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

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

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

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

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

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

@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" 为常量。

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

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

    ...
}

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

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

Annotation

Purpose

@FetchProfile

声明一个命名的抓取概况,可选地包括一些 @FetchOverride

@FetchProfile.FetchOverride

@FetchProfile 定义中声明提取策略覆盖

@FetchProfileOverride

在给定的提取配置中,指定注释关联的提取策略

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

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

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

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

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

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 成员的类型,这很不错。