Hibernate ORM 5.4.18. 最终用户指南

Preface

同时使用面向对象的软件和关系数据库既麻烦又费时。由于对象和关系数据库中数据表示方式之间的范例不匹配,因此开发成本明显更高。 Hibernate 是用于 Java 环境的对象/关系 Map 解决方案。术语Object/Relational Mapping是指将数据从对象模型表示形式 Map 到关系数据模型表示形式(反之亦然)的技术。

Hibernate 不仅负责从 Java 类到数据库表(从 Java 数据类型到 SQL 数据类型)的 Map,而且还提供数据查询和检索功能。它可以大大减少开发时间,而这些开发时间要花费在 SQL 和 JDBC 中的手动数据处理上。 Hibernate 的设计目标是通过消除使用 SQL 和 JDBC 进行手工数据手工处理的需求,使开发人员摆脱 95%的常见数据持久性相关编程任务。但是,与许多其他持久性解决方案不同,Hibernate 不会向您隐藏 SQL 的强大功能,并保证您对关系技术和知识的投资一如既往地有效。

对于仅使用存储过程在数据库中实现业务逻辑的以数据为中心的应用程序,Hibernate 可能不是最佳解决方案,它对于基于 Java 的中间层中的面向对象域模型和业务逻辑最有用。但是,Hibernate 当然可以帮助您删除或封装特定于供应商的 SQL 代码,并将帮助完成将结果集从表格表示形式转换为对象图的常见任务。

Get Involved

  • 使用 Hibernate 并报告发现的任何错误或问题。有关详情,请参见Issue Tracker

  • 尝试解决一些错误或实施增强功能。同样,请参见Issue Tracker

  • 使用邮件列表,论坛,IRC 或Community section中列出的其他方式与社区互动。

  • 帮助改进或翻译本文档。如果您有兴趣,请在开发人员邮件列表上与我们联系。

  • 传播这个词。让您的组织其他人了解 Hibernate 的好处。

System Requirements

Hibernate 5.2 和更高版本至少需要 Java 1.8 和 JDBC 4.2.

Hibernate 5.1 和更早版本至少需要 Java 1.6 和 JDBC 4.0.

Tip

从源代码构建 Hibernate 5.1 或更早版本时,由于 JDK 1.6 编译器中的错误,您需要 Java 1.7.

入门指南

新用户可能需要首先浏览Hibernate 入门指南以获得基本信息和教程。还有一系列topical guides提供对各种主题的深入研究。

Note

虽然不需要使用 Hibernate 具有扎实的 SQL 背景,但肯定有很大帮助,因为所有这些都归结为 SQL 语句。也许更重要的是对数据建模原理的理解。您可能希望将这些资源视为一个良好的起点:

了解事务和诸如工作单元(PoEAA)或应用程序事务之类的设计模式的基础也很重要。这些主题将在文档中进行讨论,但是事先理解当然会有所帮助。

1. Architecture

1.1. Overview

数据访问层

如上图所示,作为 ORM 解决方案,Hibernate 有效地“位于” Java 应用程序数据访问层和关系数据库之间。 Java 应用程序利用 Hibernate API 加载,存储,查询等其域数据。在这里,我们将介绍基本的 Hibernate API。这将是一个简短的介绍;我们将在后面详细讨论这些 Contract。

作为 JPA 提供者,Hibernate 实现 Java Persistence API 规范,并且 JPA 接口与 Hibernate 特定实现之间的关联可以在下图中显示:

image

  • SessionFactory(org.hibernate.SessionFactory)

    • 应用程序域模型到数据库的 Map 的线程安全(且不可变)表示形式。充当org.hibernate.Session个实例的工厂。 EntityManagerFactorySessionFactory的 JPA 等效项,并且基本上,这两个会融合为相同的SessionFactory实现。

创建SessionFactory非常昂贵,因此,对于任何给定的数据库,该应用程序应仅具有一个关联的SessionFactorySessionFactory维护 Hibernate 在所有Session(s)上使用的服务,例如二级缓存,连接池,事务系统集成等。

  • 会话(org.hibernate.Session)

    • 从概念上讲,单线程,短期对象建模“工作单元”(PoEAA)。在 JPA 命名法中,SessionEntityManager表示。

在后台,Hibernate Session包装了 JDBC java.sql.Connection并充当org.hibernate.Transaction实例的工厂。它维护应用程序域模型的一般“可重复读取”持久性上下文(一级缓存)。

  • Transaction(org.hibernate.Transaction)

    • 应用程序用来划分各个物理事务边界的单线程,短期对象。 EntityTransaction与 JPA 等价,并且两者都充当抽象 API,以将应用程序与使用中的基础事务系统(JDBC 或 JTA)隔离开。

2.域模型

术语domain model来自数据建模领域。它是最终描述您正在使用的problem domain的模型。有时您还会听到术语* persistent classes *。

最终,应用程序领域模型是 ORM 中的核心角色。它们构成了您希望 Map 的类。如果这些类遵循普通旧 Java 对象(POJO)/ JavaBean 编程模型,则 Hibernate 的效果最佳。但是,这些规则都不是硬性要求。实际上,Hibernate 对持久性对象的性质几乎不做任何假设。您可以用其他方式(例如,使用java.util.Map实例树)来表示域模型。

从历史上看,使用 Hibernate 的应用程序会为此目的使用其专有的 XMLMap 文件格式。随着 JPA 的到来,现在大多数信息都是通过 Comments(和/或标准化 XML 格式)在 ORM/JPA 提供程序之间可移植的方式定义的。本章将重点介绍 JPAMap。对于 JPA 不支持的 HibernateMap 功能,我们将更喜欢 Hibernate 扩展 Comments。

2.1. Map 类型

Hibernate 可以理解应用程序数据的 Java 和 JDBC 表示形式。 Hibernate * type *的功能是从数据库读取数据或向数据库写入数据。在这种用法中,类型是org.hibernate.type.Type接口的实现。这种 Hibernate 类型还描述了 Java 类型的各种行为方面,例如如何检查是否相等,如何克隆值等。

Usage of the word type

休眠类型既不是 Java 类型也不是 SQL 数据类型。它提供了有关将 Java 类型 Map 到 SQL 类型以及如何在关系数据库中持久化和获取给定 Java 类型的信息。

当您在 Hibernate 的讨论中遇到术语类型时,根据上下文,它可能是指 Java 类型,JDBC 类型或 Hibernate 类型。

为了帮助理解类型分类,让我们看一下我们希望 Map 的简单表和域模型。

例子 1.一个简单的表和域模型

create table Contact (
    id integer not null,
    first varchar(255),
    last varchar(255),
    middle varchar(255),
    notes varchar(255),
    starred boolean not null,
    website varchar(255),
    primary key (id)
)
@Entity(name = "Contact")
public static class Contact {

	@Id
	private Integer id;

	private Name name;

	private String notes;

	private URL website;

	private boolean starred;

	//Getters and setters are omitted for brevity
}

@Embeddable
public class Name {

	private String first;

	private String middle;

	private String last;

	// getters and setters omitted
}

从广义上讲,Hibernate 将类型分为两类:

2.1.1. 值类型

值类型是一条未定义其自身生命周期的数据。实际上,它由定义其生命周期的实体所有。

从另一种角度来看,实体的所有状态完全由值类型组成。这些状态字段或 JavaBean 属性称为持久属性Contact类的持久属性是值类型。

值类型进一步分为三个子类别:

  • Basic types

    • 在 MapContact表时,除名称以外的所有属性都是基本类型。基本类型将在Basic types中详细讨论。
  • Embeddable types

  • 集合 类型

    • 尽管集合类型在值类型中是一个明显的类别,但在上述示例中未作介绍。集合类型将在Collections中进一步讨论

2.1.2. 实体类型

实体根据其唯一标识符的性质独立于其他对象而存在,而值则不存在。实体是域模型类,使用唯一标识符与数据库表中的行相关。由于需要唯一标识符,因此实体独立存在并定义自己的生命周期。 Contact类本身就是一个实体的示例。

Entity types中详细讨论了 Map 实体。

2.2. 命名策略

对象模型到关系数据库的 Map 的一部分是将对象模型的名称 Map 到相应的数据库名称。 Hibernate 将其视为两个阶段的过程:

  • 第一步是从域模型 Map 中确定适当的逻辑名。逻辑名称可以由用户明确指定(例如,使用@Column@Table),也可以由 Hibernate 通过ImplicitNamingStrategyContracts 隐式确定。

  • 第二个是将此逻辑名称解析为由PhysicalNamingStrategyContract 定义的物理名称。

Historical NamingStrategy contract

Hibernate 历史上只定义了一个org.hibernate.cfg.NamingStrategy。那个单一的 NamingStrategyContract 实际上结合了单独的关注点,这些关注点现在分别建模为 ImplicitNamingStrategy 和 PhysicalNamingStrategy。

而且,NamingStrategyContract 通常不够灵活,无法正确地应用给定的命名“规则”,这是因为 API 缺乏决定信息,或者因为 API 的 Developing 一直没有很好地定义。

由于这些限制,不推荐使用org.hibernate.cfg.NamingStrategy,而建议使用 ImplicitNamingStrategy 和 PhysicalNamingStrategy。

从根本上讲,每种命名策略背后的思想是使开发人员为 Map 域模型而必须提供的重复信息量最小化。

JPA Compatibility

JPA 定义了有关隐式逻辑名称确定的固有规则。如果主要关注 JPA 提供程序的可移植性,或者您真的很喜欢 JPA 定义的隐式命名规则,请确保坚持使用 ImplicitNamingStrategyJpaCompliantImpl(默认设置)。

而且,JPA 定义逻辑名称和物理名称之间没有分隔。按照 JPA 规范,逻辑名称**是物理名称。如果 JPA 提供程序的可移植性很重要,则应用程序不应选择不指定 PhysicalNamingStrategy。

2.2.1. ImplicitNamingStrategy

当实体未明确命名其 Map 到的数据库表时,我们需要隐式确定该表名。或者,当特定属性没有显式命名其 Map 到的数据库列时,我们需要隐式确定该列名称。当 Map 未提供显式名称时,可以使用org.hibernate.boot.model.naming.ImplicitNamingStrategyContract 来确定逻辑名称的示例。

隐式命名策略图

Hibernate 开箱即用地定义了多个 ImplicitNamingStrategy 实现。应用程序也可以自由插入自定义实现。

有多种方法可以指定要使用的 ImplicitNamingStrategy。首先,应用程序可以使用hibernate.implicit_naming_strategy配置设置指定实现,该设置接受:

  • 现成实现的 sched 义“短名称”

  • default

    • org.hibernate.boot.model.naming.ImplicitNamingStrategyJpaCompliantImpl的别名-jpa的别名
  • jpa

    • org.hibernate.boot.model.naming.ImplicitNamingStrategyJpaCompliantImpl-符合 JPA 2.0 的命名策略
  • legacy-hbm

    • org.hibernate.boot.model.naming.ImplicitNamingStrategyLegacyHbmImpl-符合原始的 Hibernate NamingStrategy
  • legacy-jpa

    • org.hibernate.boot.model.naming.ImplicitNamingStrategyLegacyJpaImpl-符合为 JPA 1.0 开发的旧版 NamingStrategy,遗憾的是,在很多方面都不清楚隐式命名规则
  • component-path

    • 对于org.hibernate.boot.model.naming.ImplicitNamingStrategyComponentPathImpl-除遵循结尾属性部分外,大多数情况都遵循ImplicitNamingStrategyJpaCompliantImpl规则,但它使用完整的复合路径
  • 引用实现org.hibernate.boot.model.naming.ImplicitNamingStrategyContract 的类

  • 实现org.hibernate.boot.model.naming.ImplicitNamingStrategyContract 的类的 FQN

其次,应用程序和集成可以利用org.hibernate.boot.MetadataBuilder#applyImplicitNamingStrategy来指定要使用的 ImplicitNamingStrategy。有关引导的更多详细信息,请参见Bootstrap

2.2.2. PhysicalNamingStrategy

许多组织围绕数据库对象(表,列,外键等)的命名定义规则。 PhysicalNamingStrategy 的思想是帮助实现此类命名规则,而不必通过显式名称将其硬编码到 Map 中。

虽然 ImplicitNamingStrategy 的目的是确定名为accountNumber的属性在未明确指定时 Map 到逻辑列名称accountNumber,但是 PhysicalNamingStrategy 的目的例如是说应将物理列名称缩写为acct_num。 。

Note

的确,在这种情况下,可以使用ImplicitNamingStrategy处理acct_num的分辨率。

但是这里的重点是关注点分离。不管属性是显式指定列名还是隐式确定列名,都将应用PhysicalNamingStrategyImplicitNamingStrategy仅在未提供明确名称的情况下才会应用。因此,这完全取决于需求和意图。

默认实现是简单地使用逻辑名作为物理名。但是,应用程序和集成可以定义此 PhysicalNamingStrategyContract 的自定义实现。这是一个名为 Acme Corp 的虚拟公司的物理命名策略示例,其命名标准为:

  • 喜欢用下划线定界的单词而不是驼峰式的单词

  • 用标准缩写替换某些单词

例子 2.例子 PhysicalNamingStrategy 实现

/*
 * Hibernate, Relational Persistence for Idiomatic Java
 *
 * License: GNU Lesser General Public License (LGPL), version 2.1 or later.
 * See the lgpl.txt file in the root directory or <http://www.gnu.org/licenses/lgpl-2.1.html>.
 */
package org.hibernate.userguide.naming;

import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.TreeMap;

import org.hibernate.boot.model.naming.Identifier;
import org.hibernate.boot.model.naming.PhysicalNamingStrategy;
import org.hibernate.engine.jdbc.env.spi.JdbcEnvironment;

import org.apache.commons.lang3.StringUtils;

/**
 * An example PhysicalNamingStrategy that implements database object naming standards
 * for our fictitious company Acme Corp.
 * <p/>
 * In general Acme Corp prefers underscore-delimited words rather than camel casing.
 * <p/>
 * Additionally standards call for the replacement of certain words with abbreviations.
 *
 * @author Steve Ebersole
 */
public class AcmeCorpPhysicalNamingStrategy implements PhysicalNamingStrategy {
	private static final Map<String,String> ABBREVIATIONS = buildAbbreviationMap();

	@Override
	public Identifier toPhysicalCatalogName(Identifier name, JdbcEnvironment jdbcEnvironment) {
		// Acme naming standards do not apply to catalog names
		return name;
	}

	@Override
	public Identifier toPhysicalSchemaName(Identifier name, JdbcEnvironment jdbcEnvironment) {
		// Acme naming standards do not apply to schema names
		return name;
	}

	@Override
	public Identifier toPhysicalTableName(Identifier name, JdbcEnvironment jdbcEnvironment) {
		final List<String> parts = splitAndReplace( name.getText() );
		return jdbcEnvironment.getIdentifierHelper().toIdentifier(
				join( parts ),
				name.isQuoted()
		);
	}

	@Override
	public Identifier toPhysicalSequenceName(Identifier name, JdbcEnvironment jdbcEnvironment) {
		final LinkedList<String> parts = splitAndReplace( name.getText() );
		// Acme Corp says all sequences should end with _seq
		if ( !"seq".equalsIgnoreCase( parts.getLast() ) ) {
			parts.add( "seq" );
		}
		return jdbcEnvironment.getIdentifierHelper().toIdentifier(
				join( parts ),
				name.isQuoted()
		);
	}

	@Override
	public Identifier toPhysicalColumnName(Identifier name, JdbcEnvironment jdbcEnvironment) {
		final List<String> parts = splitAndReplace( name.getText() );
		return jdbcEnvironment.getIdentifierHelper().toIdentifier(
				join( parts ),
				name.isQuoted()
		);
	}

	private static Map<String, String> buildAbbreviationMap() {
		TreeMap<String,String> abbreviationMap = new TreeMap<> ( String.CASE_INSENSITIVE_ORDER );
		abbreviationMap.put( "account", "acct" );
		abbreviationMap.put( "number", "num" );
		return abbreviationMap;
	}

	private LinkedList<String> splitAndReplace(String name) {
		LinkedList<String> result = new LinkedList<>();
		for ( String part : StringUtils.splitByCharacterTypeCamelCase( name ) ) {
			if ( part == null || part.trim().isEmpty() ) {
				// skip null and space
				continue;
			}
			part = applyAbbreviationReplacement( part );
			result.add( part.toLowerCase( Locale.ROOT ) );
		}
		return result;
	}

	private String applyAbbreviationReplacement(String word) {
		if ( ABBREVIATIONS.containsKey( word ) ) {
			return ABBREVIATIONS.get( word );
		}

		return word;
	}

	private String join(List<String> parts) {
		boolean firstPass = true;
		String separator = "";
		StringBuilder joined = new StringBuilder();
		for ( String part : parts ) {
			joined.append( separator ).append( part );
			if ( firstPass ) {
				firstPass = false;
				separator = "_";
			}
		}
		return joined.toString();
	}
}

有多种方法可以指定要使用的 PhysicalNamingStrategy。首先,应用程序可以使用hibernate.physical_naming_strategy配置设置指定实现,该设置接受:

  • 引用实现org.hibernate.boot.model.naming.PhysicalNamingStrategyContract 的类

  • 实现org.hibernate.boot.model.naming.PhysicalNamingStrategyContract 的类的 FQN

其次,应用程序和集成可以利用org.hibernate.boot.MetadataBuilder#applyPhysicalNamingStrategy。有关引导的更多详细信息,请参见Bootstrap

2.3. 基本类型

基本值类型通常将单个数据库列 Map 到单个非聚合 Java 类型。 Hibernate 提供了许多内置的基本类型,它们遵循 JDBC 规范建议的自然 Map。

在内部,Hibernate 需要解析特定的org.hibernate.type.Type时使用基本类型的注册表。

2.3.1. 休眠提供的 BasicTypes

*表 1.标准 BasicTypes *

Hibernate 类型(org.hibernate.type 包)JDBC typeJava typeBasicTypeRegistry key(s)
StringTypeVARCHARjava.lang.Stringstring, java.lang.String
MaterializedClobCLOBjava.lang.Stringmaterialized_clob
TextTypeLONGVARCHARjava.lang.Stringtext
CharacterTypeCHARchar, java.lang.Character字符,char,java.lang.Character
BooleanTypeBOOLEANboolean, java.lang.Booleanboolean, java.lang.Boolean
NumericBooleanType整数,0 为假,1 为真boolean, java.lang.Booleannumeric_boolean
YesNoTypeCHAR,'N'/'n'为假,'Y'/'y'为真。大写的值将写入数据库。boolean, java.lang.Booleanyes_no
TrueFalseTypeCHAR,'F'/'f'为假,'T'/'t'为真。大写的值将写入数据库。boolean, java.lang.Booleantrue_false
ByteTypeTINYINTbyte, java.lang.Bytebyte, java.lang.Byte
ShortTypeSMALLINTshort, java.lang.Shortshort, java.lang.Short
IntegerTypeINTEGERint, java.lang.Integer整数,整数,java.lang.Integer
LongTypeBIGINTlong, java.lang.Longlong, java.lang.Long
FloatTypeFLOATfloat, java.lang.Floatfloat, java.lang.Float
DoubleTypeDOUBLEdouble, java.lang.Doubledouble, java.lang.Double
BigIntegerTypeNUMERICjava.math.BigIntegerbig_integer, java.math.BigInteger
BigDecimalTypeNUMERICjava.math.BigDecimalbig_decimal, java.math.bigDecimal
TimestampTypeTIMESTAMPjava.util.Date时间戳记,java.sql.Timestamp,java.util.Date
DbTimestampTypeTIMESTAMPjava.util.Datedbtimestamp
TimeTypeTIMEjava.util.Datetime, java.sql.Time
DateTypeDATEjava.util.Datedate, java.sql.Date
CalendarTypeTIMESTAMPjava.util.Calendarcalendar,java.util.Calendar,java.util.GregorianCalendar
CalendarDateTypeDATEjava.util.Calendarcalendar_date
CalendarTimeTypeTIMEjava.util.Calendarcalendar_time
CurrencyTypeVARCHARjava.util.Currencycurrency, java.util.Currency
LocaleTypeVARCHARjava.util.Localelocale, java.util.Locale
TimeZoneTypeVARCHAR,使用 TimeZone IDjava.util.TimeZonetimezone, java.util.TimeZone
UrlTypeVARCHARjava.net.URLurl, java.net.URL
ClassTypeVARCHAR(FQN 类)java.lang.Classclass, java.lang.Class
BlobTypeBLOBjava.sql.Blobblob, java.sql.Blob
ClobTypeCLOBjava.sql.Clobclob, java.sql.Clob
BinaryTypeVARBINARYbyte[]binary, byte[]
MaterializedBlobTypeBLOBbyte[]materialized_blob
ImageTypeLONGVARBINARYbyte[]image
WrapperBinaryTypeVARBINARYjava.lang.Byte[]wrapper-binary,Byte [],java.lang.Byte []
CharArrayTypeVARCHARchar[]characters, char[]
CharacterArrayTypeVARCHARjava.lang.Character[]包装字符,Character [],java.lang.Character []
UUIDBinaryTypeBINARYjava.util.UUIDuuid-binary, java.util.UUID
UUIDCharTypeCHAR,也可以读取 VARCHARjava.util.UUIDuuid-char
PostgresUUIDType通过 Types#OTHER 的 PostgreSQL UUID,它符合 PostgreSQL JDBC 驱动程序定义java.util.UUIDpg-uuid
SerializableTypeVARBINARYjava.lang.Serializable 的实现者与其他值类型不同,该类型的多个实例被注册。它在 java.io.Serializable 下注册一次,并在特定的 java.io.Serializable 实现类名称下注册。
StringNVarcharTypeNVARCHARjava.lang.Stringnstring
NTextTypeLONGNVARCHARjava.lang.Stringntext
NClobTypeNCLOBjava.sql.NClobnclob, java.sql.NClob
MaterializedNClobTypeNCLOBjava.lang.Stringmaterialized_nclob
PrimitiveCharacterArrayNClobTypeNCHARchar[]N/A
CharacterNCharTypeNCHARjava.lang.Characterncharacter
CharacterArrayNClobTypeNCLOBjava.lang.Character[]N/A
RowVersionTypeVARBINARYbyte[]row_version
ObjectTypeVARCHARjava.lang.Serializable 的实现者object, java.lang.Object

*表 2. Java 8 BasicTypes *

Hibernate 类型(org.hibernate.type 包)JDBC typeJava typeBasicTypeRegistry key(s)
DurationTypeBIGINTjava.time.DurationDuration, java.time.Duration
InstantTypeTIMESTAMPjava.time.InstantInstant, java.time.Instant
LocalDateTimeTypeTIMESTAMPjava.time.LocalDateTimeLocalDateTime, java.time.LocalDateTime
LocalDateTypeDATEjava.time.LocalDateLocalDate, java.time.LocalDate
LocalTimeTypeTIMEjava.time.LocalTimeLocalTime, java.time.LocalTime
OffsetDateTimeTypeTIMESTAMPjava.time.OffsetDateTimeOffsetDateTime, java.time.OffsetDateTime
OffsetTimeTypeTIMEjava.time.OffsetTimeOffsetTime, java.time.OffsetTime
ZonedDateTimeTypeTIMESTAMPjava.time.ZonedDateTimeZonedDateTime, java.time.ZonedDateTime

*表 3. Hibernate Spatial BasicTypes *

Hibernate 类型(org.hibernate.spatial 包)JDBC typeJava typeBasicTypeRegistry key(s)
JTSGeometryType取决于方言com.vividsolutions.jts.geom.Geometryjts_geometry,以及 Geometry 及其子类的类名
GeolatteGeometryType取决于方言org.geolatte.geom.Geometrygeolatte_geometry,以及 Geometry 及其子类的类名

Note

要使用 Hibernate Spatial 类型,必须将hibernate-spatial依赖项添加到您的 Classpath 中使用org.hibernate.spatial.SpatialDialect实现。

有关更多详细信息,请参见Spatial章。

这些 Map 由 Hibernate 内部称为org.hibernate.type.BasicTypeRegistry的服务 Management,该服务本质上维护着以名称为关键字的org.hibernate.type.BasicType(一个org.hibernate.type.Type专业化)实例的 Map。这就是先前表中“ BasicTypeRegistry 键”列的目的。

2.3.2. @Basic 注解

严格来说,基本类型由javax.persistence.BasicComments 表示。一般来说,默认情况下可以忽略@BasicComments。以下两个示例最终都是相同的。

例子 3. @Basic明确声明

@Entity(name = "Product")
public class Product {

	@Id
	@Basic
	private Integer id;

	@Basic
	private String sku;

	@Basic
	private String name;

	@Basic
	private String description;
}

例子 4. @Basic被隐式暗示

@Entity(name = "Product")
public class Product {

	@Id
	private Integer id;

	private String sku;

	private String name;

	private String description;
}

Tip

JPA 规范严格将可以标记为基本的 Java 类型限制在以下列表中:

  • Java 基本类型(booleanint等)

  • 基本类型(java.lang.Booleanjava.lang.Integer等)的包装

  • java.lang.String

  • java.math.BigInteger

  • java.math.BigDecimal

  • java.util.Date

  • java.util.Calendar

  • java.sql.Date

  • java.sql.Time

  • java.sql.Timestamp

  • byte[]Byte[]

  • char[]Character[]

  • enums

  • 实现Serializable的任何其他类型(JPA 对Serializable类型的“支持”是直接将其状态序列化到数据库)。

如果需要提供程序的可移植性,则应仅遵循这些基本类型。

请注意,JPA 2.1 引入了javax.persistence.AttributeConverterContract 以帮助减轻其中的一些担忧。有关此主题的更多信息,请参见JPA 2.1 AttributeConverters

@BasicComments 定义 2 个属性。

  • optional-布尔值(默认为 true)

    • 定义此属性是否允许空值。 JPA 将其定义为“提示”,这实际上意味着特别需要其效果。只要类型不是原始类型,Hibernate 就会将此表示基础列应为NULLABLE
  • fetch-FetchType(默认为 EAGER)

    • 定义此属性是应立即获取还是应延迟获取。 JPA 说,EAGER 是提供程序(休眠)的一项要求,要求在获取所有者时应获取值,而 LAZY 只是提示访问属性时要获取值。除非您使用字节码增强功能,否则 Hibernate 对于基本类型将忽略此设置。有关获取和字节码增强的更多信息,请参见Bytecode Enhancement

2.3.3. @Column 注解

JPA 定义了用于隐式确定表和列名称的规则。有关隐式命名的详细讨论,请参见Naming strategies

对于基本类型属性,隐式命名规则是列名称与属性名称相同。如果该隐式命名规则不符合您的要求,则可以显式告诉 Hibernate(和其他提供程序)要使用的列名。

例子 5.显式列命名

@Entity(name = "Product")
public class Product {

	@Id
	private Integer id;

	private String sku;

	private String name;

	@Column( name = "NOTES" )
	private String description;
}

在这里,我们使用@Columndescription属性显式 Map 到NOTES列,而不是隐式列名description

@ColumnComments 还定义了其他 Map 信息。有关详细信息,请参见其 Javadocs。

2.3.4. BasicTypeRegistry

前面我们说过,Hibernate 类型既不是 Java 类型,也不是 SQL 类型,但是它既可以理解两者,又可以在它们之间进行编组。但是,从前面的示例中看到基本的类型 Map,Hibernate 如何知道使用其org.hibernate.type.StringTypeMapjava.lang.String属性,还是使用org.hibernate.type.IntegerTypeMapjava.lang.Integer属性?

答案在于 Hibernate 内部的一个名为org.hibernate.type.BasicTypeRegistry的服务,该服务实际上维护着以名称为关键字的org.hibernate.type.BasicType(一个org.hibernate.type.Type专业化)实例的 Map。

稍后我们将在Explicit BasicTypes部分中看到,我们可以明确告诉 Hibernate 对特定属性使用哪个 BasicType。但是首先,让我们探讨隐式分辨率的工作原理以及应用程序如何调整隐式分辨率。

Note

BasicTypeRegistry和所有其他类型的贡献方式的详尽讨论超出了本文档的范围。

有关完整的详细信息,请参见Integration Guide

例如,采用我们之前在 Product#sku 中看到的 String 属性。由于没有显式的类型 Map,因此 Hibernate 依靠BasicTypeRegistry查找java.lang.String的注册 Map。这可以 traceback 到我们在本章开头的表中看到的“ BasicTypeRegistry 键”列。

作为BasicTypeRegistry的基线,Hibernate 遵循针对 Java 类型的 JDBC 推荐 Map。 JDBC 建议将字符串 Map 到 VARCHAR,VARCHAR 是StringType处理的确切 Map。这就是BasicTypeRegistry中字符串的基线 Map。

应用程序还可以在引导过程中使用MetadataBuilder#applyBasicType方法或MetadataBuilder#applyTypes方法之一扩展(添加新的BasicType注册)或覆盖(替换现有的BasicType注册)。有关更多详细信息,请参见Custom BasicTypes部分。

2.3.5. 显式 BasicType

有时您希望对特定属性进行不同的处理。有时,Hibernate 会隐式选择一个您不需要的BasicType(并且由于某些原因,您不想调整BasicTypeRegistry)。

在这些情况下,您必须通过org.hibernate.annotations.Type注解明确告知 Hibernate 使用BasicType

例子 6.使用@org.hibernate.annotations.Type

@Entity(name = "Product")
public class Product {

	@Id
	private Integer id;

	private String sku;

	@org.hibernate.annotations.Type( type = "nstring" )
	private String name;

	@org.hibernate.annotations.Type( type = "materialized_nclob" )
	private String description;
}

这告诉 Hibernate 将字符串存储为国有化数据。这只是出于说明目的;有关指示民族化字符数据的更好方法,请参见Map 民族化字符数据部分。

另外,该描述将作为 LOB 处理。同样,有关指示 LOB 的更好方法,请参见Mapping LOBs部分。

org.hibernate.annotations.Type#type属性可以命名以下任意一项:

  • org.hibernate.type.Type实现的全限定名称

  • BasicTypeRegistry注册的任何密钥

  • 任何已知的“类型定义”的名称

2.3.6. 自定义 BasicType

Hibernate 使开发人员相对容易地创建自己的基本类型 Map 类型。例如,您可能要保留java.util.BigIntegerVARCHAR列的属性,或支持全新的类型。

开发自定义类型有两种方法:

  • 实施并注册BasicType

  • 实现不需要类型注册的UserType

作为说明不同方法的一种方式,让我们考虑一个用例,在该用例中,我们需要支持存储为 VARCHAR 的java.util.BitSetMap。

实现 BasicType

第一种方法是直接实现BasicType接口。

Note

因为BasicType接口有很多实现的方法,所以如果将值存储在单个数据库列中,则扩展AbstractStandardBasicTypeAbstractSingleColumnStandardBasicType Hibernate 类更加方便。

首先,我们需要像这样扩展AbstractSingleColumnStandardBasicType

例子 7.定制BasicType实现

public class BitSetType
        extends AbstractSingleColumnStandardBasicType<BitSet>
        implements DiscriminatorType<BitSet> {

    public static final BitSetType INSTANCE = new BitSetType();

    public BitSetType() {
        super( VarcharTypeDescriptor.INSTANCE, BitSetTypeDescriptor.INSTANCE );
    }

    @Override
    public BitSet stringToObject(String xml) throws Exception {
        return fromString( xml );
    }

    @Override
    public String objectToSQLString(BitSet value, Dialect dialect) throws Exception {
        return toString( value );
    }

    @Override
    public String getName() {
        return "bitset";
    }

}

AbstractSingleColumnStandardBasicType需要sqlTypeDescriptorjavaTypeDescriptorsqlTypeDescriptorVarcharTypeDescriptor.INSTANCE,因为数据库列是 VARCHAR。在 Java 方面,我们需要使用BitSetTypeDescriptor实例,该实例可以这样实现:

例子 8.定制AbstractTypeDescriptor实现

public class BitSetTypeDescriptor extends AbstractTypeDescriptor<BitSet> {

    private static final String DELIMITER = ",";

    public static final BitSetTypeDescriptor INSTANCE = new BitSetTypeDescriptor();

    public BitSetTypeDescriptor() {
        super( BitSet.class );
    }

    @Override
    public String toString(BitSet value) {
        StringBuilder builder = new StringBuilder();
        for ( long token : value.toLongArray() ) {
            if ( builder.length() > 0 ) {
                builder.append( DELIMITER );
            }
            builder.append( Long.toString( token, 2 ) );
        }
        return builder.toString();
    }

    @Override
    public BitSet fromString(String string) {
        if ( string == null || string.isEmpty() ) {
            return null;
        }
        String[] tokens = string.split( DELIMITER );
        long[] values = new long[tokens.length];

        for ( int i = 0; i < tokens.length; i++ ) {
            values[i] = Long.valueOf( tokens[i], 2 );
        }
        return BitSet.valueOf( values );
    }

    @SuppressWarnings({"unchecked"})
    public <X> X unwrap(BitSet value, Class<X> type, WrapperOptions options) {
        if ( value == null ) {
            return null;
        }
        if ( BitSet.class.isAssignableFrom( type ) ) {
            return (X) value;
        }
        if ( String.class.isAssignableFrom( type ) ) {
            return (X) toString( value);
        }
        throw unknownUnwrap( type );
    }

    public <X> BitSet wrap(X value, WrapperOptions options) {
        if ( value == null ) {
            return null;
        }
        if ( String.class.isInstance( value ) ) {
            return fromString( (String) value );
        }
        if ( BitSet.class.isInstance( value ) ) {
            return (BitSet) value;
        }
        throw unknownWrap( value.getClass() );
    }
}

BitSet作为PreparedStatement绑定参数传递时使用unwrap方法,而wrap方法用于将 JDBC 列值对象(例如本例中的String)转换为实际的 Map 对象类型(例如本例中的BitSet)。

BasicType必须注册,这可以在引导时完成:

例子 9.注册一个 Custom BasicType实现

configuration.registerTypeContributor( (typeContributions, serviceRegistry) -> {
	typeContributions.contributeType( BitSetType.INSTANCE );
} );

或使用MetadataBuilder

ServiceRegistry standardRegistry =
    new StandardServiceRegistryBuilder().build();

MetadataSources sources = new MetadataSources( standardRegistry );

MetadataBuilder metadataBuilder = sources.getMetadataBuilder();

metadataBuilder.applyBasicType( BitSetType.INSTANCE );

将新的BitSetType注册为bitset,实体 Map 如下所示:

例子 10.自定义BasicTypeMap

@Entity(name = "Product")
public static class Product {

	@Id
	private Integer id;

	@Type( type = "bitset" )
	private BitSet bitSet;

	public Integer getId() {
		return id;
	}

	//Getters and setters are omitted for brevity
}

或者,您可以使用@TypeDef并跳过注册阶段:

例子 11.使用@TypeDef注册一个自定义类型

@Entity(name = "Product")
@TypeDef(
	name = "bitset",
	defaultForType = BitSet.class,
	typeClass = BitSetType.class
)
public static class Product {

	@Id
	private Integer id;

	private BitSet bitSet;

	//Getters and setters are omitted for brevity
}

要验证此新的BasicType实施,我们可以对其进行如下测试:

例子 12.坚持定制BasicType

BitSet bitSet = BitSet.valueOf( new long[] {1, 2, 3} );

doInHibernate( this::sessionFactory, session -> {
	Product product = new Product( );
	product.setId( 1 );
	product.setBitSet( bitSet );
	session.persist( product );
} );

doInHibernate( this::sessionFactory, session -> {
	Product product = session.get( Product.class, 1 );
	assertEquals(bitSet, product.getBitSet());
} );

执行此单元测试时,Hibernate 生成以下 SQL 语句:

例子 13.坚持定制BasicType

DEBUG SQL:92 -
    insert
    into
        Product
        (bitSet, id)
    values
        (?, ?)

TRACE BasicBinder:65 - binding parameter [1] as [VARCHAR] - [{0, 65, 128, 129}]
TRACE BasicBinder:65 - binding parameter [2] as [INTEGER] - [1]

DEBUG SQL:92 -
    select
        bitsettype0_.id as id1_0_0_,
        bitsettype0_.bitSet as bitSet2_0_0_
    from
        Product bitsettype0_
    where
        bitsettype0_.id=?

TRACE BasicBinder:65 - binding parameter [1] as [INTEGER] - [1]
TRACE BasicExtractor:61 - extracted value ([bitSet2_0_0_] : [VARCHAR]) - [{0, 65, 128, 129}]

如您所见,BitSetType负责* Java-to-SQL SQL-to-Java *类型转换。

实现用户类型

第二种方法是实现UserType接口。

例子 14.定制UserType实现

public class BitSetUserType implements UserType {

	public static final BitSetUserType INSTANCE = new BitSetUserType();

    private static final Logger log = Logger.getLogger( BitSetUserType.class );

    @Override
    public int[] sqlTypes() {
        return new int[] {StringType.INSTANCE.sqlType()};
    }

    @Override
    public Class returnedClass() {
        return BitSet.class;
    }

    @Override
    public boolean equals(Object x, Object y)
			throws HibernateException {
        return Objects.equals( x, y );
    }

    @Override
    public int hashCode(Object x)
			throws HibernateException {
        return Objects.hashCode( x );
    }

    @Override
    public Object nullSafeGet(
            ResultSet rs, String[] names, SharedSessionContractImplementor session, Object owner)
            throws HibernateException, SQLException {
        String columnName = names[0];
        String columnValue = (String) rs.getObject( columnName );
        log.debugv("Result set column {0} value is {1}", columnName, columnValue);
        return columnValue == null ? null :
				BitSetTypeDescriptor.INSTANCE.fromString( columnValue );
    }

    @Override
    public void nullSafeSet(
            PreparedStatement st, Object value, int index, SharedSessionContractImplementor session)
            throws HibernateException, SQLException {
        if ( value == null ) {
            log.debugv("Binding null to parameter {0} ",index);
            st.setNull( index, Types.VARCHAR );
        }
        else {
            String stringValue = BitSetTypeDescriptor.INSTANCE.toString( (BitSet) value );
            log.debugv("Binding {0} to parameter {1} ", stringValue, index);
            st.setString( index, stringValue );
        }
    }

    @Override
    public Object deepCopy(Object value)
			throws HibernateException {
        return value == null ? null :
            BitSet.valueOf( BitSet.class.cast( value ).toLongArray() );
    }

    @Override
    public boolean isMutable() {
        return true;
    }

    @Override
    public Serializable disassemble(Object value)
			throws HibernateException {
        return (BitSet) deepCopy( value );
    }

    @Override
    public Object assemble(Serializable cached, Object owner)
			throws HibernateException {
        return deepCopy( cached );
    }

    @Override
    public Object replace(Object original, Object target, Object owner)
			throws HibernateException {
        return deepCopy( original );
    }
}

实体 Map 如下所示:

例子 15.自定义UserTypeMap

@Entity(name = "Product")
public static class Product {

	@Id
	private Integer id;

	@Type( type = "bitset" )
	private BitSet bitSet;

	//Constructors, getters, and setters are omitted for brevity
}

在此示例中,UserTypebitset名称注册,并且这样做是这样的:

例子 16.注册一个 Custom UserType实现

configuration.registerTypeContributor( (typeContributions, serviceRegistry) -> {
	typeContributions.contributeType( BitSetUserType.INSTANCE, "bitset");
} );

或使用MetadataBuilder

ServiceRegistry standardRegistry =
    new StandardServiceRegistryBuilder().build();

MetadataSources sources = new MetadataSources( standardRegistry );

MetadataBuilder metadataBuilder = sources.getMetadataBuilder();

metadataBuilder.applyBasicType( BitSetUserType.INSTANCE, "bitset" );

Note

BasicType一样,您也可以使用简单的名称注册UserType

无需注册名称,UserTypeMap 就需要完全限定的类名称:

@Type( type = "org.hibernate.userguide.mapping.basic.BitSetUserType" )

当针对BitSetUserType实体 Map 运行先前的测试用例时,Hibernate 执行以下 SQL 语句:

例子 17.坚持定制BasicType

DEBUG SQL:92 -
    insert
    into
        Product
        (bitSet, id)
    values
        (?, ?)

DEBUG BitSetUserType:71 - Binding 1,10,11 to parameter 1
TRACE BasicBinder:65 - binding parameter [2] as [INTEGER] - [1]

DEBUG SQL:92 -
    select
        bitsetuser0_.id as id1_0_0_,
        bitsetuser0_.bitSet as bitSet2_0_0_
    from
        Product bitsetuser0_
    where
        bitsetuser0_.id=?

TRACE BasicBinder:65 - binding parameter [1] as [INTEGER] - [1]
DEBUG BitSetUserType:56 - Result set column bitSet2_0_0_ value is 1,10,11

2.3.7. Map 枚举

Hibernate 支持通过多种不同方式将 Java 枚举 Map 为基本值类型。

@Enumerated

最初的 JPA 兼容 Map 枚举方法是通过@Enumerated@MapKeyEnumerated进行 Map 键 Comments,其工作原理是,枚举值根据javax.persistence.EnumType指示的两种策略之一进行存储:

  • ORDINAL

    • 根据枚举类中枚举值的序号位置存储,如java.lang.Enum#ordinal
  • STRING

    • 根据枚举值的名称存储,如java.lang.Enum#name

假设以下列举:

例子 18. PhoneType枚举

public enum PhoneType {
    LAND_LINE,
    MOBILE;
}

在 ORDINAL 示例中,phone_type列被定义为(可为空)INTEGER 类型,并且将保留:

  • NULL

    • 对于空值
  • 0

    • 对于LAND_LINE枚举
  • 1

    • 对于MOBILE枚举

例子 19. @Enumerated(ORDINAL)例子

@Entity(name = "Phone")
public static class Phone {

	@Id
	private Long id;

	@Column(name = "phone_number")
	private String number;

	@Enumerated(EnumType.ORDINAL)
	@Column(name = "phone_type")
	private PhoneType type;

	//Getters and setters are omitted for brevity

}

持久化该实体时,Hibernate 生成以下 SQL 语句:

例子 20.用@Enumerated(ORDINAL)Map 持久化一个实体

Phone phone = new Phone( );
phone.setId( 1L );
phone.setNumber( "123-456-78990" );
phone.setType( PhoneType.MOBILE );
entityManager.persist( phone );
INSERT INTO Phone (phone_number, phone_type, id)
VALUES ('123-456-78990', 2, 1)

在 STRING 示例中,phone_type列被定义为(空)VARCHAR 类型,并将保留:

  • NULL

    • 对于空值
  • LAND_LINE

    • 对于LAND_LINE枚举
  • MOBILE

    • 对于MOBILE枚举

例子 21. @Enumerated(STRING)例子

@Entity(name = "Phone")
public static class Phone {

	@Id
	private Long id;

	@Column(name = "phone_number")
	private String number;

	@Enumerated(EnumType.STRING)
	@Column(name = "phone_type")
	private PhoneType type;

	//Getters and setters are omitted for brevity

}

@Enumerated(ORDINAL)示例中的实体相同,Hibernate 生成以下 SQL 语句:

例子 22.用@Enumerated(STRING)Map 持久化一个实体

INSERT INTO Phone (phone_number, phone_type, id)
VALUES ('123-456-78990', 'MOBILE', 1)
AttributeConverter

让我们考虑以下Gender枚举,该枚举使用'M''F'代码存储其值。

例子 23.带有自定义构造函数的枚举

public enum Gender {

    MALE( 'M' ),
    FEMALE( 'F' );

    private final char code;

    Gender(char code) {
        this.code = code;
    }

    public static Gender fromCode(char code) {
        if ( code == 'M' || code == 'm' ) {
            return MALE;
        }
        if ( code == 'F' || code == 'f' ) {
            return FEMALE;
        }
        throw new UnsupportedOperationException(
            "The code " + code + " is not supported!"
        );
    }

    public char getCode() {
        return code;
    }
}

您可以使用 JPA 2.1 AttributeConverter 以符合 JPA 的方式 Map 枚举。

例子 24.带有AttributeConverter例子的枚举 Map

@Entity(name = "Person")
public static class Person {

	@Id
	private Long id;

	private String name;

	@Convert( converter = GenderConverter.class )
	public Gender gender;

	//Getters and setters are omitted for brevity

}

@Converter
public static class GenderConverter
		implements AttributeConverter<Gender, Character> {

	public Character convertToDatabaseColumn( Gender value ) {
		if ( value == null ) {
			return null;
		}

		return value.getCode();
	}

	public Gender convertToEntityAttribute( Character value ) {
		if ( value == null ) {
			return null;
		}

		return Gender.fromCode( value );
	}
}

在这里,gender 列定义为 CHAR 类型,并将保留:

  • NULL

    • 对于空值
  • 'M'

    • 对于MALE枚举
  • 'F'

    • 对于FEMALE枚举

有关使用 AttributeConverters 的其他详细信息,请参见JPA 2.1 AttributeConverters部分。

Note

JPA 明确禁止使用带有标记为@Enumerated的属性的AttributeConverter

因此,在使用AttributeConverter方法时,请确保不要将属性标记为@Enumerated

使用 AttributeConverter 实体属性作为查询参数

假设您具有以下实体:

例子 25. Photo实体和AttributeConverter

@Entity(name = "Photo")
public static class Photo {

	@Id
	private Integer id;

	private String name;

	@Convert(converter = CaptionConverter.class)
	private Caption caption;

	//Getters and setters are omitted for brevity
}

Caption类如下所示:

例子 26. Caption Java 对象

public static class Caption {

	private String text;

	public Caption(String text) {
		this.text = text;
	}

	public String getText() {
		return text;
	}

	public void setText(String text) {
		this.text = text;
	}

	@Override
	public boolean equals(Object o) {
		if ( this == o ) {
			return true;
		}
		if ( o == null || getClass() != o.getClass() ) {
			return false;
		}
		Caption caption = (Caption) o;
		return text != null ? text.equals( caption.text ) : caption.text == null;

	}

	@Override
	public int hashCode() {
		return text != null ? text.hashCode() : 0;
	}
}

我们有一个AttributeConverter处理Caption Java 对象:

例子 27. Caption Java 对象 AttributeConverter

public static class CaptionConverter
		implements AttributeConverter<Caption, String> {

	@Override
	public String convertToDatabaseColumn(Caption attribute) {
		return attribute.getText();
	}

	@Override
	public Caption convertToEntityAttribute(String dbData) {
		return new Caption( dbData );
	}
}

传统上,在引用caption实体属性时,只能使用 DB 数据Caption表示形式(在我们的情况下为String)。

例子 28.使用数据库数据表示按Caption属性过滤

Photo photo = entityManager.createQuery(
	"select p " +
	"from Photo p " +
	"where upper(caption) = upper(:caption) ", Photo.class )
.setParameter( "caption", "Nicolae Grigorescu" )
.getSingleResult();

为了使用 Java 对象Caption表示,您必须获取关联的 Hibernate Type

例子 29.使用 Java Object 表示按Caption属性过滤

SessionFactory sessionFactory = entityManager.getEntityManagerFactory()
		.unwrap( SessionFactory.class );

MetamodelImplementor metamodelImplementor = (MetamodelImplementor) sessionFactory.getMetamodel();

Type captionType = metamodelImplementor
		.entityPersister( Photo.class.getName() )
		.getPropertyType( "caption" );

Photo photo = (Photo) entityManager.createQuery(
	"select p " +
	"from Photo p " +
	"where upper(caption) = upper(:caption) ", Photo.class )
.unwrap( Query.class )
.setParameter( "caption", new Caption("Nicolae Grigorescu"), captionType)
.getSingleResult();

通过传递关联的 Hibernate Type,可以在绑定查询参数值时使用Caption对象。

使用 HBMMapMapAttributeConverter

使用 HBMMap 时,您仍然可以使用 JPA AttributeConverter,因为 Hibernate 通过type属性支持这种 Map,如以下示例所示。

让我们考虑一下我们有一个特定于应用程序的Money类型:

例子 30.特定于应用的Money类型

public class Money {

    private long cents;

    public Money(long cents) {
        this.cents = cents;
    }

    public long getCents() {
        return cents;
    }

    public void setCents(long cents) {
        this.cents = cents;
    }
}

现在,我们想在 MapAccount实体时使用Money类型:

例子 31. Account使用Money类型的实体

public class Account {

    private Long id;

    private String owner;

    private Money balance;

    //Getters and setters are omitted for brevity
}

由于 Hibernate 不知道如何持久化Money类型,因此我们可以使用 JPA AttributeConverterMoney类型转换为Long。为此,我们将使用以下MoneyConverterUtil:

例子 32. MoneyConverter实现 JPA AttributeConverter接口

public class MoneyConverter
        implements AttributeConverter<Money, Long> {

    @Override
    public Long convertToDatabaseColumn(Money attribute) {
        return attribute == null ? null : attribute.getCents();
    }

    @Override
    public Money convertToEntityAttribute(Long dbData) {
        return dbData == null ? null : new Money( dbData );
    }
}

要使用 HBM 配置文件 MapMoneyConverter,您需要在property元素的type属性中使用converted::前缀。

例子 33. AttributeConverter的 HBMMap

<?xml version="1.0"?>
<!DOCTYPE hibernate-mapping PUBLIC
        "-//Hibernate/Hibernate Mapping DTD 3.0//EN"
        "http://www.hibernate.org/dtd/hibernate-mapping-3.0.dtd">

<hibernate-mapping package="org.hibernate.userguide.mapping.converter.hbm">
    <class name="Account" table="account" >
        <id name="id"/>

        <property name="owner"/>

        <property name="balance"
            type="converted::org.hibernate.userguide.mapping.converter.hbm.MoneyConverter"/>

    </class>
</hibernate-mapping>
Custom type

您还可以使用 Hibernate 自定义类型 Map 来 Map 枚举。让我们再次回顾 Gender 枚举示例,这次使用自定义类型存储更标准化的'M''F'代码。

例子 34.带有自定义类型的枚举 Map 例子

@Entity(name = "Person")
public static class Person {

	@Id
	private Long id;

	private String name;

	@Type( type = "org.hibernate.userguide.mapping.basic.GenderType" )
	public Gender gender;

	//Getters and setters are omitted for brevity

}

public class GenderType extends AbstractSingleColumnStandardBasicType<Gender> {

    public static final GenderType INSTANCE = new GenderType();

    public GenderType() {
        super(
            CharTypeDescriptor.INSTANCE,
            GenderJavaTypeDescriptor.INSTANCE
        );
    }

    public String getName() {
        return "gender";
    }

    @Override
    protected boolean registerUnderJavaType() {
        return true;
    }
}

public class GenderJavaTypeDescriptor extends AbstractTypeDescriptor<Gender> {

    public static final GenderJavaTypeDescriptor INSTANCE =
        new GenderJavaTypeDescriptor();

    protected GenderJavaTypeDescriptor() {
        super( Gender.class );
    }

    public String toString(Gender value) {
        return value == null ? null : value.name();
    }

    public Gender fromString(String string) {
        return string == null ? null : Gender.valueOf( string );
    }

    public <X> X unwrap(Gender value, Class<X> type, WrapperOptions options) {
        return CharacterTypeDescriptor.INSTANCE.unwrap(
            value == null ? null : value.getCode(),
            type,
            options
        );
    }

    public <X> Gender wrap(X value, WrapperOptions options) {
        return Gender.fromCode(
            CharacterTypeDescriptor.INSTANCE.wrap( value, options )
        );
    }
}

同样,gender 列被定义为 CHAR 类型,并将保留:

  • NULL

    • 对于空值
  • 'M'

    • 对于MALE枚举
  • 'F'

    • 对于FEMALE枚举

有关使用自定义类型的其他详细信息,请参见Custom BasicTypes部分。

2.3.8. MapLOB

MapLOB(数据库大对象)有两种形式,一种使用 JDBC 定位器类型,另一种用于实现 LOB 数据。

存在 JDBC LOB 定位器以允许有效访问 LOB 数据。它们允许 JDBC 驱动程序根据需要流式传输 LOB 数据的一部分,从而潜在地释放内存空间。但是,它们可能不自然地处理并且具有一定的局限性。例如,LOB 定位器仅在获得它的 Transaction 期间有效。

物化 LOB 的想法是,使用熟悉的 Java 类型(例如Stringbyte[]等)为这些 LOB 权衡潜在的效率(并非所有驱动程序都有效地处理 LOB 数据),以实现更自然的编程范例。

物化处理内存中的整个 LOB 内容,而 LOB 定位器(理论上)允许根据需要将部分 LOB 内容流式传输到内存中。

JDBC LOB 定位器类型包括:

  • java.sql.Blob

  • java.sql.Clob

  • java.sql.NClob

Map 这些 LOB 值的实体化形式将使用更熟悉的 Java 类型,例如Stringchar[]byte[]等。“更熟悉”的权衡通常是性能。

Mapping CLOB

乍一看,假设我们有一个要 Map 的CLOB列(Map 民族化字符数据部分将介绍NCLOB字符LOB数据)。

考虑到我们有以下数据库表:

例子 35. CLOB-SQL

CREATE TABLE Product (
  id INTEGER NOT NULL,
  name VARCHAR(255),
  warranty CLOB,
  PRIMARY KEY (id)
)

首先使用@Lob JPA 注解和java.sql.Clob类型对此进行 Map:

例子 36. CLOBMap 到java.sql.Clob

@Entity(name = "Product")
public static class Product {

    @Id
    private Integer id;

    private String name;

    @Lob
    private Clob warranty;

    //Getters and setters are omitted for brevity

}

要保留这样的实体,您必须使用ClobProxy HibernateUtil 创建Clob

例子 37.坚持一个java.sql.Clob实体

String warranty = "My product warranty";

final Product product = new Product();
product.setId( 1 );
product.setName( "Mobile phone" );

product.setWarranty( ClobProxy.generateProxy( warranty ) );

entityManager.persist( product );

要检索Clob的内容,您需要转换基础的java.io.Reader

例子 38.返回一个java.sql.Clob实体

Product product = entityManager.find( Product.class, productId );

try (Reader reader = product.getWarranty().getCharacterStream()) {
    assertEquals( "My product warranty", toString( reader ) );
}

我们还可以将物化形式 Map 到 CLOB。这样,我们可以使用Stringchar[]

例子 39. CLOBMap 到String

@Entity(name = "Product")
public static class Product {

	@Id
	private Integer id;

	private String name;

	@Lob
	private String warranty;

	//Getters and setters are omitted for brevity

}

Note

JDBC 处理LOB数据的方式因驱动程序而异,并且 Hibernate 尝试代表您处理所有这些差异。

但是,某些驱动程序比较棘手(例如 PostgreSQL),在这种情况下,您可能需要执行一些额外的步骤才能使 LOB 正常工作。此类讨论超出了本指南的范围。

我们甚至可能希望将物化数据作为 char 数组(尽管这可能不是一个好主意)。

例子 40. CLOB-实现char[]Map

@Entity(name = "Product")
public static class Product {

	@Id
	private Integer id;

	private String name;

	@Lob
	private char[] warranty;

	//Getters and setters are omitted for brevity

}
Mapping BLOB

BLOB数据以类似的方式 Map。

考虑到我们有以下数据库表:

例子 41. BLOB-SQL

CREATE TABLE Product (
    id INTEGER NOT NULL ,
    image blob ,
    name VARCHAR(255) ,
    PRIMARY KEY ( id )
)

让我们首先使用 JDBC java.sql.Blob类型对此进行 Map。

例子 42. BLOBMap 到java.sql.Blob

@Entity(name = "Product")
public static class Product {

    @Id
    private Integer id;

    private String name;

    @Lob
    private Blob image;

    //Getters and setters are omitted for brevity

}

要保留这样的实体,您必须使用BlobProxy HibernateUtil 创建Blob

例子 43.坚持一个java.sql.Blob实体

byte[] image = new byte[] {1, 2, 3};

final Product product = new Product();
product.setId( 1 );
product.setName( "Mobile phone" );

product.setImage( BlobProxy.generateProxy( image ) );

entityManager.persist( product );

要检索Blob的内容,您需要转换基础的java.io.InputStream

例子 44.返回一个java.sql.Blob实体

Product product = entityManager.find( Product.class, productId );

try (InputStream inputStream = product.getImage().getBinaryStream()) {
    assertArrayEquals(new byte[] {1, 2, 3}, toBytes( inputStream ) );
}

我们还可以将物化形式的 BLOBMap(例如byte[])。

例子 45. BLOBMap 到byte[]

@Entity(name = "Product")
public static class Product {

    @Id
    private Integer id;

    private String name;

    @Lob
    private byte[] image;

    //Getters and setters are omitted for brevity

}

2.3.9. Map 民族化字符数据

JDBC 4 添加了显式处理国有化字符数据的功能。为此,它添加了特定的国有化字符数据类型:

  • NCHAR

  • NVARCHAR

  • LONGNVARCHAR

  • NCLOB

考虑到我们有以下数据库表:

例子 46. NVARCHAR-SQL

CREATE TABLE Product (
    id INTEGER NOT NULL ,
    name VARCHAR(255) ,
    warranty NVARCHAR(255) ,
    PRIMARY KEY ( id )
)

为了将特定的属性 Map 到国家化的变量数据类型,Hibernate 定义了@NationalizedComments。

例子 47. NVARCHARMap

@Entity(name = "Product")
public static class Product {

    @Id
    private Integer id;

    private String name;

    @Nationalized
    private String warranty;

    //Getters and setters are omitted for brevity

}

就像CLOB一样,Hibernate 也可以处理NCLOB SQL 数据类型:

例子 48. NCLOB-SQL

CREATE TABLE Product (
    id INTEGER NOT NULL ,
    name VARCHAR(255) ,
    warranty nclob ,
    PRIMARY KEY ( id )
)

Hibernate 可以将NCLOBMap 到java.sql.NClob

例子 49. NCLOBMap 到java.sql.NClob

@Entity(name = "Product")
public static class Product {

    @Id
    private Integer id;

    private String name;

    @Lob
    @Nationalized
    // Clob also works, because NClob extends Clob.
    // The database type is still NCLOB either way and handled as such.
    private NClob warranty;

    //Getters and setters are omitted for brevity

}

要保留这样的实体,您必须使用NClobProxy HibernateUtil 创建NClob

例子 50.坚持一个java.sql.NClob实体

String warranty = "My product warranty";

final Product product = new Product();
product.setId( 1 );
product.setName( "Mobile phone" );

product.setWarranty( NClobProxy.generateProxy( warranty ) );

entityManager.persist( product );

要检索NClob的内容,您需要转换基础的java.io.Reader

例子 51.返回一个java.sql.NClob实体

Product product = entityManager.find( Product.class, productId );

try (Reader reader = product.getWarranty().getCharacterStream()) {
    assertEquals( "My product warranty", toString( reader ) );
}

我们也可以以实体化形式 MapNCLOB。这样,我们可以使用Stringchar[]

例子 52. NCLOBMap 到String

@Entity(name = "Product")
public static class Product {

    @Id
    private Integer id;

    private String name;

    @Lob
    @Nationalized
    private String warranty;

    //Getters and setters are omitted for brevity

}

我们甚至可能希望将物化数据作为 char 数组。

例子 53. NCLOB-物化的char[]Map

@Entity(name = "Product")
public static class Product {

    @Id
    private Integer id;

    private String name;

    @Lob
    @Nationalized
    private char[] warranty;

    //Getters and setters are omitted for brevity

}

Note

如果您的应用程序和数据库使用国有化,则您可能希望启用国有化字符数据作为默认设置。

您可以通过hibernate.use_nationalized_character_data设置或在引导过程中调用MetadataBuilder#enableGlobalNationalizedCharacterDataSupport来执行此操作。

2.3.10. MapUUID 值

Hibernate 还允许您以多种方式 MapUUID 值。

Note

默认的 UUIDMap 是二进制 Map,因为它使用了更有效的列存储。

但是,许多应用程序更喜欢基于字符的列存储的可读性。要切换默认 Map,只需调用MetadataBuilder.applyBasicType( UUIDCharType.INSTANCE, UUID.class.getName() )

2.3.11. UUID 为二进制

如前所述,UUID 属性的默认 Map。使用java.util.UUID#getMostSignificantBitsjava.util.UUID#getLeastSignificantBits将 UUIDMap 到byte[]并将其存储为BINARY数据。

之所以选择默认值,是因为从存储角度来看,它通常更有效。

2.3.12. UUID 为(var)char

使用java.util.UUID#toStringjava.util.UUID#fromString将 UUIDMap 到字符串,并将其存储为CHARVARCHAR数据。

2.3.13. PostgreSQL 特定的 UUID

Tip

使用 PostgreSQL 方言之一时,特定于 PostgreSQL 的 UUID 休眠类型将成为默认的 UUIDMap。

使用 PostgreSQL 特定的 UUID 数据类型 MapUUID。 PostgreSQL JDBC 驱动程序选择将其 UUID 类型 Map 到OTHER代码。请注意,这可能会导致困难,因为驱动程序选择将许多不同的数据类型 Map 到OTHER

2.3.14. UUID 作为标识符

Hibernate 支持使用 UUID 值作为标识符,甚至可以代表用户生成它们。有关详细信息,请参见Identifiers中有关生成器的讨论。

2.3.15. Map 日期/时间值

Hibernate 允许将各种 Java Date/Time 类 Map 为持久域模型实体属性。 SQL 标准定义了三种日期/时间类型:

  • DATE

    • 通过存储年,月和日来表示 calendar 日期。 JDBC 等效为java.sql.Date
  • TIME

    • 表示一天中的时间,并存储小时,分钟和秒。 JDBC 等效为java.sql.Time
  • TIMESTAMP

    • 它存储 DATE 和 TIME 加上纳秒。 JDBC 等效为java.sql.Timestamp

Note

为了避免依赖java.sql包,通常使用java.utiljava.time日期/时间类而不是java.sql.Timestampjava.sql.Time类。

虽然java.sql类定义了与 SQL 日期/时间数据类型的直接关联,但是java.utiljava.time属性需要使用@TemporalComments 显式标记 SQL 类型相关性。这样,可以将java.util.Datejava.util.CalendarMap 到 SQL DATETIMETIMESTAMP类型。

考虑以下实体:

例子 54. java.util.DateMap 为DATE

@Entity(name = "DateEvent")
public static class DateEvent {

	@Id
	@GeneratedValue
	private Long id;

	@Column(name = "`timestamp`")
	@Temporal(TemporalType.DATE)
	private Date timestamp;

	//Getters and setters are omitted for brevity

}

保留此类实体时:

例子 55.保持java.util.DateMap

DateEvent dateEvent = new DateEvent( new Date() );
entityManager.persist( dateEvent );

Hibernate 生成以下 INSERT 语句:

INSERT INTO DateEvent ( timestamp, id )
VALUES ( '2015-12-29', 1 )

仅年,月和日字段被保存到数据库中。

如果我们将@Temporal类型更改为TIME

例子 56. java.util.DateMap 为TIME

@Column(name = "`timestamp`")
@Temporal(TemporalType.TIME)
private Date timestamp;

Hibernate 将发出一个包含小时,分钟和秒的 INSERT 语句。

INSERT INTO DateEvent ( timestamp, id )
VALUES ( '16:51:58', 1 )

@Temporal类型设置为TIMESTAMP时:

例子 57. java.util.DateMap 为TIMESTAMP

@Column(name = "`timestamp`")
@Temporal(TemporalType.TIMESTAMP)
private Date timestamp;

Hibernate 将在 INSERT 语句中同时包含DATETIME和纳秒:

INSERT INTO DateEvent ( timestamp, id )
VALUES ( '2015-12-29 16:54:04.544', 1 )

Note

就像java.util.Date一样,java.util.Calendar需要@Temporal注解,以便知道要选择哪种 JDBC 数据类型:DATETIMETIMESTAMP

如果java.util.Date标记为某个时间点,则java.util.Calendar将考虑默认时区。

MapJava 8 日期/时间值

Java 8 附带了一个新的 Date/Time API,它对即时日期,时间间隔,本地和分区的 Date/Time 不可变实例提供支持,这些实例 Binding 在java.time软件包中。

标准 SQL 日期/时间类型和受支持的 Java 8 日期/时间类类型之间的 Map 如下所示;

  • DATE

    • java.time.LocalDate
  • TIME

    • java.time.LocalTime , java.time.OffsetTime
  • TIMESTAMP

    • java.time.Instantjava.time.LocalDateTimejava.time.OffsetDateTimejava.time.ZonedDateTime

Tip

因为 Java 8 Date/Time 类和 SQL 类型之间的 Map 是隐式的,所以不需要指定@TemporalComments。

将其设置在java.time类上会引发以下异常:

org.hibernate.AnnotationException: @Temporal should only be set on a java.util.Date or java.util.Calendar property
使用特定时区

默认情况下,Hibernate 在保存java.sql.Timestampjava.sql.Time属性时将使用PreparedStatement.setTimestamp(int parameterIndex,java.sql.Timestamp)PreparedStatement.setTime(int parameterIndex,java.sql.Time x)

如果未指定时区,则 JDBC 驱动程序将使用底层的 JVM 默认时区,如果在 Global 范围内使用该应用程序,则可能不适合。因此,每当从数据库中保存/加载数据时,通常都使用单个参考时区(例如 UTC)。

一种替代方法是将所有 JVM 配置为使用参考时区:

  • Declaratively

java -Duser.timezone = UTC ...


 - Programmatically

   - ```java
TimeZone.setDefault( TimeZone.getTimeZone( "UTC" ) );

但是,如this article中所述,这并不总是可行的,尤其是对于前端节点。因此,Hibernate 提供了hibernate.jdbc.time_zone配置属性,该属性可以配置:

  • 声明性地,在SessionFactory级别

settings.put(
AvailableSettings.JDBC_TIME_ZONE,
TimeZone.getTimeZone(“ UTC”)
);


 - Programmatically, on a per  `Session`  basis

   - ```java
Session session = sessionFactory()
    .withOptions()
    .jdbcTimeZone( TimeZone.getTimeZone( "UTC" ) )
    .openSession();

使用此配置属性后,Hibernate 将调用PreparedStatement.setTimestamp(int parameterIndex,java.sql.Timestamp,Calendar cal)PreparedStatement.setTime(int parameterIndex,java.sql.Time x,Calendar cal),其中java.util.Calendar引用通过hibernate.jdbc.time_zone属性提供的时区。

2.3.16. JPA 2.1 AttributeConverters

尽管 Hibernate 长期以来一直提供custom types作为 JPA 2.1 提供程序,但它也支持AttributeConverter

使用自定义AttributeConverter,应用程序开发人员可以将给定的 JDBC 类型 Map 到实体基本类型。

在下面的示例中,java.time.Period将 Map 到VARCHAR数据库列。

例子 58. java.time.Period自定义AttributeConverter

@Converter
public class PeriodStringConverter
        implements AttributeConverter<Period, String> {

    @Override
    public String convertToDatabaseColumn(Period attribute) {
        return attribute.toString();
    }

    @Override
    public Period convertToEntityAttribute(String dbData) {
        return Period.parse( dbData );
    }
}

要使用此自定义转换器,@ConvertComments 必须修饰实体属性。

例子 59.使用自定义java.time.Period AttributeConverterMap 的实体

@Entity(name = "Event")
public static class Event {

    @Id
    @GeneratedValue
    private Long id;

    @Convert(converter = PeriodStringConverter.class)
    @Column(columnDefinition = "")
    private Period span;

    //Getters and setters are omitted for brevity

}

当持久化此类实体时,Hibernate 将基于AttributeConverter逻辑进行类型转换:

例子 60.使用自定义AttributeConverter持久化实体

INSERT INTO Event ( span, id )
VALUES ( 'P1Y2M3D', 1 )
AttributeConverter Java 和 JDBC 类型

如果为转换的“数据库端”指定的 Java 类型(第二个AttributeConverter绑定参数)未知,则 Hibernate 将回退为java.io.Serializable类型。

如果 Hibernate 不知道 Java 类型,您将遇到以下消息:

Note

HHH000481:遇到 Java 类型,我们无法为其找到 JavaTypeDescriptor,并且该 Java 类型似乎未实现 equals 和/或 hashCode。执行涉及此 Java 类型的相等/脏检查时,这可能导致严重的性能问题。考虑注册一个自定义 JavaTypeDescriptor 或至少实现 equals/hashCode。

Java 类型是否为“已知”意味着它在JavaTypeDescriptorRegistry中具有一个条目。虽然默认情况下,Hibernate 将许多 JDK 类型加载到JavaTypeDescriptorRegistry,但是应用程序还可以通过添加新的JavaTypeDescriptor条目来扩展JavaTypeDescriptorRegistry

这样,Hibernate 也将知道如何在 JDBC 级别处理特定的 Java 对象类型。

JPA 2.1 AttributeConverter 可变性计划

如果基础 Java 类型是不可变的,则由 JPA AttributeConverter转换的基本类型是不可变的;如果关联的属性类型也是可变的,则该类型是可变的。

因此,可变性由关联实体属性类型的JavaTypeDescriptor#getMutabilityPlan给出。

Immutable types

如果实体属性是String,原始包装器(例如IntegerLong),枚举类型或任何其他不可变的Object类型,则只能通过将其重新分配为新值来更改它。

考虑到我们具有与JPA 2.1 AttributeConverters部分中所示的相同的Period实体属性:

@Entity(name = "Event")
public static class Event {

    @Id
    @GeneratedValue
    private Long id;

    @Convert(converter = PeriodStringConverter.class)
    @Column(columnDefinition = "")
    private Period span;

    //Getters and setters are omitted for brevity

}

更改span属性的唯一方法是将其重新分配为其他值:

Event event = entityManager.createQuery( "from Event", Event.class ).getSingleResult();
event.setSpan(Period
    .ofYears( 3 )
    .plusMonths( 2 )
    .plusDays( 1 )
);
Mutable types

另一方面,请考虑以下示例,其中Money类型是可变的。

public static class Money {

	private long cents;

	//Getters and setters are omitted for brevity
}

@Entity(name = "Account")
public static class Account {

	@Id
	private Long id;

	private String owner;

	@Convert(converter = MoneyConverter.class)
	private Money balance;

	//Getters and setters are omitted for brevity
}

public static class MoneyConverter
		implements AttributeConverter<Money, Long> {

	@Override
	public Long convertToDatabaseColumn(Money attribute) {
		return attribute == null ? null : attribute.getCents();
	}

	@Override
	public Money convertToEntityAttribute(Long dbData) {
		return dbData == null ? null : new Money( dbData );
	}
}

可变的Object允许您修改其内部结构,而 Hibernate 脏检查机制将把更改传播到数据库:

Account account = entityManager.find( Account.class, 1L );
account.getBalance().setCents( 150 * 100L );
entityManager.persist( account );

Tip

尽管AttributeConverter类型是可变的,以便脏检查,深度复制和二级缓存正常工作,但将它们视为不可变的(实际上是不可变的)更为有效。

因此,在可能的情况下,最好使用不可变类型而不是可变类型。

2.3.17. SQL 带引号的标识符

您可以通过将表名或列名放在 Map 文档的反引号中来强制 Hibernate 在生成的 SQL 中用标识符引起来。传统上,Hibernate 使用反引号转义 SQL 保留关键字,而 JPA 则使用双引号。

一旦保留的关键字被转义,Hibernate 将对 SQL Dialect使用正确的引号样式。这通常是双引号,但是 SQL Server 使用方括号,而 MySQL 使用反引号。

例子 61.休眠传统的报价

@Entity(name = "Product")
public static class Product {

	@Id
	private Long id;

	@Column(name = "`name`")
	private String name;

	@Column(name = "`number`")
	private String number;

	//Getters and setters are omitted for brevity

}

例子 62. JPA 引用

@Entity(name = "Product")
public static class Product {

	@Id
	private Long id;

	@Column(name = "\"name\"")
	private String name;

	@Column(name = "\"number\"")
	private String number;

	//Getters and setters are omitted for brevity

}

因为namenumber是保留字,所以Product实体 Map 使用反引号来引用这些列名。

保存以下Product entity时,Hibernate 会生成以下 SQL 插入语句:

例子 63.保留一个带引号的列名

Product product = new Product();
product.setId( 1L );
product.setName( "Mobile phone" );
product.setNumber( "123-456-7890" );
entityManager.persist( product );
INSERT INTO Product ("name", "number", id)
VALUES ('Mobile phone', '123-456-7890', 1)
Global quoting

Hibernate 还可以使用以下配置属性引用所有标识符(例如表,列):

<property
    name="hibernate.globally_quoted_identifiers"
    value="true"
/>

这样,我们不需要手动引用任何标识符:

例子 64. JPA 引用

@Entity(name = "Product")
public static class Product {

	@Id
	private Long id;

	private String name;

	private String number;

	//Getters and setters are omitted for brevity

}

持久化Product实体时,Hibernate 将引用所有标识符,如以下示例所示:

INSERT INTO "Product" ("name", "number", "id")
VALUES ('Mobile phone', '123-456-7890', 1)

如您所见,表名和所有列均已被引用。

有关与报价相关的配置属性的更多信息,请同时查看Mapping configurations部分。

2.3.18. 生成的属性

生成的属性是其值由数据库生成的属性。通常,Hibernate 应用程序需要refresh个对象,这些对象包含数据库为其生成值的任何属性。但是,将属性标记为已生成可让应用程序将此职责委派给 Hibernate。当 Hibernate 对已定义生成属性的实体发出 SQL INSERT 或 UPDATE 时,它将立即发出选择以检索生成的值。

标记为已生成的属性还必须是不可插入不可更新。只能将@Version@Basic类型标记为已生成。

  • NEVER(默认值)

    • 给定的属性值不在数据库内生成。
  • INSERT

    • 给定的属性值在插入时生成,但在后续更新中不会重新生成。 * creationTimestamp *之类的属性属于此类别。
  • ALWAYS

    • 属性值是在插入和更新时生成的。

要将属性标记为已生成,请使用特定于 Hibernate 的@GeneratedComments。

@Generated annotation

使用@Generated注解,以便 Hibernate 在持久保存或更新实体后可以获取当前注解的属性。因此,@Generated注解接受GenerationTime枚举值。

考虑以下实体:

例子 65. @GeneratedMap 例子

@Entity(name = "Person")
public static class Person {

	@Id
	private Long id;

	private String firstName;

	private String lastName;

	private String middleName1;

	private String middleName2;

	private String middleName3;

	private String middleName4;

	private String middleName5;

	@Generated( value = GenerationTime.ALWAYS )
	@Column(columnDefinition =
		"AS CONCAT(" +
		"	COALESCE(firstName, ''), " +
		"	COALESCE(' ' + middleName1, ''), " +
		"	COALESCE(' ' + middleName2, ''), " +
		"	COALESCE(' ' + middleName3, ''), " +
		"	COALESCE(' ' + middleName4, ''), " +
		"	COALESCE(' ' + middleName5, ''), " +
		"	COALESCE(' ' + lastName, '') " +
		")")
	private String fullName;

}

Person实体保留后,Hibernate 将从数据库中获取计算出的fullName列,该列将名字,中间名和姓氏连接在一起。

例子 66. @Generated坚持的例子

Person person = new Person();
person.setId( 1L );
person.setFirstName( "John" );
person.setMiddleName1( "Flávio" );
person.setMiddleName2( "André" );
person.setMiddleName3( "Frederico" );
person.setMiddleName4( "Rúben" );
person.setMiddleName5( "Artur" );
person.setLastName( "Doe" );

entityManager.persist( person );
entityManager.flush();

assertEquals("John Flávio André Frederico Rúben Artur Doe", person.getFullName());
INSERT INTO Person
(
    firstName,
    lastName,
    middleName1,
    middleName2,
    middleName3,
    middleName4,
    middleName5,
    id
)
values
(?, ?, ?, ?, ?, ?, ?, ?)

-- binding parameter [1] as [VARCHAR] - [John]
-- binding parameter [2] as [VARCHAR] - [Doe]
-- binding parameter [3] as [VARCHAR] - [Flávio]
-- binding parameter [4] as [VARCHAR] - [André]
-- binding parameter [5] as [VARCHAR] - [Frederico]
-- binding parameter [6] as [VARCHAR] - [Rúben]
-- binding parameter [7] as [VARCHAR] - [Artur]
-- binding parameter [8] as [BIGINT]  - [1]

SELECT
    p.fullName as fullName3_0_
FROM
    Person p
WHERE
    p.id=?

-- binding parameter [1] as [BIGINT] - [1]
-- extracted value ([fullName3_0_] : [VARCHAR]) - [John Flávio André Frederico Rúben Artur Doe]

Person实体更新时也是如此。修改实体后,Hibernate 将从数据库中获取计算出的fullName列。

例子 67. @Generated更新例子

Person person = entityManager.find( Person.class, 1L );
person.setLastName( "Doe Jr" );

entityManager.flush();
assertEquals("John Flávio André Frederico Rúben Artur Doe Jr", person.getFullName());
UPDATE
    Person
SET
    firstName=?,
    lastName=?,
    middleName1=?,
    middleName2=?,
    middleName3=?,
    middleName4=?,
    middleName5=?
WHERE
    id=?

-- binding parameter [1] as [VARCHAR] - [John]
-- binding parameter [2] as [VARCHAR] - [Doe Jr]
-- binding parameter [3] as [VARCHAR] - [Flávio]
-- binding parameter [4] as [VARCHAR] - [André]
-- binding parameter [5] as [VARCHAR] - [Frederico]
-- binding parameter [6] as [VARCHAR] - [Rúben]
-- binding parameter [7] as [VARCHAR] - [Artur]
-- binding parameter [8] as [BIGINT]  - [1]

SELECT
    p.fullName as fullName3_0_
FROM
    Person p
WHERE
    p.id=?

-- binding parameter [1] as [BIGINT] - [1]
-- extracted value ([fullName3_0_] : [VARCHAR]) - [John Flávio André Frederico Rúben Artur Doe Jr]
@GeneratorType annotation

使用@GeneratorType注解,以便您可以提供一个自定义生成器来设置当前已注解的属性的值。

因此,@GeneratorType注解接受GenerationTime枚举值和自定义ValueGenerator类类型。

考虑以下实体:

例子 68. @GeneratorTypeMap 例子

public static class CurrentUser {

	public static final CurrentUser INSTANCE = new CurrentUser();

	private static final ThreadLocal<String> storage = new ThreadLocal<>();

	public void logIn(String user) {
		storage.set( user );
	}

	public void logOut() {
		storage.remove();
	}

	public String get() {
		return storage.get();
	}
}

public static class LoggedUserGenerator implements ValueGenerator<String> {

	@Override
	public String generateValue(
			Session session, Object owner) {
		return CurrentUser.INSTANCE.get();
	}
}

@Entity(name = "Person")
public static class Person {

	@Id
	private Long id;

	private String firstName;

	private String lastName;

	@GeneratorType( type = LoggedUserGenerator.class, when = GenerationTime.INSERT)
	private String createdBy;

	@GeneratorType( type = LoggedUserGenerator.class, when = GenerationTime.ALWAYS)
	private String updatedBy;

}

保留Person实体后,Hibernate 将使用当前登录的用户填充createdBy列。

例子 69. @Generated坚持的例子

CurrentUser.INSTANCE.logIn( "Alice" );

doInJPA( this::entityManagerFactory, entityManager -> {

	Person person = new Person();
	person.setId( 1L );
	person.setFirstName( "John" );
	person.setLastName( "Doe" );

	entityManager.persist( person );
} );

CurrentUser.INSTANCE.logOut();
INSERT INTO Person
(
    createdBy,
    firstName,
    lastName,
    updatedBy,
    id
)
VALUES
(?, ?, ?, ?, ?)

-- binding parameter [1] as [VARCHAR] - [Alice]
-- binding parameter [2] as [VARCHAR] - [John]
-- binding parameter [3] as [VARCHAR] - [Doe]
-- binding parameter [4] as [VARCHAR] - [Alice]
-- binding parameter [5] as [BIGINT]  - [1]

Person实体更新时也是如此。 Hibernate 将使用当前登录的用户填充updatedBy列。

例子 70. @Generated更新例子

CurrentUser.INSTANCE.logIn( "Bob" );

doInJPA( this::entityManagerFactory, entityManager -> {
	Person person = entityManager.find( Person.class, 1L );
	person.setFirstName( "Mr. John" );
} );

CurrentUser.INSTANCE.logOut();
UPDATE Person
SET
    createdBy = ?,
    firstName = ?,
    lastName = ?,
    updatedBy = ?
WHERE
    id = ?

-- binding parameter [1] as [VARCHAR] - [Alice]
-- binding parameter [2] as [VARCHAR] - [Mr. John]
-- binding parameter [3] as [VARCHAR] - [Doe]
-- binding parameter [4] as [VARCHAR] - [Bob]
-- binding parameter [5] as [BIGINT]  - [1]
@CreationTimestamp annotation

持久化实体时,@CreationTimestampComments 指示 Hibernate 使用 JVM 的当前时间戳值设置带 Comments 的实体属性。

支持的属性类型为:

  • java.util.Date

  • java.util.Calendar

  • java.sql.Date

  • java.sql.Time

  • java.sql.Timestamp

例子 71. @CreationTimestampMap 例子

@Entity(name = "Event")
public static class Event {

	@Id
	@GeneratedValue
	private Long id;

	@Column(name = "`timestamp`")
	@CreationTimestamp
	private Date timestamp;

	//Constructors, getters, and setters are omitted for brevity
}

保留Event实体后,Hibernate 将使用当前的 JVM 时间戳值填充基础timestamp列:

例子 72. @CreationTimestamp坚持的例子

Event dateEvent = new Event( );
entityManager.persist( dateEvent );
INSERT INTO Event ("timestamp", id)
VALUES (?, ?)

-- binding parameter [1] as [TIMESTAMP] - [Tue Nov 15 16:24:20 EET 2016]
-- binding parameter [2] as [BIGINT]    - [1]
@UpdateTimestamp annotation

持久化实体时,@UpdateTimestampComments 指示 Hibernate 使用 JVM 的当前时间戳值设置带 Comments 的实体属性。

支持的属性类型为:

  • java.util.Date

  • java.util.Calendar

  • java.sql.Date

  • java.sql.Time

  • java.sql.Timestamp

例子 73. @UpdateTimestampMap 例子

@Entity(name = "Bid")
public static class Bid {

	@Id
	@GeneratedValue
	private Long id;

	@Column(name = "updated_on")
	@UpdateTimestamp
	private Date updatedOn;

	@Column(name = "updated_by")
	private String updatedBy;

	private Long cents;

	//Getters and setters are omitted for brevity

}

保留Bid实体后,Hibernate 将使用当前的 JVM 时间戳值填充基础updated_on列:

例子 74. @UpdateTimestamp坚持的例子

Bid bid = new Bid();
bid.setUpdatedBy( "John Doe" );
bid.setCents( 150 * 100L );
entityManager.persist( bid );
INSERT INTO Bid (cents, updated_by, updated_on, id)
VALUES (?, ?, ?, ?)

-- binding parameter [1] as [BIGINT]    - [15000]
-- binding parameter [2] as [VARCHAR]   - [John Doe]
-- binding parameter [3] as [TIMESTAMP] - [Tue Apr 18 17:21:46 EEST 2017]
-- binding parameter [4] as [BIGINT]    - [1]

更新Bid实体时,Hibernate 将使用当前 JVM 时间戳值修改updated_on列:

例子 75. @UpdateTimestamp更新例子

Bid bid = entityManager.find( Bid.class, 1L );

bid.setUpdatedBy( "John Doe Jr." );
bid.setCents( 160 * 100L );
entityManager.persist( bid );
UPDATE Bid SET
    cents = ?,
    updated_by = ?,
    updated_on = ?
where
    id = ?

-- binding parameter [1] as [BIGINT]    - [16000]
-- binding parameter [2] as [VARCHAR]   - [John Doe Jr.]
-- binding parameter [3] as [TIMESTAMP] - [Tue Apr 18 17:49:24 EEST 2017]
-- binding parameter [4] as [BIGINT]    - [1]
@ValueGenerationType meta-annotation

Hibernate 4.3 引入了@ValueGenerationType元 Comments,这是一种声明生成的属性或定制生成器的新方法。

@Generated已被改装为使用@ValueGenerationType元 Comments。但是@ValueGenerationType所提供的功能比@Generated当前所支持的功能更多,并且要利用其中的某些功能,您只需连接一个新的生成器 Comments。

正如您将在以下示例中看到的那样,在声明用于标记需要特定生成策略的实体属性的自定义 Comments 时,将使用@ValueGenerationType元 Comments。必须将实际的生成逻辑添加到实现AnnotationValueGeneration接口的类中。

Database-generated values

例如,假设我们希望通过对标准 ANSI SQL 函数current_timestamp(而不是触发器或 DEFAULT 值)的调用来生成时间戳:

例子 76.一个用于数据库生成的ValueGenerationTypeMap

@Entity(name = "Event")
public static class Event {

	@Id
	@GeneratedValue
	private Long id;

	@Column(name = "`timestamp`")
	@FunctionCreationTimestamp
	private Date timestamp;

	//Constructors, getters, and setters are omitted for brevity
}

@ValueGenerationType(generatedBy = FunctionCreationValueGeneration.class)
@Retention(RetentionPolicy.RUNTIME)
public @interface FunctionCreationTimestamp {}

public static class FunctionCreationValueGeneration
		implements AnnotationValueGeneration<FunctionCreationTimestamp> {

	@Override
	public void initialize(FunctionCreationTimestamp annotation, Class<?> propertyType) {
	}

	/**
	 * Generate value on INSERT
	 * @return when to generate the value
	 */
	public GenerationTiming getGenerationTiming() {
		return GenerationTiming.INSERT;
	}

	/**
	 * Returns null because the value is generated by the database.
	 * @return null
	 */
	public ValueGenerator<?> getValueGenerator() {
		return null;
	}

	/**
	 * Returns true because the value is generated by the database.
	 * @return true
	 */
	public boolean referenceColumnInSql() {
		return true;
	}

	/**
	 * Returns the database-generated value
	 * @return database-generated value
	 */
	public String getDatabaseGeneratedReferencedColumnValue() {
		return "current_timestamp";
	}
}

持久化Event实体时,Hibernate 生成以下 SQL 语句:

INSERT INTO Event ("timestamp", id)
VALUES (current_timestamp, 1)

如您所见,current_timestamp值用于分配timestamp列值。

In-memory-generated values

如果需要在内存中生成时间戳记值,则必须使用以下 Map:

例子 77.用于内存中值生成的ValueGenerationTypeMap

@Entity(name = "Event")
public static class Event {

	@Id
	@GeneratedValue
	private Long id;

	@Column(name = "`timestamp`")
	@FunctionCreationTimestamp
	private Date timestamp;

	//Constructors, getters, and setters are omitted for brevity
}

@ValueGenerationType(generatedBy = FunctionCreationValueGeneration.class)
@Retention(RetentionPolicy.RUNTIME)
public @interface FunctionCreationTimestamp {}

public static class FunctionCreationValueGeneration
		implements AnnotationValueGeneration<FunctionCreationTimestamp> {

	@Override
	public void initialize(FunctionCreationTimestamp annotation, Class<?> propertyType) {
	}

	/**
	 * Generate value on INSERT
	 * @return when to generate the value
	 */
	public GenerationTiming getGenerationTiming() {
		return GenerationTiming.INSERT;
	}

	/**
	 * Returns the in-memory generated value
	 * @return {@code true}
	 */
	public ValueGenerator<?> getValueGenerator() {
		return (session, owner) -> new Date( );
	}

	/**
	 * Returns false because the value is generated by the database.
	 * @return false
	 */
	public boolean referenceColumnInSql() {
		return false;
	}

	/**
	 * Returns null because the value is generated in-memory.
	 * @return null
	 */
	public String getDatabaseGeneratedReferencedColumnValue() {
		return null;
	}
}

持久化Event实体时,Hibernate 生成以下 SQL 语句:

INSERT INTO Event ("timestamp", id)
VALUES ('Tue Mar 01 10:58:18 EET 2016', 1)

如您所见,new Date()对象值用于分配timestamp列值。

2.3.19. 列转换器:读取和写入表达式

Hibernate 允许您自定义用于读取和写入 Map 到@Basic类型的列的值的 SQL。例如,如果您的数据库提供了一组数据加密功能,则可以像下面的示例一样为各个列调用它们。

例子 78. @ColumnTransformer例子

@Entity(name = "Employee")
public static class Employee {

	@Id
	private Long id;

	@NaturalId
	private String username;

	@Column(name = "pswd")
	@ColumnTransformer(
		read = "decrypt( 'AES', '00', pswd  )",
		write = "encrypt('AES', '00', ?)"
	)
	private String password;

	private int accessLevel;

	@ManyToOne(fetch = FetchType.LAZY)
	private Department department;

	@ManyToMany(mappedBy = "employees")
	private List<Project> projects = new ArrayList<>();

	//Getters and setters omitted for brevity
}

如果一个属性使用多个列,则必须使用forColumn属性来指定@ColumnTransformer读写表达式所针对的列。

例子 79. @ColumnTransformer forColumn属性的用法

@Entity(name = "Savings")
public static class Savings {

	@Id
	private Long id;

	@Type(type = "org.hibernate.userguide.mapping.basic.MonetaryAmountUserType")
	@Columns(columns = {
		@Column(name = "money"),
		@Column(name = "currency")
	})
	@ColumnTransformer(
		forColumn = "money",
		read = "money / 100",
		write = "? * 100"
	)
	private MonetaryAmount wallet;

	//Getters and setters omitted for brevity

}

只要在查询中引用了属性,Hibernate 就会自动应用自定义表达式。此功能类似于派生属性@Formula,但有两个区别:

  • 该属性由作为自动模式生成的一部分导出的一列或多列支持。

  • 该属性是可读写的,而不是只读的。

write表达式(如果指定)必须恰好包含一个“?”价值的占位符。

例子 80.持久化具有@ColumnTransformer和复合类型的实体

doInJPA( this::entityManagerFactory, entityManager -> {
	Savings savings = new Savings( );
	savings.setId( 1L );
	savings.setWallet( new MonetaryAmount( BigDecimal.TEN, Currency.getInstance( Locale.US ) ) );
	entityManager.persist( savings );
} );

doInJPA( this::entityManagerFactory, entityManager -> {
	Savings savings = entityManager.find( Savings.class, 1L );
	assertEquals( 10, savings.getWallet().getAmount().intValue());
} );
INSERT INTO Savings (money, currency, id)
VALUES (10 * 100, 'USD', 1)

SELECT
    s.id as id1_0_0_,
    s.money / 100 as money2_0_0_,
    s.currency as currency3_0_0_
FROM
    Savings s
WHERE
    s.id = 1

2.3.20. @Formula

有时,您希望数据库为您而不是在 JVM 中执行一些计算,因此您可能还会创建某种虚拟列。您可以使用 SQL 片段(也称为公式)来代替将属性 Map 到列中。这种属性是只读的(其值由您的公式片段计算)

Note

您应该知道@FormulaComments 带有本机 SQL 子句,这可能会影响数据库的可移植性。

例子 81. @FormulaMap 用法

@Entity(name = "Account")
public static class Account {

	@Id
	private Long id;

	private Double credit;

	private Double rate;

	@Formula(value = "credit * rate")
	private Double interest;

	//Getters and setters omitted for brevity

}

加载Account实体时,Hibernate 将使用配置的@Formula来计算interest属性:

例子 82.用@FormulaMap 持久化一个实体

doInJPA( this::entityManagerFactory, entityManager -> {
	Account account = new Account( );
	account.setId( 1L );
	account.setCredit( 5000d );
	account.setRate( 1.25 / 100 );
	entityManager.persist( account );
} );

doInJPA( this::entityManagerFactory, entityManager -> {
	Account account = entityManager.find( Account.class, 1L );
	assertEquals( Double.valueOf( 62.5d ), account.getInterest());
} );
INSERT INTO Account (credit, rate, id)
VALUES (5000.0, 0.0125, 1)

SELECT
    a.id as id1_0_0_,
    a.credit as credit2_0_0_,
    a.rate as rate3_0_0_,
    a.credit * a.rate as formula0_0_
FROM
    Account a
WHERE
    a.id = 1

Note

@Formula注解定义的 SQL 片段可以任意复杂,甚至可以包含子选择。

2.4. 可嵌入类型

Hibernate 历史上称这些组件。 JPA 称它们为可嵌入对象。无论哪种方式,概念都是相同的:价值构成。

例如,我们可能有一个Publisher类,由namecountry组成,或者有一个Location类,由countrycity组成。

Usage of the word embeddable

为了避免与标记给定可嵌入类型的 Comments 产生任何混淆,该 Comments 将进一步称为@Embeddable

在本章及其后的整个章节中,为了简洁起见,可嵌入类型也可以称为* embeddable *。

例子 83.可嵌入类型的例子

@Embeddable
public static class Publisher {

	private String name;

	private Location location;

	public Publisher(String name, Location location) {
		this.name = name;
		this.location = location;
	}

	private Publisher() {}

	//Getters and setters are omitted for brevity
}

@Embeddable
public static class Location {

	private String country;

	private String city;

	public Location(String country, String city) {
		this.country = country;
		this.city = city;
	}

	private Location() {}

	//Getters and setters are omitted for brevity
}

可嵌入类型是值类型的另一种形式,其生命周期绑定到父实体类型,因此从其父类继承属性访问(有关属性访问的详细信息,请参见Access strategies)。

可嵌入类型可以由基本值以及关联组成,但需要注意的是,当用作集合元素时,它们不能自己定义集合。

2.4.1. 组件/嵌入式

通常,可嵌入类型用于对多个基本类型 Map 进行分组,并在多个实体之间重用它们。

例子 84.简单的可嵌入

@Entity(name = "Book")
public static class Book {

	@Id
	@GeneratedValue
	private Long id;

	private String title;

	private String author;

	private Publisher publisher;

	//Getters and setters are omitted for brevity
}

@Embeddable
public static class Publisher {

	@Column(name = "publisher_name")
	private String name;

	@Column(name = "publisher_country")
	private String country;

	//Getters and setters, equals and hashCode methods omitted for brevity

}
create table Book (
    id bigint not null,
    author varchar(255),
    publisher_country varchar(255),
    publisher_name varchar(255),
    title varchar(255),
    primary key (id)
)

Note

JPA 定义了两个用于可嵌入类型的术语:@Embeddable@Embedded

@Embeddable用于描述 Map 类型本身(例如Publisher)。

@Embedded用于引用给定的可嵌入类型(例如book.publisher)。

因此,可嵌入类型由Publisher类表示,并且父实体通过book#publisher对象组成来使用它。

组合值 Map 到与父表相同的表。组合是良好的面向对象数据建模(惯用 Java)的一部分。实际上,该表也可以由以下实体类型 Map。

例子 85.可嵌入类型组成的替代

@Entity(name = "Book")
public static class Book {

	@Id
	@GeneratedValue
	private Long id;

	private String title;

	private String author;

	@Column(name = "publisher_name")
	private String publisherName;

	@Column(name = "publisher_country")
	private String publisherCountry;

	//Getters and setters are omitted for brevity
}

组合形式当然更面向对象,并且随着我们使用多个可嵌入类型而变得更加明显。

2.4.2. 多种可嵌入类型

尽管从面向对象的角度来看,使用可嵌入类型要方便得多,但是此示例不能按原样工作。当同一父实体类型中多次包含同一可嵌入类型时,JPA 规范要求显式设置关联的列名称。

此要求是由于如何将对象属性 Map 到数据库列。默认情况下,JPA 期望数据库列与其关联的对象属性具有相同的名称。当包含多个可嵌入对象时,基于隐式基于名称的 Map 规则不再起作用,因为多个对象属性最终可能会 Map 到同一数据库列。

我们有一些解决方案。

2.4.3. 覆盖可嵌入类型

JPA 定义了@AttributeOverrideComments 来处理这种情况。这样,可以通过设置显式的基于名称的属性-列类型 Map 来解决 Map 冲突。

如果某个实体中多次使用 Embeddable 类型,则需要使用@AttributeOverride@AssociationOverride注解来覆盖 Embeddable 定义的默认列名称。

考虑到您具有以下Publisher可嵌入类型,该类型定义了与Country实体的@ManyToOne关联:

例子 86.具有@ManyToOne关联的可嵌入类型

@Embeddable
public static class Publisher {

	private String name;

	@ManyToOne(fetch = FetchType.LAZY)
	private Country country;

	//Getters and setters, equals and hashCode methods omitted for brevity

}

@Entity(name = "Country")
public static class Country {

	@Id
	@GeneratedValue
	private Long id;

	@NaturalId
	private String name;

	//Getters and setters are omitted for brevity
}
create table Country (
    id bigint not null,
    name varchar(255),
    primary key (id)
)

alter table Country
    add constraint UK_p1n05aafu73sbm3ggsxqeditd
    unique (name)

现在,如果您有一个Book实体为电子书和平装本声明了两种Publisher可嵌入类型,则不能使用默认的Publisher可嵌入 Map,因为这两个可嵌入列 Map 之间会发生冲突。

因此,Book实体需要覆盖每个Publisher属性的可嵌入类型 Map:

例子 87.重写可嵌入类型属性

@Entity(name = "Book")
@AttributeOverrides({
	@AttributeOverride(
		name = "ebookPublisher.name",
		column = @Column(name = "ebook_publisher_name")
	),
	@AttributeOverride(
		name = "paperBackPublisher.name",
		column = @Column(name = "paper_back_publisher_name")
	)
})
@AssociationOverrides({
	@AssociationOverride(
		name = "ebookPublisher.country",
		joinColumns = @JoinColumn(name = "ebook_publisher_country_id")
	),
	@AssociationOverride(
		name = "paperBackPublisher.country",
		joinColumns = @JoinColumn(name = "paper_back_publisher_country_id")
	)
})
public static class Book {

	@Id
	@GeneratedValue
	private Long id;

	private String title;

	private String author;

	private Publisher ebookPublisher;

	private Publisher paperBackPublisher;

	//Getters and setters are omitted for brevity
}
create table Book (
    id bigint not null,
    author varchar(255),
    ebook_publisher_name varchar(255),
    paper_back_publisher_name varchar(255),
    title varchar(255),
    ebook_publisher_country_id bigint,
    paper_back_publisher_country_id bigint,
    primary key (id)
)

alter table Book
    add constraint FKm39ibh5jstybnslaoojkbac2g
    foreign key (ebook_publisher_country_id)
    references Country

alter table Book
    add constraint FK7kqy9da323p7jw7wvqgs6aek7
    foreign key (paper_back_publisher_country_id)
    references Country

2.4.4. 嵌入式和隐式命名策略

Tip

ImplicitNamingStrategyComponentPathImpl是特定于 Hibernate 的功能。关心 JPA 提供程序可移植性的用户应改为使用@AttributeOverride进行显式列命名。

Naming详细介绍了休眠命名策略。但是,出于本讨论的目的,Hibernate 具有以安全的方式解释隐式列名的能力,可与多种可嵌入类型一起使用。

例子 88.隐式多个可嵌入类型 Map

@Entity(name = "Book")
public static class Book {

	@Id
	@GeneratedValue
	private Long id;

	private String title;

	private String author;

	private Publisher ebookPublisher;

	private Publisher paperBackPublisher;

	//Getters and setters are omitted for brevity
}

@Embeddable
public static class Publisher {

	private String name;

	@ManyToOne(fetch = FetchType.LAZY)
	private Country country;

	//Getters and setters, equals and hashCode methods omitted for brevity
}

@Entity(name = "Country")
public static class Country {

	@Id
	@GeneratedValue
	private Long id;

	@NaturalId
	private String name;

	//Getters and setters are omitted for brevity
}

要使其正常工作,您需要使用ImplicitNamingStrategyComponentPathImpl命名策略。

例子 89.使用组件路径命名策略启用隐式可嵌入类型 Map

metadataBuilder.applyImplicitNamingStrategy(
	ImplicitNamingStrategyComponentPathImpl.INSTANCE
);

现在,隐式列命名中使用了属性的“路径”:

create table Book (
    id bigint not null,
    author varchar(255),
    ebookPublisher_name varchar(255),
    paperBackPublisher_name varchar(255),
    title varchar(255),
    ebookPublisher_country_id bigint,
    paperBackPublisher_country_id bigint,
    primary key (id)
)

您甚至可以开发自己的命名策略来执行其他类型的隐式命名策略。

2.4.5. 可嵌入类型的集合

可嵌入类型的集合是特定值的集合(因为可嵌入类型是值类型)。 值类型的集合中详细介绍了值集合。

2.4.6. 可嵌入类型作为 Map 键

可嵌入类型也可以用作Map键。该主题在Map-键中进行了详细转换。

2.4.7. 可嵌入类型作为标识符

可嵌入类型也可以用作实体类型标识符。 Composite identifiers中详细介绍了此用法。

Tip

用作集合条目,Map 键或实体类型标识符的可嵌入类型不能包含其自己的集合 Map。

2.4.8. @TargetMap

@Target注解用于指定通过接口 Map 的给定关联的实现类。 @ManyToOne@OneToOne@OneToMany@ManyToMany具有targetEntity属性,用于在将接口用于 Map 时指定实体关联的实际类。

出于相同目的,@ElementCollection关联具有targetClass属性。

但是,对于简单的可嵌入类型,没有这样的构造,因此您需要使用特定于 Hibernate 的@TargetComments。

例子 90. @TargetMap 用法

public interface Coordinates {
	double x();
	double y();
}

@Embeddable
public static class GPS implements Coordinates {

	private double latitude;

	private double longitude;

	private GPS() {
	}

	public GPS(double latitude, double longitude) {
		this.latitude = latitude;
		this.longitude = longitude;
	}

	@Override
	public double x() {
		return latitude;
	}

	@Override
	public double y() {
		return longitude;
	}
}

@Entity(name = "City")
public static class City {

	@Id
	@GeneratedValue
	private Long id;

	private String name;

	@Embedded
	@Target( GPS.class )
	private Coordinates coordinates;

	//Getters and setters omitted for brevity

}

coordinates可嵌入类型被 Map 为Coordinates接口。但是,Hibernate 需要知道实际的实现方式,在这种情况下为GPS,因此@TargetComments 用于提供此信息。

假设我们保留了以下City实体:

例子 91. @Target坚持的例子

doInJPA( this::entityManagerFactory, entityManager -> {

	City cluj = new City();
	cluj.setName( "Cluj" );
	cluj.setCoordinates( new GPS( 46.77120, 23.62360 ) );

	entityManager.persist( cluj );
} );

提取City实体时,@Target表达式 Mapcoordinates属性:

例子 92. @Target取得例子

doInJPA( this::entityManagerFactory, entityManager -> {

	City cluj = entityManager.find( City.class, 1L );

	assertEquals( 46.77120, cluj.getCoordinates().x(), 0.00001 );
	assertEquals( 23.62360, cluj.getCoordinates().y(), 0.00001 );
} );
SELECT
    c.id as id1_0_0_,
    c.latitude as latitude2_0_0_,
    c.longitude as longitud3_0_0_,
    c.name as name4_0_0_
FROM
    City c
WHERE
    c.id = ?

-- binding parameter [1] as [BIGINT] - [1]

-- extracted value ([latitude2_0_0_] : [DOUBLE])  - [46.7712]
-- extracted value ([longitud3_0_0_] : [DOUBLE])  - [23.6236]
-- extracted value ([name4_0_0_]     : [VARCHAR]) - [Cluj]

因此,@Target注解用于定义父子关联之间的自定义联接关联。

2.4.9. @父 Map

特定于 Hibernate 的@Parent注解允许您从可嵌入对象内部引用所有者实体。

例子 93. @ParentMap 用法

@Embeddable
public static class GPS {

	private double latitude;

	private double longitude;

	@Parent
	private City city;

	//Getters and setters omitted for brevity

}

@Entity(name = "City")
public static class City {

	@Id
	@GeneratedValue
	private Long id;

	private String name;

	@Embedded
	@Target( GPS.class )
	private GPS coordinates;

	//Getters and setters omitted for brevity

}

假设我们保留了以下City实体:

例子 94. @Parent坚持的例子

doInJPA( this::entityManagerFactory, entityManager -> {

	City cluj = new City();
	cluj.setName( "Cluj" );
	cluj.setCoordinates( new GPS( 46.77120, 23.62360 ) );

	entityManager.persist( cluj );
} );

提取City实体时,可嵌入类型的city属性充当对拥有父实体的反向引用:

例子 95. @Parent取得例子

doInJPA( this::entityManagerFactory, entityManager -> {

	City cluj = entityManager.find( City.class, 1L );

	assertSame( cluj, cluj.getCoordinates().getCity() );
} );

因此,@Parent注解用于定义可嵌入类型与拥有实体之间的关联。

2.5. 实体类型

Usage of the word entity

实体类型描述了实际的持久域模型对象和数据库表行之间的 Map。为了避免与标记给定实体类型的 Comments 产生任何混淆,该 Comments 将进一步称为@Entity

在本章及其后的整个章节中,实体类型将简称为* entity *。

2.5.1. POJO 模型

  • JPA 2.1 规范 2.1 节中的实体类*定义了其对实体类的要求。希望在 JPA 提供程序之间可移植的应用程序应遵守以下要求:
  • 实体类必须使用javax.persistence.EntityComments 进行 Comments(或在 XMLMap 中这样表示)。

  • 实体类必须具有公共或受保护的无参数构造函数。它还可以定义其他构造函数。

  • 实体类必须是顶级类。

  • 枚举或接口不能指定为实体。

  • 实体类不能是最终的。实体类的任何方法或持久实例变量都不得为最终的。

  • 如果实体实例要作为分离对象远程使用,则实体类必须实现Serializable接口。

  • 抽象类和具体类都可以是实体。实体可以扩展非实体类以及实体类,并且非实体类可以扩展实体类。

  • 实体的持久状态由实例变量表示,实例变量可以对应于 JavaBean 样式的属性。实例变量只能由实体实例本身直接从实体的方法内部访问。Client 端只能通过实体的访问器方法(getter/setter 方法)或其他业务方法来使用实体的状态。

但是,Hibernate 的要求并不严格。与上面列表的区别包括:

  • 实体类必须具有无参数构造函数,该构造函数可以是公共的,受保护的或程序包可见性。它还可以定义其他构造函数。

  • 实体类不必是顶级类。

  • 从技术上讲,Hibernate 可以持久化最终类或具有最终持久性状态访问器(getter/setter)方法的类。但是,通常不是一个好主意,因为这样做将使 Hibernate 不能生成用于延迟加载实体的代理。

  • Hibernate 并不限制应用程序开发人员公开实例变量并从实体类本身之外引用它们。然而,这种范例的有效性至多是有争议的。

让我们详细了解每个需求。

2.5.2. 偏好非决赛类

Hibernate 的主要功能是可以通过运行时代理延迟加载某些实体实例变量(属性)。此功能取决于实体类是否为非最终类,或者取决于实现声明所有属性获取器/设置器的接口。您仍然可以使用 Hibernate 持久化未实现此类接口的最终类,但是您将无法使用代理来获取懒惰的关联,因此限制了性能调整的选项。出于同样的原因,您还应该避免将持久属性 getter 和 setter 声明为 final。

Note

从 5.0 开始,Hibernate 提供了更健壮的字节码增强版本,作为处理延迟加载的另一种方法。 Hibernate 在 5.0 之前具有一些字节码重写功能,但是它们非常初级。有关获取和字节码增强的更多信息,请参见Bytecode Enhancement

2.5.3. 实现无参数构造函数

实体类应具有无参数的构造函数。 Hibernate 和 JPA 都需要这样做。

JPA 要求将此构造函数定义为 public 或 protected。大多数情况下,只要系统 SecurityManager 允许覆盖可见性设置,Hibernate 都不在乎构造函数的可见性。就是说,如果您希望利用运行时代理生成,则应至少使用包可见性来定义构造函数。

2.5.4. 声明持久性属性的获取器和设置器

JPA 规范要求这样做,否则,该模型将阻止直接从实体本身外部访问实体持久状态字段。

尽管 Hibernate 不需要它,但建议遵循 JavaBean 约定并为实体持久属性定义 getter 和 setter。尽管如此,您仍然可以告诉 Hibernate 直接访问实体字段。

无需将属性(无论是字段还是 getter/setter)声明为公共属性。 Hibernate 可以处理以公共,受保护,程序包或私有可见性声明的属性。同样,如果要使用运行时代理生成进行延迟加载,则 getter/setter 方法应至少授予对程序包可见性的访问权限。

2.5.5. 提供标识符属性

Tip

从历史上看,提供标识符属性被认为是可选的。

但是,未在实体上定义标识符属性应被视为已弃用的功能,该功能将在以后的版本中删除。

标识符属性不一定需要 Map 到物理上定义主键的列。但是,它应该 Map 到可以唯一标识每一行的列。

Note

我们建议您在持久性类上声明以统一名称命名的标识符属性,并使用包装器(即非原始)类型(例如LongInteger)。

@IdComments 的位置标记为持久状态访问策略

例子 96.标识符 Map

@Id
private Long id;

Hibernate 提供了多种标识符生成策略,有关此主题的更多信息,请参见Identifier Generators章。

2.5.6. Map 实体

Map 实体的主要步骤是javax.persistence.EntityComments。

@EntityComments 仅定义name属性,该属性用于提供用于 JPQL 查询的特定实体名称。

默认情况下,如果缺少@Entity注解的 name 属性,则实体类本身的非限定名称将用作实体名称。

Tip

因为实体名称是由类的非限定名称给出的,所以即使实体类驻留在不同的包中,Hibernate 也不允许注册具有相同名称的多个实体。

如果不加此限制,则如果不合格的实体名称与一个以上的实体类相关联,则 Hibernate 将不知道在 JPQL 查询中引用了哪个实体类。

在以下示例中,实体名称(例如Book)由实体类名称的非限定名称给出。

例子 97. @Entity隐式名字的 Map

@Entity
public class Book {

	@Id
	private Long id;

	private String title;

	private String author;

	//Getters and setters are omitted for brevity
}

但是,也可以如以下示例所示显式设置实体名称。

例子 98. @Entity用一个明确的名字 Map

@Entity(name = "Book")
public static class Book {

	@Id
	private Long id;

	private String title;

	private String author;

	//Getters and setters are omitted for brevity
}

实体为数据库表建模。该标识符唯一地标识该表中的每一行。默认情况下,假定表的名称与实体的名称相同。要显式给出表的名称或指定有关表的其他信息,我们将使用javax.persistence.Table注解。

例子 99.简单@Entity@Table

@Entity(name = "Book")
@Table(
        catalog = "public",
        schema = "store",
        name = "book"
)
public static class Book {

    @Id
    private Long id;

    private String title;

    private String author;

    //Getters and setters are omitted for brevity
}
Map 关联表的目录

没有指定给定实体 Map 到的关联数据库表的目录,Hibernate 将使用与当前数据库连接关联的默认目录。

但是,如果您的数据库托管多个目录,则可以使用 JPA @Table注解的catalog属性指定给定表所在的目录。

假设我们正在使用 MySQL,并且想将Book实体 Map 到public目录中的book表,该表如下所示。

例子 100. public目录中的book

create table public.book (
  id bigint not null,
  author varchar(255),
  title varchar(255),
  primary key (id)
) engine=InnoDB

现在,要将Book实体 Map 到public目录中的book表,我们可以使用@Table JPA 注解的catalog属性。

例子 101.使用@Table注解指定数据库目录

@Entity(name = "Book")
@Table(
	catalog = "public",
	name = "book"
)
public static class Book {

	@Id
	private Long id;

	private String title;

	private String author;

	//Getters and setters are omitted for brevity
}
Map 关联表的架构

在不指定给定实体 Map 到关联数据库表的架构的情况下,Hibernate 将使用与当前数据库连接关联的默认架构。

但是,如果数据库支持架构,则可以使用 JPA @Table注解的schema属性指定给定表所在的架构。

假设我们使用的是 PostgreSQL,并且想将Book实体 Map 到library模式中的book表,该表如下所示。

例子 102. library模式中的book

create table library.book (
  id int8 not null,
  author varchar(255),
  title varchar(255),
  primary key (id)
)

现在,要将Book实体 Map 到library模式中的book表,我们可以使用@Table JPA 注解的schema属性。

例子 103.使用@Table注解指定数据库模式

@Entity(name = "Book")
@Table(
	schema = "library",
	name = "book"
)
public static class Book {

	@Id
	private Long id;

	private String title;

	private String author;

	//Getters and setters are omitted for brevity
}

Tip

@Table注解的schema属性仅在基础数据库支持架构(例如 PostgreSQL)时有效。

因此,如果使用的 MySQL 或 MariaDB 本身不支持架构(方案只是目录的别名),则需要使用catalog属性,而不是schema属性。

2.5.7. 实现 equals()和 hashCode()

Note

本节中的许多讨论都针对实体与 Hibernate Session 的关系,无论该实体是托管的,临时的还是分离的。如果您不熟悉这些主题,请在Persistence Context章中对它们进行说明。

对于 ORM,是否在您的域模型中实现equals()hashCode()方法,更不用说如何实现它们了,是一个非常棘手的讨论。

确实只有一种绝对情况:充当标识符的类必须基于 id 值实现 equals/hashCode。通常,这与用作复合标识符的用户定义类有关。除了这个非常具体的用例之外,我们将在下面讨论其他几个用例,您可能要考虑完全不实施 equals/hashCode。

那有什么大惊小怪的?通常,大多数 Java 对象根据对象的身份提供内置的equals()hashCode(),因此每个新对象都将与其他所有对象不同。这通常是普通 Java 编程中想要的。但是,从概念上讲,当您开始考虑某个类的多个实例代表同一数据的可能性时,这种情况就开始崩溃。

实际上,在处理来自数据库的数据时确实如此。每次我们从数据库中加载特定的Person时,我们自然都会获得一个唯一的实例。但是,Hibernate 会努力确保在给定的Session内不会发生这种情况。实际上,Hibernate 保证了特定会话范围内的持久身份(数据库行)和 Java 身份相等。因此,如果我们要求休眠Session多次加载该特定 Person,我们实际上将返回相同的* instance *:

例子 104.身份范围

Book book1 = entityManager.find( Book.class, 1L );
Book book2 = entityManager.find( Book.class, 1L );

assertTrue( book1 == book2 );

考虑我们有一个Library父实体,其中包含java.util.SetBook实体:

例子 105.库实体 Map

@Entity(name = "Library")
public static class Library {

	@Id
	private Long id;

	private String name;

	@OneToMany(cascade = CascadeType.ALL)
	@JoinColumn(name = "book_id")
	private Set<Book> books = new HashSet<>();

	//Getters and setters are omitted for brevity
}

例子 106.使用会话范围的身份设置用法

Library library = entityManager.find( Library.class, 1L );

Book book1 = entityManager.find( Book.class, 1L );
Book book2 = entityManager.find( Book.class, 1L );

library.getBooks().add( book1 );
library.getBooks().add( book2 );

assertEquals( 1, library.getBooks().size() );

但是,当我们混合从不同 Session 加载的实例时,语义会发生变化:

例子 107.混合会话

Book book1 = doInJPA( this::entityManagerFactory, entityManager -> {
	return entityManager.find( Book.class, 1L );
} );

Book book2 = doInJPA( this::entityManagerFactory, entityManager -> {
	return entityManager.find( Book.class, 1L );
} );

assertFalse( book1 == book2 );
doInJPA( this::entityManagerFactory, entityManager -> {
	Set<Book> books = new HashSet<>();

	books.add( book1 );
	books.add( book2 );

	assertEquals( 2, books.size() );
} );

具体来说,最后一个示例中的结果将取决于实现的Book类是否等于 equals/hashCode,如果取决于,则取决于方式。

如果Book类没有覆盖默认的 equals/hashCode,则这两个Book对象引用将不相等,因为它们的引用不同。

考虑另一种情况:

例子 108.具有瞬态实体的集合

Library library = entityManager.find( Library.class, 1L );

Book book1 = new Book();
book1.setId( 100L );
book1.setTitle( "High-Performance Java Persistence" );

Book book2 = new Book();
book2.setId( 101L );
book2.setTitle( "Java Persistence with Hibernate" );

library.getBooks().add( book1 );
library.getBooks().add( book2 );

assertEquals( 2, library.getBooks().size() );

如果您要处理会话外部的实体(无论它们是临时实体还是分离实体),尤其是在 Java 集合中使用它们的情况下,则应考虑实现 equals/hashCode。

一种常见的初始方法是使用实体的标识符属性作为 equals/hashCode 计算的基础:

例子 109.天真的 equals/hashCode 实现

@Entity(name = "Library")
public static class Library {

	@Id
	private Long id;

	private String name;

	@OneToMany(cascade = CascadeType.ALL)
	@JoinColumn(name = "book_id")
	private Set<Book> books = new HashSet<>();

	//Getters and setters are omitted for brevity
}

@Entity(name = "Book")
public static class Book {

	@Id
	@GeneratedValue
	private Long id;

	private String title;

	private String author;

	//Getters and setters are omitted for brevity

	@Override
	public boolean equals(Object o) {
		if ( this == o ) {
			return true;
		}
		if ( o == null || getClass() != o.getClass() ) {
			return false;
		}
		Book book = (Book) o;
		return Objects.equals( id, book.id );
	}

	@Override
	public int hashCode() {
		return Objects.hash( id );
	}
}

事实证明,如上一个示例所示,当将Book的瞬时实例添加到集合中时,这仍然无法解决:

例子 110.自动生成的带有 Sets 和朴素的 equals/hashCode 的标识符

Book book1 = new Book();
book1.setTitle( "High-Performance Java Persistence" );

Book book2 = new Book();
book2.setTitle( "Java Persistence with Hibernate" );

Library library = doInJPA( this::entityManagerFactory, entityManager -> {
	Library _library = entityManager.find( Library.class, 1L );

	_library.getBooks().add( book1 );
	_library.getBooks().add( book2 );

	return _library;
} );

assertFalse( library.getBooks().contains( book1 ) );
assertFalse( library.getBooks().contains( book2 ) );

这里的问题是生成的标识符的使用,Set的约定和 equals/hashCode 实现之间的冲突。 Set表示,当对象是Set的一部分时,对象的 equals/hashCode 值不应更改。但这正是此处发生的情况,因为 equals/hasCode 基于(生成的)id,在提交 JPA 事务之前,该 id 才设置。

注意,使用生成的标识符时,这只是一个问题。如果您正在使用分配的标识符,这将是没有问题的,假设标识符值是在添加到Set之前分配的。

另一种选择是强制在添加到Set之前生成并设置标识符:

例子 111.在添加到集合之前强制刷新

Book book1 = new Book();
book1.setTitle( "High-Performance Java Persistence" );

Book book2 = new Book();
book2.setTitle( "Java Persistence with Hibernate" );

Library library = doInJPA( this::entityManagerFactory, entityManager -> {
	Library _library = entityManager.find( Library.class, 1L );

	entityManager.persist( book1 );
	entityManager.persist( book2 );
	entityManager.flush();

	_library.getBooks().add( book1 );
	_library.getBooks().add( book2 );

	return _library;
} );

assertTrue( library.getBooks().contains( book1 ) );
assertTrue( library.getBooks().contains( book2 ) );

但这通常是不可行的。

最终的方法是使用“更好”的 equals/hashCode 实现,并使用自然 ID 或业务密钥。

例子 112.自然 Id 等于/ hashCode

@Entity(name = "Library")
public static class Library {

	@Id
	private Long id;

	private String name;

	@OneToMany(cascade = CascadeType.ALL)
	@JoinColumn(name = "book_id")
	private Set<Book> books = new HashSet<>();

	//Getters and setters are omitted for brevity
}

@Entity(name = "Book")
public static class Book {

	@Id
	@GeneratedValue
	private Long id;

	private String title;

	private String author;

	@NaturalId
	private String isbn;

	//Getters and setters are omitted for brevity

	@Override
	public boolean equals(Object o) {
		if ( this == o ) {
			return true;
		}
		if ( o == null || getClass() != o.getClass() ) {
			return false;
		}
		Book book = (Book) o;
		return Objects.equals( isbn, book.isbn );
	}

	@Override
	public int hashCode() {
		return Objects.hash( isbn );
	}
}

这次,将Book添加到Library Set时,即使已将Book保留下来,也可以检索它:

例子 113.自然 Id equals/hashCode 保持例子

Book book1 = new Book();
book1.setTitle( "High-Performance Java Persistence" );
book1.setIsbn( "978-9730228236" );

Library library = doInJPA( this::entityManagerFactory, entityManager -> {
	Library _library = entityManager.find( Library.class, 1L );

	_library.getBooks().add( book1 );

	return _library;
} );

assertTrue( library.getBooks().contains( book1 ) );

如您所见,equals/hashCode 问题并不简单,也没有一种“一刀切”的解决方案。

Tip

尽管最适合equalshashCode的用户使用自然 ID,但有时您只有提供唯一约束的实体标识符。

可以使用实体标识符进行相等性检查,但是需要一种变通方法:

  • 您需要为hashCode提供一个恒定值,以便在刷新实体前后哈希码值不会改变。

  • 您只需要比较非瞬态实体的实体标识符相等性。

有关 Map 标识符的详细信息,请参见Identifiers章。

2.5.8. 将实体 Map 到 SQL 查询

您可以使用@Subselect注解将实体 Map 到 SQL 查询。

例子 114. @Subselect实体 Map

@Entity(name = "Client")
@Table(name = "client")
public static class Client {

	@Id
	private Long id;

	@Column(name = "first_name")
	private String firstName;

	@Column(name = "last_name")
	private String lastName;

	//Getters and setters omitted for brevity

}

@Entity(name = "Account")
@Table(name = "account")
public static class Account {

	@Id
	private Long id;

	@ManyToOne
	private Client client;

	private String description;

	//Getters and setters omitted for brevity

}

@Entity(name = "AccountTransaction")
@Table(name = "account_transaction")
public static class AccountTransaction {

	@Id
	@GeneratedValue
	private Long id;

	@ManyToOne
	private Account account;

	private Integer cents;

	private String description;

	//Getters and setters omitted for brevity

}

@Entity(name = "AccountSummary")
@Subselect(
	"select " +
	"	a.id as id, " +
	"	concat(concat(c.first_name, ' '), c.last_name) as clientName, " +
	"	sum(atr.cents) as balance " +
	"from account a " +
	"join client c on c.id = a.client_id " +
	"join account_transaction atr on a.id = atr.account_id " +
	"group by a.id, concat(concat(c.first_name, ' '), c.last_name)"
)
@Synchronize( {"client", "account", "account_transaction"} )
public static class AccountSummary {

	@Id
	private Long id;

	private String clientName;

	private int balance;

	//Getters and setters omitted for brevity

}

在上面的示例中,Account实体不保留任何余额,因为每个帐户操作都被注册为AccountTransaction。要找到Account余额,我们需要查询与Account实体共享相同标识符的AccountSummary

但是,AccountSummary并不 Map 到物理表,而是 Map 到 SQL 查询。

因此,如果我们有以下AccountTransaction记录,则AccountSummary余额将与此Account中的适当金额匹配。

例子 115.找到一个@Subselect实体

doInJPA( this::entityManagerFactory, entityManager -> {
	Client client = new Client();
	client.setId( 1L );
	client.setFirstName( "John" );
	client.setLastName( "Doe" );
	entityManager.persist( client );

	Account account = new Account();
	account.setId( 1L );
	account.setClient( client );
	account.setDescription( "Checking account" );
	entityManager.persist( account );

	AccountTransaction transaction = new AccountTransaction();
	transaction.setAccount( account );
	transaction.setDescription( "Salary" );
	transaction.setCents( 100 * 7000 );
	entityManager.persist( transaction );

	AccountSummary summary = entityManager.createQuery(
		"select s " +
		"from AccountSummary s " +
		"where s.id = :id", AccountSummary.class)
	.setParameter( "id", account.getId() )
	.getSingleResult();

	assertEquals( "John Doe", summary.getClientName() );
	assertEquals( 100 * 7000, summary.getBalance() );
} );

如果我们添加新的AccountTransaction实体并刷新AccountSummary实体,则余额将相应更新:

例子 116.刷新一个@Subselect实体

doInJPA( this::entityManagerFactory, entityManager -> {
	AccountSummary summary = entityManager.find( AccountSummary.class, 1L );
	assertEquals( "John Doe", summary.getClientName() );
	assertEquals( 100 * 7000, summary.getBalance() );

	AccountTransaction transaction = new AccountTransaction();
	transaction.setAccount( entityManager.getReference( Account.class, 1L ) );
	transaction.setDescription( "Shopping" );
	transaction.setCents( -100 * 2200 );
	entityManager.persist( transaction );
	entityManager.flush();

	entityManager.refresh( summary );
	assertEquals( 100 * 4800, summary.getBalance() );
} );

Tip

AccountSummary实体 Map 中的@SynchronizeComments 的目的是指示 Hibernate 基础@Subselect SQL 查询需要哪些数据库表。这是因为与 JPQL 和 HQL 查询不同,Hibernate 无法解析基础本机 SQL 查询。

有了@Synchronize注解,当执行从AccountSummary实体中选择的 HQL 或 JPQL 时,如果有未决的AccountClientAccountTransaction实体状态转换,则 Hibernate 将触发持久性上下文刷新。

2.5.9. 定义自定义实体代理

默认情况下,当需要使用代理而不是实际的 POJO 时,Hibernate 将使用JavassistByte Buddy之类的字节码操作库。

但是,如果实体类是最终的,则 Javassist 将不会创建代理,即使您仅需要代理引用,也将获得 POJO。在这种情况下,您可以代理此特定实体实现的接口,如以下示例所示。

例子 117.实现Identifiable接口的最终实体类

public interface Identifiable {

	Long getId();

	void setId(Long id);
}

@Entity( name = "Book" )
@Proxy(proxyClass = Identifiable.class)
public static final class Book implements Identifiable {

	@Id
	private Long id;

	private String title;

	private String author;

	@Override
	public Long getId() {
		return id;
	}

	@Override
	public void setId(Long id) {
		this.id = id;
	}

	//Other getters and setters omitted for brevity
}

@ProxyComments 用于为当前带 Comments 的实体指定自定义代理实现。

加载Book实体代理时,Hibernate 将代为Identifiable接口代理,如以下示例所示:

例子 118.代理实现Identifiable接口的最终实体类

doInHibernate( this::sessionFactory, session -> {
	Book book = new Book();
	book.setId( 1L );
	book.setTitle( "High-Performance Java Persistence" );
	book.setAuthor( "Vlad Mihalcea" );

	session.persist( book );
} );

doInHibernate( this::sessionFactory, session -> {
	Identifiable book = session.getReference( Book.class, 1L );

	assertTrue(
		"Loaded entity is not an instance of the proxy interface",
		book instanceof Identifiable
	);
	assertFalse(
		"Proxy class was not created",
		book instanceof Book
	);
} );
insert
into
    Book
    (author, title, id)
values
    (?, ?, ?)

-- binding parameter [1] as [VARCHAR] - [Vlad Mihalcea]
-- binding parameter [2] as [VARCHAR] - [High-Performance Java Persistence]
-- binding parameter [3] as [BIGINT]  - [1]

正如您在关联的 SQL 代码段中所看到的那样,Hibernate 不会发出任何 SQL SELECT 查询,因为可以构造代理而不需要获取实际的实体 POJO。

2.5.10. 使用@Tuplizer 注解的动态实体代理

可以使用@TuplizerComments 将您的实体 Map 为动态代理。

在以下实体 Map 中,可嵌入对象和实体都被 Map 为接口,而不是 POJO。

例子 119.动态实体代理 Map

@Entity
@Tuplizer(impl = DynamicEntityTuplizer.class)
public interface Cuisine {

    @Id
    @GeneratedValue
    Long getId();
    void setId(Long id);

    String getName();
    void setName(String name);

    @Tuplizer(impl = DynamicEmbeddableTuplizer.class)
    Country getCountry();
    void setCountry(Country country);
}
@Embeddable
public interface Country {

    @Column(name = "CountryName")
    String getName();

    void setName(String name);
}

@Tuplizer指示 Hibernate 使用DynamicEntityTuplizerDynamicEmbeddableTuplizer处理关联的实体和可嵌入对象类型。

Cuisine实体和Country可嵌入类型都将被实例化为 Java 动态代理,如下面的DynamicInstantiator示例所示:

例子 120.实例化实体和可嵌入对象作为动态代理

public class DynamicEntityTuplizer extends PojoEntityTuplizer {

    public DynamicEntityTuplizer(
            EntityMetamodel entityMetamodel,
            PersistentClass mappedEntity) {
        super( entityMetamodel, mappedEntity );
    }

    @Override
    protected Instantiator buildInstantiator(
            EntityMetamodel entityMetamodel,
            PersistentClass persistentClass) {
        return new DynamicInstantiator(
            persistentClass.getClassName()
        );
    }

    @Override
    protected ProxyFactory buildProxyFactory(
            PersistentClass persistentClass,
            Getter idGetter,
            Setter idSetter) {
        return super.buildProxyFactory(
            persistentClass, idGetter,
            idSetter
        );
    }
}
public class DynamicEmbeddableTuplizer
        extends PojoComponentTuplizer {

    public DynamicEmbeddableTuplizer(Component embeddable) {
        super( embeddable );
    }

    protected Instantiator buildInstantiator(Component embeddable) {
        return new DynamicInstantiator(
            embeddable.getComponentClassName()
        );
    }
}
public class DynamicInstantiator
        implements Instantiator {

    private final Class targetClass;

    public DynamicInstantiator(String targetClassName) {
        try {
            this.targetClass = Class.forName( targetClassName );
        }
        catch (ClassNotFoundException e) {
            throw new HibernateException( e );
        }
    }

    public Object instantiate(Serializable id) {
        return ProxyHelper.newProxy( targetClass, id );
    }

    public Object instantiate() {
        return instantiate( null );
    }

    public boolean isInstance(Object object) {
        try {
            return targetClass.isInstance( object );
        }
        catch( Throwable t ) {
            throw new HibernateException(
                "could not get handle to entity as interface : " + t
            );
        }
    }
}
public class ProxyHelper {

    public static <T> T newProxy(Class<T> targetClass, Serializable id) {
        return ( T ) Proxy.newProxyInstance(
            targetClass.getClassLoader(),
            new Class[] {
                targetClass
            },
            new DataProxyHandler(
                targetClass.getName(),
                id
            )
        );
    }

    public static String extractEntityName(Object object) {
        if ( Proxy.isProxyClass( object.getClass() ) ) {
            InvocationHandler handler = Proxy.getInvocationHandler(
                object
            );
            if ( DataProxyHandler.class.isAssignableFrom( handler.getClass() ) ) {
                DataProxyHandler myHandler = (DataProxyHandler) handler;
                return myHandler.getEntityName();
            }
        }
        return null;
    }
}
public final class DataProxyHandler implements InvocationHandler {

    private String entityName;

    private Map<String, Object> data = new HashMap<>();

    public DataProxyHandler(String entityName, Serializable id) {
        this.entityName = entityName;
        data.put( "Id", id );
    }

    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        String methodName = method.getName();
        if ( methodName.startsWith( "set" ) ) {
            String propertyName = methodName.substring( 3 );
            data.put( propertyName, args[0] );
        }
        else if ( methodName.startsWith( "get" ) ) {
            String propertyName = methodName.substring( 3 );
            return data.get( propertyName );
        }
        else if ( "toString".equals( methodName ) ) {
            return entityName + "#" + data.get( "Id" );
        }
        else if ( "hashCode".equals( methodName ) ) {
            return this.hashCode();
        }
        return null;
    }

    public String getEntityName() {
        return entityName;
    }
}

使用DynamicInstantiator后,我们可以像使用 POJO 实体一样使用动态代理实体。

例子 121.持久化实体和可嵌入对象作为动态代理

Cuisine _cuisine = doInHibernateSessionBuilder(
		() -> sessionFactory()
				.withOptions()
				.interceptor( new EntityNameInterceptor() ),
		session -> {
	Cuisine cuisine = ProxyHelper.newProxy( Cuisine.class, null );
	cuisine.setName( "Française" );

	Country country = ProxyHelper.newProxy( Country.class, null );
	country.setName( "France" );

	cuisine.setCountry( country );
	session.persist( cuisine );

	return cuisine;
} );

doInHibernateSessionBuilder(
		() -> sessionFactory()
				.withOptions()
				.interceptor( new EntityNameInterceptor() ),
		session -> {
	Cuisine cuisine = session.get( Cuisine.class, _cuisine.getId() );

	assertEquals( "Française", cuisine.getName() );
	assertEquals( "France", cuisine.getCountry().getName() );
} );

2.5.11. 定义自定义实体持久性

@Persister注解用于指定自定义实体或集合持久性。

对于实体,自定义持久程序必须实现EntityPersister接口。

对于集合,自定义持久程序必须实现CollectionPersister接口。

例子 122.实体持久性 Map

@Entity
@Persister( impl = EntityPersister.class )
public class Author {

    @Id
    public Integer id;

    @OneToMany( mappedBy = "author" )
    @Persister( impl = CollectionPersister.class )
    public Set<Book> books = new HashSet<>();

    //Getters and setters omitted for brevity

    public void addBook(Book book) {
        this.books.add( book );
        book.setAuthor( this );
    }
}
@Entity
@Persister( impl = EntityPersister.class )
public class Book {

    @Id
    public Integer id;

    private String title;

    @ManyToOne(fetch = FetchType.LAZY)
    public Author author;

    //Getters and setters omitted for brevity
}

通过提供自己的EntityPersisterCollectionPersister实现,您可以控制实体和集合如何持久存储到数据库中。

2.5.12. 访问策略

作为 JPA 提供程序,Hibernate 可以同时检查实体属性(实例字段)或访问器(实例属性)。默认情况下,@IdComments 的位置提供默认的访问策略。当放在一个字段上时,Hibernate 将假定基于字段的访问。当放置在标识符 getter 上时,Hibernate 将使用基于属性的访问。

Tip

为避免出现诸如HCANN-63-以至少两个大写字符开头的属性名称在 HQL 中具有奇数功能之类的问题,在命名属性方面应注意Java Bean 规范

可嵌入类型从其父实体继承访问策略。

Field-based access

例子 123.基于字段的访问

@Entity(name = "Book")
public static class Book {

	@Id
	private Long id;

	private String title;

	private String author;

	//Getters and setters are omitted for brevity
}

当使用基于字段的访问时,添加其他实体级别的方法更加灵活,因为 Hibernate 不会考虑持久性状态的那些部分。要将字段排除在实体持久状态的一部分之外,必须使用@Transient注解标记该字段。

Note

使用基于字段的访问的另一个优点是,某些实体属性可以从实体外部隐藏。

这种属性的一个例子是实体@Version字段,通常不需要数据访问层对其进行操作。

使用基于字段的访问,我们可以简单地忽略此版本字段的 getter 和 setter,并且 Hibernate 仍然可以利用开放式并发控制机制。

Property-based access

例子 124.基于属性的访问

@Entity(name = "Book")
public static class Book {

	private Long id;

	private String title;

	private String author;

	@Id
	public Long getId() {
		return id;
	}

	public void setId(Long id) {
		this.id = id;
	}

	public String getTitle() {
		return title;
	}

	public void setTitle(String title) {
		this.title = title;
	}

	public String getAuthor() {
		return author;
	}

	public void setAuthor(String author) {
		this.author = author;
	}
}

当使用基于属性的访问时,Hibernate 使用访问器来读取和写入实体状态。将会添加到该实体的所有其他方法(例如,用于同步双向一对多关联的两端的辅助方法)都必须使用@TransientComments 进行标记。

覆盖默认访问策略

可以使用 JPA @AccessComments 覆盖默认的访问策略机制。在下面的示例中,@Version属性是通过其字段而不是其 getter 来访问的,就像其余实体属性一样。

例子 125.覆盖访问策略

@Entity(name = "Book")
public static class Book {

	private Long id;

	private String title;

	private String author;

	@Access( AccessType.FIELD )
	@Version
	private int version;

	@Id
	public Long getId() {
		return id;
	}

	public void setId(Long id) {
		this.id = id;
	}

	public String getTitle() {
		return title;
	}

	public void setTitle(String title) {
		this.title = title;
	}

	public String getAuthor() {
		return author;
	}

	public void setAuthor(String author) {
		this.author = author;
	}
}
可嵌入的类型和访问策略

由于可嵌入对象由它们自己的实体 Management,因此访问策略也从该实体继承。这既适用于简单的可嵌入类型,也适用于可嵌入对象的集合。

可嵌入类型可以推翻默认的隐式访问策略(从拥有的实体继承)。在以下示例中,无论拥有实体选择哪种访问策略,可嵌入对象都使用基于属性的访问:

例子 126.可嵌入的独占访问策略

@Embeddable
@Access( AccessType.PROPERTY )
public static class Author {

	private String firstName;

	private String lastName;

	public Author() {
	}

	public Author(String firstName, String lastName) {
		this.firstName = firstName;
		this.lastName = lastName;
	}

	public String getFirstName() {
		return firstName;
	}

	public void setFirstName(String firstName) {
		this.firstName = firstName;
	}

	public String getLastName() {
		return lastName;
	}

	public void setLastName(String lastName) {
		this.lastName = lastName;
	}
}

拥有实体可以使用基于字段的访问,而可嵌入实体可以使用基于属性的访问,因为它已明确选择:

例子 127.包括一个单一可嵌入类型的实体

@Entity(name = "Book")
public static class Book {

	@Id
	private Long id;

	private String title;

	@Embedded
	private Author author;

	//Getters and setters are omitted for brevity
}

这也适用于可嵌入类型的收集:

例子 128.实体包括可嵌入类型的集合

@Entity(name = "Book")
public static class Book {

	@Id
	private Long id;

	private String title;

	@ElementCollection
	@CollectionTable(
		name = "book_author",
		joinColumns = @JoinColumn(name = "book_id")
	)
	private List<Author> authors = new ArrayList<>();

	//Getters and setters are omitted for brevity
}

2.6. Identifiers

标识符为实体的主键建模。它们用于唯一地标识每个特定实体。

Hibernate 和 JPA 都对相应的数据库列进行以下假设:

  • UNIQUE

    • 这些值必须唯一地标识每一行。
  • NOT NULL

    • 该值不能为空。对于复合 ID,任何部分都不能为 null。
  • IMMUTABLE

    • 这些值一旦插入,就无法更改。这是更一般的指南,而不是因意见而异的硬性规定。 JPA 定义了将标识符属性的值更改为未定义的行为;休眠根本不支持。如果您选择的 PK 的值将被更新,Hibernate 建议将可变值 Map 为自然 ID,并为 PK 使用替代 ID。参见Natural Ids

Note

从技术上讲,标识符不必 Map 到物理上定义为表主键的列。他们只需要 Map 到唯一标识每一行的列即可。但是,本文档将 continue 互换使用术语标识符和主键。

每个实体都必须定义一个标识符。对于实体继承层次结构,必须仅在作为层次结构根的实体上定义标识符。

标识符可以是简单的(单个值)或复合的(多个值)。

2.6.1. 简单标识符

简单标识符 Map 到单个基本属性,并使用javax.persistence.IdComments 表示。

根据 JPA,只能将以下类型用作标识符属性类型:

  • 任何 Java 基本类型

  • 任何原始包装器类型

  • java.lang.String

  • java.util.Date(TemporalType#DATE)

  • java.sql.Date

  • java.math.BigDecimal

  • java.math.BigInteger

此列表之外的用于标识符属性的任何类型都将不可移植。

Assigned identifiers

如上面的示例所示,可以分配简单标识符的值。对分配的标识符值的期望是应用程序在调用保存/持久化之前分配(在实体属性上设置它们)。

例子 129.简单的分配实体标识符

@Entity(name = "Book")
public static class Book {

	@Id
	private Long id;

	private String title;

	private String author;

	//Getters and setters are omitted for brevity
}
Generated identifiers

可以生成简单标识符的值。为了表示已生成标识符属性,将使用javax.persistence.GeneratedValue对其进行 Comments。

例子 130.简单生成的标识符

@Entity(name = "Book")
public static class Book {

	@Id
	@GeneratedValue
	private Long id;

	private String title;

	private String author;

	//Getters and setters are omitted for brevity
}

此外,对于上面的类型限制列表,JPA 表示,如果使用生成的标识符值(请参见下文),则仅可移植地支持整数类型(short,int,long)。

对于生成的标识符值的期望是,当保存/持久化发生时,Hibernate 将生成该值。

标识符值生成策略将在生成的标识符值部分中详细讨论。

2.6.2. 复合标识符

复合标识符对应于一个或多个持久属性。这是 JPA 规范定义的 Management 组合标识符的规则:

  • 复合标识符必须由“主键类”表示。主键类可以使用javax.persistence.EmbeddedId注解定义(请参见具有@EmbeddedId 的复合标识符),也可以使用javax.persistence.IdClass注解定义(请参见具有@IdClass 的复合标识符)。

  • 主键类必须是公共的,并且必须具有公共的无参数构造函数。

  • 主键类必须可序列化。

  • 主键类必须定义 equals 和 hashCode 方法,该方法与主键 Map 到的基础数据库类型的相等性一致。

Note

组合标识符必须由“主键类”(例如@EmbeddedId@IdClass)表示的限制仅针对 JPA。

Hibernate 确实允许通过多个@Id属性定义组合标识符,而无需使用“主键类”。

组成合成的属性可以是 basic,复合@ManyToOne。特别要注意的是,收集和一对一是绝对不合适的。

2.6.3. 具有@EmbeddedId 的复合标识符

使用 EmbeddedId 对复合标识符进行建模仅意味着将可嵌入对象定义为组成该标识符的一个或多个属性的组合,然后在实体上公开该可嵌入类型的属性。

例子 131.基本的@EmbeddedId

@Entity(name = "SystemUser")
public static class SystemUser {

	@EmbeddedId
	private PK pk;

	private String name;

	//Getters and setters are omitted for brevity
}

@Embeddable
public static class PK implements Serializable {

	private String subsystem;

	private String username;

	public PK(String subsystem, String username) {
		this.subsystem = subsystem;
		this.username = username;
	}

	private PK() {
	}

	@Override
	public boolean equals(Object o) {
		if ( this == o ) {
			return true;
		}
		if ( o == null || getClass() != o.getClass() ) {
			return false;
		}
		PK pk = (PK) o;
		return Objects.equals( subsystem, pk.subsystem ) &&
				Objects.equals( username, pk.username );
	}

	@Override
	public int hashCode() {
		return Objects.hash( subsystem, username );
	}
}

如前所述,EmbeddedIds 甚至可以包含@ManyToOne属性:

例子 132. @EmbeddedId@ManyToOne

@Entity(name = "SystemUser")
public static class SystemUser {

	@EmbeddedId
	private PK pk;

	private String name;

	//Getters and setters are omitted for brevity
}

@Entity(name = "Subsystem")
public static class Subsystem {

	@Id
	private String id;

	private String description;

	//Getters and setters are omitted for brevity
}

@Embeddable
public static class PK implements Serializable {

	@ManyToOne(fetch = FetchType.LAZY)
	private Subsystem subsystem;

	private String username;

	public PK(Subsystem subsystem, String username) {
		this.subsystem = subsystem;
		this.username = username;
	}

	private PK() {
	}

	@Override
	public boolean equals(Object o) {
		if ( this == o ) {
			return true;
		}
		if ( o == null || getClass() != o.getClass() ) {
			return false;
		}
		PK pk = (PK) o;
		return Objects.equals( subsystem, pk.subsystem ) &&
				Objects.equals( username, pk.username );
	}

	@Override
	public int hashCode() {
		return Objects.hash( subsystem, username );
	}
}

Note

Hibernate 支持直接在@EmbeddedId@IdClass主键类中对@ManyToOne关联进行建模。

但是,JPA 规范并不可移植地支持该功能。用 JPA 术语,将使用“派生的标识符”。有关更多详细信息,请参见Derived Identifiers

2.6.4. 具有@IdClass 的复合标识符

使用 IdClass 对复合标识符进行建模与使用 EmbeddedId 进行建模的区别在于,实体定义了组成合成的每个单独属性。 IdClass 只是充当“影子”。

例子 133.基本的@IdClass

@Entity(name = "SystemUser")
@IdClass( PK.class )
public static class SystemUser {

	@Id
	private String subsystem;

	@Id
	private String username;

	private String name;

	public PK getId() {
		return new PK(
			subsystem,
			username
		);
	}

	public void setId(PK id) {
		this.subsystem = id.getSubsystem();
		this.username = id.getUsername();
	}

	//Getters and setters are omitted for brevity
}

public static class PK implements Serializable {

	private String subsystem;

	private String username;

	public PK(String subsystem, String username) {
		this.subsystem = subsystem;
		this.username = username;
	}

	private PK() {
	}

	//Getters and setters are omitted for brevity

	@Override
	public boolean equals(Object o) {
		if ( this == o ) {
			return true;
		}
		if ( o == null || getClass() != o.getClass() ) {
			return false;
		}
		PK pk = (PK) o;
		return Objects.equals( subsystem, pk.subsystem ) &&
				Objects.equals( username, pk.username );
	}

	@Override
	public int hashCode() {
		return Objects.hash( subsystem, username );
	}
}

非聚合的复合标识符也可以包含 ManyToOne 属性,正如我们在聚合的标识符中看到的那样(仍然是非便携式的)。

例子 134.具有@ManyToOne的 IdClass

@Entity(name = "SystemUser")
@IdClass( PK.class )
public static class SystemUser {

	@Id
	@ManyToOne(fetch = FetchType.LAZY)
	private Subsystem subsystem;

	@Id
	private String username;

	private String name;

	//Getters and setters are omitted for brevity
}

@Entity(name = "Subsystem")
public static class Subsystem {

	@Id
	private String id;

	private String description;

	//Getters and setters are omitted for brevity
}

public static class PK implements Serializable {

	private Subsystem subsystem;

	private String username;

	public PK(Subsystem subsystem, String username) {
		this.subsystem = subsystem;
		this.username = username;
	}

	private PK() {
	}

	//Getters and setters are omitted for brevity
}

使用非聚合的复合标识符,Hibernate 还支持复合值的“部分”生成。

例子 135. @IdClass使用@GeneratedValue生成部分标识符

@Entity(name = "SystemUser")
@IdClass( PK.class )
public static class SystemUser {

	@Id
	private String subsystem;

	@Id
	private String username;

	@Id
	@GeneratedValue
	private Integer registrationId;

	private String name;

	public PK getId() {
		return new PK(
			subsystem,
			username,
			registrationId
		);
	}

	public void setId(PK id) {
		this.subsystem = id.getSubsystem();
		this.username = id.getUsername();
		this.registrationId = id.getRegistrationId();
	}

	//Getters and setters are omitted for brevity
}

public static class PK implements Serializable {

	private String subsystem;

	private String username;

	private Integer registrationId;

	public PK(String subsystem, String username) {
		this.subsystem = subsystem;
		this.username = username;
	}

	public PK(String subsystem, String username, Integer registrationId) {
		this.subsystem = subsystem;
		this.username = username;
		this.registrationId = registrationId;
	}

	private PK() {
	}

	//Getters and setters are omitted for brevity

	@Override
	public boolean equals(Object o) {
		if ( this == o ) {
			return true;
		}
		if ( o == null || getClass() != o.getClass() ) {
			return false;
		}
		PK pk = (PK) o;
		return Objects.equals( subsystem, pk.subsystem ) &&
				Objects.equals( username, pk.username ) &&
				Objects.equals( registrationId, pk.registrationId );
	}

	@Override
	public int hashCode() {
		return Objects.hash( subsystem, username, registrationId );
	}
}

Note

由于 SpecJ 委员会对 JPA 规范进行了高度质疑的解释,因此存在允许在组合标识符中自动生成值的功能。

Hibernate 并不认为 JPA 定义了对此的支持,而是添加了该功能只是为了在 SpecJ 基准测试中可用。从 JPA 角度来看,此功能的使用可能是可移植的,也可能不是。

2.6.5. 具有关联的复合标识符

Hibernate 允许从实体关联中定义复合标识符。在以下示例中,PersonAddress实体标识符由两个@ManyToOne关联组成。

例子 136.具有关联的复合标识符

@Entity(name = "Book")
public static class Book implements Serializable {

	@Id
	@ManyToOne(fetch = FetchType.LAZY)
	private Author author;

	@Id
	@ManyToOne(fetch = FetchType.LAZY)
	private Publisher publisher;

	@Id
	private String title;

	public Book(Author author, Publisher publisher, String title) {
		this.author = author;
		this.publisher = publisher;
		this.title = title;
	}

	private Book() {
	}

	//Getters and setters are omitted for brevity

	@Override
	public boolean equals(Object o) {
		if ( this == o ) {
			return true;
		}
		if ( o == null || getClass() != o.getClass() ) {
			return false;
		}
		Book book = (Book) o;
		return Objects.equals( author, book.author ) &&
				Objects.equals( publisher, book.publisher ) &&
				Objects.equals( title, book.title );
	}

	@Override
	public int hashCode() {
		return Objects.hash( author, publisher, title );
	}
}

@Entity(name = "Author")
public static class Author implements Serializable {

	@Id
	private String name;

	//Getters and setters are omitted for brevity

	@Override
	public boolean equals(Object o) {
		if ( this == o ) {
			return true;
		}
		if ( o == null || getClass() != o.getClass() ) {
			return false;
		}
		Author author = (Author) o;
		return Objects.equals( name, author.name );
	}

	@Override
	public int hashCode() {
		return Objects.hash( name );
	}
}

@Entity(name = "Publisher")
public static class Publisher implements Serializable {

	@Id
	private String name;

	//Getters and setters are omitted for brevity

	@Override
	public boolean equals(Object o) {
		if ( this == o ) {
			return true;
		}
		if ( o == null || getClass() != o.getClass() ) {
			return false;
		}
		Publisher publisher = (Publisher) o;
		return Objects.equals( name, publisher.name );
	}

	@Override
	public int hashCode() {
		return Objects.hash( name );
	}
}

尽管 Map 比使用@EmbeddedId@IdClass简单得多,但实体实例和实际标识符之间没有分隔。要查询该实体,必须将实体本身的实例提供给持久性上下文。

例子 137.用复合标识符获取

Book book = entityManager.find( Book.class, new Book(
	author,
	publisher,
	"High-Performance Java Persistence"
) );

assertEquals( "Vlad Mihalcea", book.getAuthor().getName() );

2.6.6. 具有生成属性的复合标识符

使用组合标识符时,底层标识符属性必须由用户手动分配。

不支持将自动生成的属性用于生成构成复合标识符的基础属性的值。

因此,您不能使用生成的属性部分描述的任何自动属性生成器,例如@Generated@CreationTimestamp@ValueGenerationType或数据库生成的值。

但是,仍然可以在构造复合标识符之前生成标识符属性,如以下示例所示。

假设我们有以下EventId复合标识符和使用上述复合标识符的Event实体。

例子 138.事件实体和 EventId 复合标识符

@Entity
class Event {

    @Id
    private EventId id;

    @Column(name = "event_key")
    private String key;

    @Column(name = "event_value")
    private String value;

    //Getters and setters are omitted for brevity
}
@Embeddable
class EventId implements Serializable {

	private Integer category;

	private Timestamp createdOn;

	//Getters and setters are omitted for brevity
	@Override
	public boolean equals(Object o) {
		if ( this == o ) {
			return true;
		}
		if ( o == null || getClass() != o.getClass() ) {
			return false;
		}
		EventId that = (EventId) o;
		return Objects.equals( category, that.category ) &&
				Objects.equals( createdOn, that.createdOn );
	}

	@Override
	public int hashCode() {
		return Objects.hash( category, createdOn );
	}
}
内存中生成的组合标识符属性

如果要在内存中生成复合标识符属性,则需要执行以下操作:

例子 139.内存中生成的复合标识符属性的例子

EventId id = new EventId();
id.setCategory( 1 );
id.setCreatedOn( new Timestamp( System.currentTimeMillis() ) );

Event event = new Event();
event.setId( id );
event.setKey( "Temperature" );
event.setValue( "9" );

entityManager.persist( event );

请注意,EventId复合标识符的createdOn属性是由数据访问代码生成的,并在持久保存Event实体之前分配给该标识符。

数据库生成的复合标识符属性

如果要使用数据库函数或存储过程生成复合标识符属性,则可以按照以下示例所示进行操作。

例子 140.数据库生成的复合标识符属性例子

Timestamp currentTimestamp = (Timestamp) entityManager
.createNativeQuery(
	"SELECT CURRENT_TIMESTAMP" )
.getSingleResult();

EventId id = new EventId();
id.setCategory( 1 );
id.setCreatedOn( currentTimestamp );

Event event = new Event();
event.setId( id );
event.setKey( "Temperature" );
event.setValue( "9" );

entityManager.persist( event );

请注意,EventId复合标识符的createdOn属性是通过调用CURRENT_TIMESTAMP数据库函数生成的,在持久化Event实体之前,我们将其分配给了复合标识符。

2.6.7. 生成的标识符值

Note

您还可以自动生成非标识符属性的值。有关更多详细信息,请参见Generated properties部分。

Hibernate 支持跨多种不同类型的标识符值生成。请记住,JPA 仅针对整数类型可移植地定义标识符值的生成。

标识符值的生成使用javax.persistence.GeneratedValueComments 指示。这里最重要的信息是指定的javax.persistence.GenerationType,它指示如何生成值。

Note

下面的讨论假定应用程序在引导过程中使用hibernate.id.new_generator_mappings设置或MetadataBuilder.enableNewIdentifierGeneratorSupport方法指示的 Hibernate 的“新生成器 Map”。

从 Hibernate 5 开始,默认情况下将其设置为true。在将hibernate.id.new_generator_mappings配置设置为false的应用中,此处讨论的分辨率将有很大不同。此处的其余讨论假定启用了此设置(true)。

Tip

在 Hibernate 5.3 中,如果刷新模式不等于AUTO,则 Hibernate 尝试延迟实体的插入。对于使用IDENTITYSEQUENCE生成的标识符的实体来说,这有点问题,这些标识符也以某种形式与同一笔 Transaction 中的另一个实体相关联。

在 Hibernate 5.4 中,Hibernate 尝试使用算法来决定插入是否应该延迟或是否需要立即插入的算法来解决该问题。我们只想在合理的非常特殊的用例中恢复 5.3 之前的行为。

实体 Map 有时可能很复杂,并且可能忽略了一个极端情况。如果DelayedPostInsertIdentifier出现问题,Hibernate 提供了一种完全禁用 5.3 行为的方法。要启用旧版行为,请设置hibernate.id.disable_delayed_identity_inserts=true

此配置选项旨在充当临时修复程序,并弥合 Hibernate 5.x 发行版中此行为的更改之间的差距。如果 Map 需要此配置设置,请打开 JIRA 并报告 Map,以便可以检查算法。

2.6.8. 解释自动

持久性提供程序如何解释 AUTO 生成类型取决于该提供程序。

默认行为是查看标识符属性的 Java 类型。

如果标识符类型为 UUID,则 Hibernate 将使用UUID identifier

如果标识符类型为数字(例如LongInteger),则 Hibernate 将使用IdGeneratorStrategyInterpreter来解析标识符生成器策略。 IdGeneratorStrategyInterpreter有两个实现:

  • FallbackInterpreter

    • 这是 Hibernate 5.0 以来的默认策略。对于较旧的版本,可通过hibernate.id.new_generator_mappings配置属性启用此策略。使用此策略时,AUTO始终解析为SequenceStyleGenerator。如果基础数据库支持序列,则使用 SEQUENCE 生成器。否则,将改为使用 TABLE 生成器。
  • LegacyFallbackInterpreter

2.6.9. 使用序列

为了实现基于数据库序列的标识符值生成,Hibernate 利用了它的org.hibernate.id.enhanced.SequenceStyleGenerator id 生成器。重要的是要注意,SequenceStyleGenerator 能够通过切换到表作为基础支持来处理不支持序列的数据库。这使 Hibernate 在数据库之间具有很大程度的可移植性,同时仍保持一致的 id 生成行为(相对于说在 SEQUENCE 和 IDENTITY 之间进行选择)。此后备存储对用户完全透明。

配置此生成器的首选(便携式)方法是使用 JPA 定义的javax.persistence.SequenceGeneratorComments。

最简单的形式是简单地请求序列生成。对于所有此类未命名的定义,Hibernate 将使用单个隐式命名的序列(hibernate_sequence)。

例子 141.未命名的序列

@Entity(name = "Product")
public static class Product {

	@Id
	@GeneratedValue(
		strategy = GenerationType.SEQUENCE
	)
	private Long id;

	@Column(name = "product_name")
	private String name;

	//Getters and setters are omitted for brevity

}

使用javax.persistence.SequenceGenerator,可以指定特定的数据库序列名称。

例子 142.命名序列

@Entity(name = "Product")
public static class Product {

	@Id
	@GeneratedValue(
		strategy = GenerationType.SEQUENCE,
		generator = "sequence-generator"
	)
	@SequenceGenerator(
		name = "sequence-generator",
		sequenceName = "product_sequence"
	)
	private Long id;

	@Column(name = "product_name")
	private String name;

	//Getters and setters are omitted for brevity

}

javax.persistence.SequenceGeneratorComments 还允许您指定其他配置。

例子 143.配置的序列

@Entity(name = "Product")
public static class Product {

	@Id
	@GeneratedValue(
		strategy = GenerationType.SEQUENCE,
		generator = "sequence-generator"
	)
	@SequenceGenerator(
		name = "sequence-generator",
		sequenceName = "product_sequence",
		allocationSize = 5
	)
	private Long id;

	@Column(name = "product_name")
	private String name;

	//Getters and setters are omitted for brevity

}

2.6.10. 使用 IDENTITY 列

为了实现基于 IDENTITY 列的标识符值生成,Hibernate 利用其org.hibernate.id.IdentityGenerator id 生成器,该生成器期望通过 INSERT 将标识符生成到表中。 IdentityGenerator 了解可以检索 INSERT 生成的值的 3 种不同方式:

  • 如果 Hibernate 认为 JDBC 环境支持java.sql.Statement#getGeneratedKeys,那么该方法将用于提取 IDENTITY 生成的键。

  • 否则,如果Dialect#supportsInsertSelectIdentity报告为 true,则 Hibernate 将使用方言特定的 INSERT SELECT 语句语法。

  • 否则,Hibernate 将期望数据库支持某种形式的查询,这些请求通过Dialect#getIdentitySelectString指示的单独 SQL 命令来请求最近插入的 IDENTITY 值。

Tip

重要的是要认识到,使用 IDENTITY 列会强加一种运行时行为,其中必须在知道标识符值之前物理上**插入实体行。

这会弄乱扩展的持久性上下文(长时间的对话)。由于运行时强加/不一致,Hibernate 建议使用其他形式的标识符值生成(例如 SEQUENCE)。

Note

选择 IDENTITY 生成还有另一个重要的运行时影响:Hibernate 将无法使用 IDENTITY 生成为实体批处理 INSERT 语句。

这的重要性取决于特定于应用程序的用例。如果应用程序通常不使用 IDENTITY 生成器创建给定实体类型的许多新实例,则此限制将变得不那么重要,因为无论如何批处理都不会很有帮助。

2.6.11. 使用表标识符生成器

Hibernate 基于其org.hibernate.id.enhanced.TableGenerator实现了基于表的标识符生成,该org.hibernate.id.enhanced.TableGenerator定义了一个表,该表能够为任意数量的实体保存多个命名值段。

基本思想是给定的表生成器表(例如hibernate_sequences)可以保存标识符生成值的多个段。

例子 144.未命名表生成器

@Entity(name = "Product")
public static class Product {

	@Id
	@GeneratedValue(
		strategy = GenerationType.TABLE
	)
	private Long id;

	@Column(name = "product_name")
	private String name;

	//Getters and setters are omitted for brevity

}
create table hibernate_sequences (
    sequence_name varchar2(255 char) not null,
    next_val number(19,0),
    primary key (sequence_name)
)

如果未给出表名,则 Hibernate 假定隐式名称为hibernate_sequences

另外,由于未指定javax.persistence.TableGenerator#pkColumnValue,因此 Hibernate 将使用 hibernate_sequences 表中的默认段(sequence_name='default')。

但是,您可以使用@TableGeneratorComments 配置表标识符生成器。

例子 145.配置的表生成器

@Entity(name = "Product")
public static class Product {

	@Id
	@GeneratedValue(
		strategy = GenerationType.TABLE,
		generator = "table-generator"
	)
	@TableGenerator(
		name =  "table-generator",
		table = "table_identifier",
		pkColumnName = "table_name",
		valueColumnName = "product_id",
		allocationSize = 5
	)
	private Long id;

	@Column(name = "product_name")
	private String name;

	//Getters and setters are omitted for brevity

}
create table table_identifier (
    table_name varchar2(255 char) not null,
    product_id number(19,0),
    primary key (table_name)
)

现在,当插入 3 个Product实体时,Hibernate 生成以下语句:

例子 146.配置的表生成器持久化例子

for ( long i = 1; i <= 3; i++ ) {
	Product product = new Product();
	product.setName( String.format( "Product %d", i ) );
	entityManager.persist( product );
}
select
    tbl.product_id
from
    table_identifier tbl
where
    tbl.table_name = ?
for update

-- binding parameter [1] - [Product]

insert
into
    table_identifier
    (table_name, product_id)
values
    (?, ?)

-- binding parameter [1] - [Product]
-- binding parameter [2] - [1]

update
    table_identifier
set
    product_id= ?
where
    product_id= ?
    and table_name= ?

-- binding parameter [1] - [6]
-- binding parameter [2] - [1]

select
    tbl.product_id
from
    table_identifier tbl
where
    tbl.table_name= ? for update

update
    table_identifier
set
    product_id= ?
where
    product_id= ?
    and table_name= ?

-- binding parameter [1] - [11]
-- binding parameter [2] - [6]

insert
into
    Product
    (product_name, id)
values
    (?, ?)

-- binding parameter [1] as [VARCHAR] - [Product 1]
-- binding parameter [2] as [BIGINT]  - [1]

insert
into
    Product
    (product_name, id)
values
    (?, ?)

-- binding parameter [1] as [VARCHAR] - [Product 2]
-- binding parameter [2] as [BIGINT]  - [2]

insert
into
    Product
    (product_name, id)
values
    (?, ?)

-- binding parameter [1] as [VARCHAR] - [Product 3]
-- binding parameter [2] as [BIGINT]  - [3]

2.6.12. 使用 UUID 生成

如上所述,Hibernate 支持 UUID 标识符值生成。它的org.hibernate.id.UUIDGenerator id 生成器对此提供支持。

UUIDGenerator支持可插拔策略,以准确生成 UUID。这些策略由org.hibernate.id.UUIDGenerationStrategyContract 定义。默认策略是根据 IETF RFC 4122 的版本 4(随机)策略。Hibernate 确实随附了另一种策略,即 RFC 4122 版本 1(基于时间)策略(使用 IP 地址而不是 mac 地址)。

例子 147.隐式地使用随机 UUID 策略

@Entity(name = "Book")
public static class Book {

	@Id
	@GeneratedValue
	private UUID id;

	private String title;

	private String author;

	//Getters and setters are omitted for brevity
}

要指定替代的生成策略,我们必须通过@GenericGenerator定义一些配置。在这里,我们选择名为org.hibernate.id.uuid.CustomVersionOneStrategy的符合 RFC 4122 版本 1 的策略。

例子 148.隐式地使用随机 UUID 策略

@Entity(name = "Book")
public static class Book {

	@Id
	@GeneratedValue( generator = "custom-uuid" )
	@GenericGenerator(
		name = "custom-uuid",
		strategy = "org.hibernate.id.UUIDGenerator",
		parameters = {
			@Parameter(
				name = "uuid_gen_strategy_class",
				value = "org.hibernate.id.uuid.CustomVersionOneStrategy"
			)
		}
	)
	private UUID id;

	private String title;

	private String author;

	//Getters and setters are omitted for brevity
}

2.6.13. Optimizers

大多数从数据库结构中分别获取标识符值的 Hibernate 生成器都支持可插入优化器的使用。优化器可帮助 ManagementHibernate 与数据库对话以生成标识符值的次数。例如,在没有将优化器应用于序列生成器的情况下,每次应用程序要求 Hibernate 生成标识符时,它都需要从数据库中获取下一个序列值。但是,如果我们可以最大程度地减少与数据库通信的次数,则应用程序将能够更好地运行,这实际上就是这些优化器的作用。

  • none

    • 没有执行优化。每当生成器需要标识符值时,我们都会与数据库进行通信。
  • pooled-lo

    • pool-lo 优化器的工作原理是将增量值编码到数据库表/序列结构中。在序列项中,这意味着序列以大于 1 的增量大小定义。

例如,考虑定义为create sequence m_sequence start with 1 increment by 20的全新序列。每次我们要求它的下一个值时,此序列实质上定义了一个由 20 个可用 id 值组成的“池”。 pool-lo 优化器将下一个值解释为该池的低端。

因此,当我们第一次要求它的下一个值时,我们将得到 1.然后,我们假定有效池将是 1 到 20 之间的值。

对该序列的下一个调用将得到 21,它将 21-40 定义为有效范围。等等。名称的“ lo”部分表示来自数据库表/序列的值被解释为池 lo(w)的末尾。

  • pooled

    • 就像 pooled-lo 一样,除了这里的表/序列中的值被解释为值池的高端。
  • hilo; legacy-hilo

    • 定义一个自定义算法,用于基于表或序列中的单个值生成值池。

不建议使用这些优化器。它们在这里维护(并提到)仅供以前使用这些策略的旧应用程序使用。

Note

应用程序还可以根据org.hibernate.id.enhanced.OptimizerContract 定义并实施自己的优化器策略。

2.6.14. 使用@GenericGenerator

@GenericGenerator允许集成任何 Hibernate org.hibernate.id.IdentifierGenerator实现,包括此处讨论的任何特定实现和任何自定义实现。

要使用池式或池式优化器,实体 Map 必须使用@GenericGenerator注解:

例子 149.使用@GenericGeneratorMap 的 Pool-lo 优化器 Map

@Entity(name = "Product")
public static class Product {

	@Id
	@GeneratedValue(
		strategy = GenerationType.SEQUENCE,
		generator = "product_generator"
	)
	@GenericGenerator(
		name = "product_generator",
		strategy = "org.hibernate.id.enhanced.SequenceStyleGenerator",
		parameters = {
			@Parameter(name = "sequence_name", value = "product_sequence"),
			@Parameter(name = "initial_value", value = "1"),
			@Parameter(name = "increment_size", value = "3"),
			@Parameter(name = "optimizer", value = "pooled-lo")
		}
	)
	private Long id;

	@Column(name = "p_name")
	private String name;

	@Column(name = "p_number")
	private String number;

	//Getters and setters are omitted for brevity

}

现在,当保存 5 个Person实体并在每 3 个实体后刷新持久化上下文时:

例子 150.使用@GenericGeneratorMap 的 pool-lo 优化器 Map

for ( long i = 1; i <= 5; i++ ) {
	if(i % 3 == 0) {
		entityManager.flush();
	}
	Product product = new Product();
	product.setName( String.format( "Product %d", i ) );
	product.setNumber( String.format( "P_100_%d", i ) );
	entityManager.persist( product );
}
CALL NEXT VALUE FOR product_sequence

INSERT INTO Product (p_name, p_number, id)
VALUES (?, ?, ?)

-- binding parameter [1] as [VARCHAR] - [Product 1]
-- binding parameter [2] as [VARCHAR] - [P_100_1]
-- binding parameter [3] as [BIGINT]  - [1]

INSERT INTO Product (p_name, p_number, id)
VALUES (?, ?, ?)

-- binding parameter [1] as [VARCHAR] - [Product 2]
-- binding parameter [2] as [VARCHAR] - [P_100_2]
-- binding parameter [3] as [BIGINT]  - [2]

CALL NEXT VALUE FOR product_sequence

INSERT INTO Product (p_name, p_number, id)
VALUES (?, ?, ?)

-- binding parameter [1] as [VARCHAR] - [Product 3]
-- binding parameter [2] as [VARCHAR] - [P_100_3]
-- binding parameter [3] as [BIGINT]  - [3]

INSERT INTO Product (p_name, p_number, id)
VALUES (?, ?, ?)

-- binding parameter [1] as [VARCHAR] - [Product 4]
-- binding parameter [2] as [VARCHAR] - [P_100_4]
-- binding parameter [3] as [BIGINT]  - [4]

INSERT INTO Product (p_name, p_number, id)
VALUES (?, ?, ?)

-- binding parameter [1] as [VARCHAR] - [Product 5]
-- binding parameter [2] as [VARCHAR] - [P_100_5]
-- binding parameter [3] as [BIGINT]  - [5]

从生成的 SQL 语句列表中可以看到,只需一个数据库序列调用就可以插入 3 个实体。这样,池优化器和池优化器可以减少数据库往返的次数,从而减少总体事务响应时间。

2.6.15. 派生标识符

JPA 2.0 增加了对派生标识符的支持,允许实体从多对一或一对一关联中借用该标识符。

例子 151.派生的标识符为@MapsId

@Entity(name = "Person")
public static class Person  {

	@Id
	private Long id;

	@NaturalId
	private String registrationNumber;

	public Person() {}

	public Person(String registrationNumber) {
		this.registrationNumber = registrationNumber;
	}

	//Getters and setters are omitted for brevity
}

@Entity(name = "PersonDetails")
public static class PersonDetails  {

	@Id
	private Long id;

	private String nickName;

	@OneToOne
	@MapsId
	private Person person;

	//Getters and setters are omitted for brevity
}

在上面的示例中,PersonDetails实体将id列用于实体标识符和与Person实体的一对一关联。 PersonDetails实体标识符的值是从其父Person实体的标识符“派生”的。

例子 152.带有@MapsId持久性例子的派生标识符

doInJPA( this::entityManagerFactory, entityManager -> {
	Person person = new Person( "ABC-123" );
	person.setId( 1L );
	entityManager.persist( person );

	PersonDetails personDetails = new PersonDetails();
	personDetails.setNickName( "John Doe" );
	personDetails.setPerson( person );

	entityManager.persist( personDetails );
} );

doInJPA( this::entityManagerFactory, entityManager -> {
	PersonDetails personDetails = entityManager.find( PersonDetails.class, 1L );

	assertEquals("John Doe", personDetails.getNickName());
} );

@MapsIdComments 也可以引用@EmbeddedId标识符中的列。

上一个示例也可以使用@PrimaryKeyJoinColumn进行 Map。

例子 153.派生标识符@PrimaryKeyJoinColumn

@Entity(name = "Person")
public static class Person  {

	@Id
	private Long id;

	@NaturalId
	private String registrationNumber;

	public Person() {}

	public Person(String registrationNumber) {
		this.registrationNumber = registrationNumber;
	}

	//Getters and setters are omitted for brevity
}

@Entity(name = "PersonDetails")
public static class PersonDetails  {

	@Id
	private Long id;

	private String nickName;

	@OneToOne
	@PrimaryKeyJoinColumn
	private Person person;

	public void setPerson(Person person) {
		this.person = person;
		this.id = person.getId();
	}

	//Other getters and setters are omitted for brevity
}

Note

@MapsId不同,应用程序开发人员负责确保实体标识符和多对一(或一对一)关联是同步的,正如您在PersonDetails#setPerson方法中看到的那样。

2.6.16. @RowId

如果您使用@RowIdComments 对给定的实体进行 Comments,并且基础数据库支持通过 ROWID(例如 Oracle)提取记录,则 Hibernate 可以将ROWID伪列用于 CRUD 操作。

例子 154. @RowId实体 Map

@Entity(name = "Product")
@RowId("ROWID")
public static class Product {

	@Id
	private Long id;

	@Column(name = "`name`")
	private String name;

	@Column(name = "`number`")
	private String number;

	//Getters and setters are omitted for brevity

}

现在,当获取一个实体并对其进行修改时,Hibernate 将ROWID伪列用于 UPDATE SQL 语句。

例子 155. @RowId例子

Product product = entityManager.find( Product.class, 1L );

product.setName( "Smart phone" );
SELECT
    p.id as id1_0_0_,
    p."name" as name2_0_0_,
    p."number" as number3_0_0_,
    p.ROWID as rowid_0_
FROM
    Product p
WHERE
    p.id = ?

-- binding parameter [1] as [BIGINT] - [1]

-- extracted value ([name2_0_0_] : [VARCHAR]) - [Mobile phone]
-- extracted value ([number3_0_0_] : [VARCHAR]) - [123-456-7890]
-- extracted ROWID value: AAAwkBAAEAAACP3AAA

UPDATE
    Product
SET
    "name" = ?,
    "number" = ?
WHERE
    ROWID = ?

-- binding parameter [1] as [VARCHAR] - [Smart phone]
-- binding parameter [2] as [VARCHAR] - [123-456-7890]
-- binding parameter [3] as ROWID     - [AAAwkBAAEAAACP3AAA]

2.7. Associations

关联描述基于数据库连接语义的两个或多个实体如何形成关系。

2.7.1. @ManyToOne

@ManyToOne是最常见的关联,在关系数据库中也具有直接等效项(例如外键),因此它在子实体和父实体之间构建关系。

例子 156. @ManyToOne关联

@Entity(name = "Person")
public static class Person {

	@Id
	@GeneratedValue
	private Long id;

	//Getters and setters are omitted for brevity

}

@Entity(name = "Phone")
public static class Phone {

	@Id
	@GeneratedValue
	private Long id;

	@Column(name = "`number`")
	private String number;

	@ManyToOne
	@JoinColumn(name = "person_id",
			foreignKey = @ForeignKey(name = "PERSON_ID_FK")
	)
	private Person person;

	//Getters and setters are omitted for brevity

}
CREATE TABLE Person (
    id BIGINT NOT NULL ,
    PRIMARY KEY ( id )
)

CREATE TABLE Phone (
    id BIGINT NOT NULL ,
    number VARCHAR(255) ,
    person_id BIGINT ,
    PRIMARY KEY ( id )
 )

ALTER TABLE Phone
ADD CONSTRAINT PERSON_ID_FK
FOREIGN KEY (person_id) REFERENCES Person

每个实体都有其自己的生命周期。设置@ManyToOne关联后,Hibernate 将设置关联的数据库外键列。

例子 157. @ManyToOne关联生命周期

Person person = new Person();
entityManager.persist( person );

Phone phone = new Phone( "123-456-7890" );
phone.setPerson( person );
entityManager.persist( phone );

entityManager.flush();
phone.setPerson( null );
INSERT INTO Person ( id )
VALUES ( 1 )

INSERT INTO Phone ( number, person_id, id )
VALUES ( '123-456-7890', 1, 2 )

UPDATE Phone
SET    number = '123-456-7890',
       person_id = NULL
WHERE  id = 2

2.7.2. @OneToMany

@OneToMany关联将一个父实体与一个或多个子实体链接。如果@OneToMany在子端没有镜像@ManyToOne关联,则@OneToMany关联是单向的。如果子级上有@ManyToOne关联,则@OneToMany关联是双向的,应用程序开发人员可以从两端导航此关系。

Unidirectional @OneToMany

当使用单向@OneToMany关联时,Hibernate 会使用两个连接实体之间的链接表。

例子 158.单向@OneToMany关联

@Entity(name = "Person")
public static class Person {

	@Id
	@GeneratedValue
	private Long id;

	@OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
	private List<Phone> phones = new ArrayList<>();

	//Getters and setters are omitted for brevity

}

@Entity(name = "Phone")
public static class Phone {

	@Id
	@GeneratedValue
	private Long id;

	@Column(name = "`number`")
	private String number;

	//Getters and setters are omitted for brevity

}
CREATE TABLE Person (
    id BIGINT NOT NULL ,
    PRIMARY KEY ( id )
)

CREATE TABLE Person_Phone (
    Person_id BIGINT NOT NULL ,
    phones_id BIGINT NOT NULL
)

CREATE TABLE Phone (
    id BIGINT NOT NULL ,
    number VARCHAR(255) ,
    PRIMARY KEY ( id )
)

ALTER TABLE Person_Phone
ADD CONSTRAINT UK_9uhc5itwc9h5gcng944pcaslf
UNIQUE (phones_id)

ALTER TABLE Person_Phone
ADD CONSTRAINT FKr38us2n8g5p9rj0b494sd3391
FOREIGN KEY (phones_id) REFERENCES Phone

ALTER TABLE Person_Phone
ADD CONSTRAINT FK2ex4e4p7w1cj310kg2woisjl2
FOREIGN KEY (Person_id) REFERENCES Person

Note

顾名思义,@OneToMany关联是父关联,无论它是单向还是双向。只有关联的父方才有意义将其实体状态转换级联到子级。

例子 159.级联@OneToMany关联

Person person = new Person();
Phone phone1 = new Phone( "123-456-7890" );
Phone phone2 = new Phone( "321-654-0987" );

person.getPhones().add( phone1 );
person.getPhones().add( phone2 );
entityManager.persist( person );
entityManager.flush();

person.getPhones().remove( phone1 );
INSERT INTO Person
       ( id )
VALUES ( 1 )

INSERT INTO Phone
       ( number, id )
VALUES ( '123-456-7890', 2 )

INSERT INTO Phone
       ( number, id )
VALUES ( '321-654-0987', 3 )

INSERT INTO Person_Phone
       ( Person_id, phones_id )
VALUES ( 1, 2 )

INSERT INTO Person_Phone
       ( Person_id, phones_id )
VALUES ( 1, 3 )

DELETE FROM Person_Phone
WHERE  Person_id = 1

INSERT INTO Person_Phone
       ( Person_id, phones_id )
VALUES ( 1, 3 )

DELETE FROM Phone
WHERE  id = 2

当持久化Person实体时,级联还将持久化操作传播到基础的Phone子级。从 phone 集合中删除Phone后,关联行将从链接表中删除,并且orphanRemoval属性也将触发Phone删除。

Note

在删除子实体时,单向关联不是非常有效。在上面的示例中,刷新持久性上下文后,Hibernate 从链接表(例如Person_Phone)中删除与父Person实体关联的所有数据库行,然后重新插入仍在@OneToMany集合中找到的行。

另一方面,双向@OneToMany关联效率更高,因为子实体控制该关联。

Bidirectional @OneToMany

双向@OneToMany关联在子端也需要@ManyToOne关联。尽管域模型公开了两个方面来导航此关联,但在后台,关系数据库只有一个用于此关系的外键。

每个双向关联都必须仅具有一个拥有侧(子侧),另一侧称为反向(或mappedBy)侧。

例子 160. @OneToMany关联被 Map 到@ManyToOne

@Entity(name = "Person")
public static class Person {

	@Id
	@GeneratedValue
	private Long id;

	@OneToMany(mappedBy = "person", cascade = CascadeType.ALL, orphanRemoval = true)
	private List<Phone> phones = new ArrayList<>();

	//Getters and setters are omitted for brevity

	public void addPhone(Phone phone) {
		phones.add( phone );
		phone.setPerson( this );
	}

	public void removePhone(Phone phone) {
		phones.remove( phone );
		phone.setPerson( null );
	}
}

@Entity(name = "Phone")
public static class Phone {

	@Id
	@GeneratedValue
	private Long id;

	@NaturalId
	@Column(name = "`number`", unique = true)
	private String number;

	@ManyToOne
	private Person person;

	//Getters and setters are omitted for brevity

	@Override
	public boolean equals(Object o) {
		if ( this == o ) {
			return true;
		}
		if ( o == null || getClass() != o.getClass() ) {
			return false;
		}
		Phone phone = (Phone) o;
		return Objects.equals( number, phone.number );
	}

	@Override
	public int hashCode() {
		return Objects.hash( number );
	}
}
CREATE TABLE Person (
    id BIGINT NOT NULL ,
    PRIMARY KEY ( id )
)

CREATE TABLE Phone (
    id BIGINT NOT NULL ,
    number VARCHAR(255) ,
    person_id BIGINT ,
    PRIMARY KEY ( id )
)

ALTER TABLE Phone
ADD CONSTRAINT UK_l329ab0g4c1t78onljnxmbnp6
UNIQUE (number)

ALTER TABLE Phone
ADD CONSTRAINT FKmw13yfsjypiiq0i1osdkaeqpg
FOREIGN KEY (person_id) REFERENCES Person

Tip

每当形成双向关联时,应用程序开发人员都必须确保双方始终保持同步。

addPhone()removePhone()是 Util 方法,可在添加或删除子元素时同步两端。

因为Phone类具有@NaturalId列(电话 Numbers 是唯一的),所以equals()hashCode()可以利用此属性,因此removePhone()逻辑简化为remove() Java Collection方法。

例子 161.具有所有者@ManyToOne生命周期的双向@OneToMany

Person person = new Person();
Phone phone1 = new Phone( "123-456-7890" );
Phone phone2 = new Phone( "321-654-0987" );

person.addPhone( phone1 );
person.addPhone( phone2 );
entityManager.persist( person );
entityManager.flush();

person.removePhone( phone1 );
INSERT INTO Person
       ( id )
VALUES ( 1 )

INSERT INTO Phone
       ( "number", person_id, id )
VALUES ( '123-456-7890', 1, 2 )

INSERT INTO Phone
       ( "number", person_id, id )
VALUES ( '321-654-0987', 1, 3 )

DELETE FROM Phone
WHERE  id = 2

与单向@OneToMany不同,双向关联在 Management 收集持久性状态时效率更高。每次删除元素都只需要进行一次更新(外键列设置为NULL),并且,如果子实体生命周期绑定到其拥有的父代,这样子代在没有父代的情况下就无法存在,那么我们可以 Comments 关联具有orphanRemoval属性并取消关联子级也会在实际的子表行上触发一条 delete 语句。

2.7.3. @OneToOne

@OneToOne关联可以是单向或双向的。单向关联遵循关系数据库外键语义,Client 端拥有该关系。双向关联也具有mappedBy @OneToOne父侧。

Unidirectional @OneToOne

例子 162.单向的@OneToOne

@Entity(name = "Phone")
public static class Phone {

	@Id
	@GeneratedValue
	private Long id;

	@Column(name = "`number`")
	private String number;

	@OneToOne
	@JoinColumn(name = "details_id")
	private PhoneDetails details;

	//Getters and setters are omitted for brevity

}

@Entity(name = "PhoneDetails")
public static class PhoneDetails {

	@Id
	@GeneratedValue
	private Long id;

	private String provider;

	private String technology;

	//Getters and setters are omitted for brevity

}
CREATE TABLE Phone (
    id BIGINT NOT NULL ,
    number VARCHAR(255) ,
    details_id BIGINT ,
    PRIMARY KEY ( id )
)

CREATE TABLE PhoneDetails (
    id BIGINT NOT NULL ,
    provider VARCHAR(255) ,
    technology VARCHAR(255) ,
    PRIMARY KEY ( id )
)

ALTER TABLE Phone
ADD CONSTRAINT FKnoj7cj83ppfqbnvqqa5kolub7
FOREIGN KEY (details_id) REFERENCES PhoneDetails

从关系数据库的角度来看,底层架构与单向@ManyToOne关联相同,因为 Client 端基于外键列控制关系。

但是,然后将Phone视为 Client 端,将PhoneDetails视为父端是不寻常的,因为没有实际的电话,详细信息就无法存在。更为自然的 Map 是Phone是父方,因此将外键推入PhoneDetails表。如下面的示例所示,此 Map 需要双向@OneToOne关联:

Bidirectional @OneToOne

例子 163.双向@OneToOne

@Entity(name = "Phone")
public static class Phone {

	@Id
	@GeneratedValue
	private Long id;

	@Column(name = "`number`")
	private String number;

	@OneToOne(
		mappedBy = "phone",
		cascade = CascadeType.ALL,
		orphanRemoval = true,
		fetch = FetchType.LAZY
	)
	private PhoneDetails details;

	//Getters and setters are omitted for brevity

	public void addDetails(PhoneDetails details) {
		details.setPhone( this );
		this.details = details;
	}

	public void removeDetails() {
		if ( details != null ) {
			details.setPhone( null );
			this.details = null;
		}
	}
}

@Entity(name = "PhoneDetails")
public static class PhoneDetails {

	@Id
	@GeneratedValue
	private Long id;

	private String provider;

	private String technology;

	@OneToOne(fetch = FetchType.LAZY)
	@JoinColumn(name = "phone_id")
	private Phone phone;

	//Getters and setters are omitted for brevity

}
CREATE TABLE Phone (
    id BIGINT NOT NULL ,
    number VARCHAR(255) ,
    PRIMARY KEY ( id )
)

CREATE TABLE PhoneDetails (
    id BIGINT NOT NULL ,
    provider VARCHAR(255) ,
    technology VARCHAR(255) ,
    phone_id BIGINT ,
    PRIMARY KEY ( id )
)

ALTER TABLE PhoneDetails
ADD CONSTRAINT FKeotuev8ja8v0sdh29dynqj05p
FOREIGN KEY (phone_id) REFERENCES Phone

这次,PhoneDetails拥有该关联,并且像任何双向关联一样,父端可以通过级联将其生命周期传播到子端。

例子 164.双向@OneToOne生命周期

Phone phone = new Phone( "123-456-7890" );
PhoneDetails details = new PhoneDetails( "T-Mobile", "GSM" );

phone.addDetails( details );
entityManager.persist( phone );
INSERT INTO Phone ( number, id )
VALUES ( '123-456-7890', 1 )

INSERT INTO PhoneDetails ( phone_id, provider, technology, id )
VALUES ( 1, 'T-Mobile', 'GSM', 2 )

当使用双向@OneToOne关联时,Hibernate 在获取子端时会强制执行唯一约束。如果同一个 parent 有多个子女,则 Hibernate 将抛出org.hibernate.exception.ConstraintViolationException。continue 前面的示例,当添加另一个PhoneDetails时,Hibernate 在重新加载Phone对象时验证唯一性约束。

例子 165.双向@OneToOne唯一约束

PhoneDetails otherDetails = new PhoneDetails( "T-Mobile", "CDMA" );
otherDetails.setPhone( phone );
entityManager.persist( otherDetails );
entityManager.flush();
entityManager.clear();

//throws javax.persistence.PersistenceException: org.hibernate.HibernateException: More than one row with the given identifier was found: 1
phone = entityManager.find( Phone.class, phone.getId() );
双向@OneToOne 惰性关联

尽管您可能会 Comments 要延迟获取的父端关联,但是 Hibernate 无法接受此请求,因为它无法知道关联是否为null

找出子方是否有关联记录的唯一方法是使用辅助查询来获取子关联。因为这可能会导致 N 1 个查询问题,所以使用具有@MapsId注解的单向@OneToOne关联会更加有效。

但是,如果您确实需要使用双向关联,并且要确保始终懒惰地获取它,那么您需要启用惰性状态初始化字节码增强功能,并同时使用@LazyToOneComments。

例子 166.双向@OneToOne懒惰的 parent 一方关联

@Entity(name = "Phone")
public static class Phone {

	@Id
	@GeneratedValue
	private Long id;

	@Column(name = "`number`")
	private String number;

	@OneToOne(
		mappedBy = "phone",
		cascade = CascadeType.ALL,
		orphanRemoval = true,
		fetch = FetchType.LAZY
	)
	@LazyToOne( LazyToOneOption.NO_PROXY )
	private PhoneDetails details;

	//Getters and setters are omitted for brevity

	public void addDetails(PhoneDetails details) {
		details.setPhone( this );
		this.details = details;
	}

	public void removeDetails() {
		if ( details != null ) {
			details.setPhone( null );
			this.details = null;
		}
	}
}

@Entity(name = "PhoneDetails")
public static class PhoneDetails {

	@Id
	@GeneratedValue
	private Long id;

	private String provider;

	private String technology;

	@OneToOne(fetch = FetchType.LAZY)
	@JoinColumn(name = "phone_id")
	private Phone phone;

	//Getters and setters are omitted for brevity

}

有关如何启用字节码增强的更多信息,请参见字节码增强一章

2.7.4. @ManyToMany

@ManyToMany关联需要连接两个实体的链接表。像@OneToMany关联一样,@ManyToMany可以是单向或双向的。

Unidirectional @ManyToMany

例子 167.单向的@ManyToMany

@Entity(name = "Person")
public static class Person {

	@Id
	@GeneratedValue
	private Long id;

	@ManyToMany(cascade = {CascadeType.PERSIST, CascadeType.MERGE})
	private List<Address> addresses = new ArrayList<>();

	//Getters and setters are omitted for brevity

}

@Entity(name = "Address")
public static class Address {

	@Id
	@GeneratedValue
	private Long id;

	private String street;

	@Column(name = "`number`")
	private String number;

	//Getters and setters are omitted for brevity

}
CREATE TABLE Address (
    id BIGINT NOT NULL ,
    number VARCHAR(255) ,
    street VARCHAR(255) ,
    PRIMARY KEY ( id )
)

CREATE TABLE Person (
    id BIGINT NOT NULL ,
    PRIMARY KEY ( id )
)

CREATE TABLE Person_Address (
    Person_id BIGINT NOT NULL ,
    addresses_id BIGINT NOT NULL
)

ALTER TABLE Person_Address
ADD CONSTRAINT FKm7j0bnabh2yr0pe99il1d066u
FOREIGN KEY (addresses_id) REFERENCES Address

ALTER TABLE Person_Address
ADD CONSTRAINT FKba7rc9qe2vh44u93u0p2auwti
FOREIGN KEY (Person_id) REFERENCES Person

就像单向@OneToMany关联一样,链接表由拥有方控制。

@ManyToMany集合中删除实体时,Hibernate 只需删除链接表中的加入记录。不幸的是,此操作需要删除与给定父级关联的所有条目,并重新创建当前正在运行的持久性上下文中列出的条目。

例子 168.单向的@ManyToMany生命周期

Person person1 = new Person();
Person person2 = new Person();

Address address1 = new Address( "12th Avenue", "12A" );
Address address2 = new Address( "18th Avenue", "18B" );

person1.getAddresses().add( address1 );
person1.getAddresses().add( address2 );

person2.getAddresses().add( address1 );

entityManager.persist( person1 );
entityManager.persist( person2 );

entityManager.flush();

person1.getAddresses().remove( address1 );
INSERT INTO Person ( id )
VALUES ( 1 )

INSERT INTO Address ( number, street, id )
VALUES ( '12A', '12th Avenue', 2 )

INSERT INTO Address ( number, street, id )
VALUES ( '18B', '18th Avenue', 3 )

INSERT INTO Person ( id )
VALUES ( 4 )

INSERT INTO Person_Address ( Person_id, addresses_id )
VALUES ( 1, 2 )
INSERT INTO Person_Address ( Person_id, addresses_id )
VALUES ( 1, 3 )
INSERT INTO Person_Address ( Person_id, addresses_id )
VALUES ( 4, 2 )

DELETE FROM Person_Address
WHERE  Person_id = 1

INSERT INTO Person_Address ( Person_id, addresses_id )
VALUES ( 1, 3 )

Note

对于@ManyToMany关联,将REMOVE实体状态转换进行级联是没有意义的,因为它会传播到链接表之外。由于另一端可能被父端上的其他实体引用,因此自动删除可能以ConstraintViolationException结尾。

例如,如果定义了@ManyToMany(cascade = CascadeType.ALL)并且将删除第一个人,则 Hibernate 将引发异常,因为另一个人仍与要删除的地址相关联。

Person person1 = entityManager.find(Person.class, personId);
entityManager.remove(person1);

Caused by: javax.persistence.PersistenceException: org.hibernate.exception.ConstraintViolationException: could not execute statement
Caused by: org.hibernate.exception.ConstraintViolationException: could not execute statement
Caused by: java.sql.SQLIntegrityConstraintViolationException: integrity constraint violation: foreign key no action; FKM7J0BNABH2YR0PE99IL1D066U table: PERSON_ADDRESS

通过简单地删除父端,Hibernate 可以安全地删除关联的链接记录,如以下示例所示:

例子 169.单向@ManyToMany实体移除

Person person1 = entityManager.find( Person.class, personId );
entityManager.remove( person1 );
DELETE FROM Person_Address
WHERE  Person_id = 1

DELETE FROM Person
WHERE  id = 1
Bidirectional @ManyToMany

双向@ManyToMany关联具有所有者和mappedBy端。为了保持双方之间的同步,优良作法是提供用于添加或删除子实体的辅助方法。

例子 170.双向@ManyToMany

@Entity(name = "Person")
public static class Person {

	@Id
	@GeneratedValue
	private Long id;

	@NaturalId
	private String registrationNumber;

	@ManyToMany(cascade = {CascadeType.PERSIST, CascadeType.MERGE})
	private List<Address> addresses = new ArrayList<>();

	//Getters and setters are omitted for brevity

	public void addAddress(Address address) {
		addresses.add( address );
		address.getOwners().add( this );
	}

	public void removeAddress(Address address) {
		addresses.remove( address );
		address.getOwners().remove( this );
	}

	@Override
	public boolean equals(Object o) {
		if ( this == o ) {
			return true;
		}
		if ( o == null || getClass() != o.getClass() ) {
			return false;
		}
		Person person = (Person) o;
		return Objects.equals( registrationNumber, person.registrationNumber );
	}

	@Override
	public int hashCode() {
		return Objects.hash( registrationNumber );
	}
}

@Entity(name = "Address")
public static class Address {

	@Id
	@GeneratedValue
	private Long id;

	private String street;

	@Column(name = "`number`")
	private String number;

	private String postalCode;

	@ManyToMany(mappedBy = "addresses")
	private List<Person> owners = new ArrayList<>();

	//Getters and setters are omitted for brevity

	@Override
	public boolean equals(Object o) {
		if ( this == o ) {
			return true;
		}
		if ( o == null || getClass() != o.getClass() ) {
			return false;
		}
		Address address = (Address) o;
		return Objects.equals( street, address.street ) &&
				Objects.equals( number, address.number ) &&
				Objects.equals( postalCode, address.postalCode );
	}

	@Override
	public int hashCode() {
		return Objects.hash( street, number, postalCode );
	}
}
CREATE TABLE Address (
    id BIGINT NOT NULL ,
    number VARCHAR(255) ,
    postalCode VARCHAR(255) ,
    street VARCHAR(255) ,
    PRIMARY KEY ( id )
)

CREATE TABLE Person (
    id BIGINT NOT NULL ,
    registrationNumber VARCHAR(255) ,
    PRIMARY KEY ( id )
)

CREATE TABLE Person_Address (
    owners_id BIGINT NOT NULL ,
    addresses_id BIGINT NOT NULL
)

ALTER TABLE Person
ADD CONSTRAINT UK_23enodonj49jm8uwec4i7y37f
UNIQUE (registrationNumber)

ALTER TABLE Person_Address
ADD CONSTRAINT FKm7j0bnabh2yr0pe99il1d066u
FOREIGN KEY (addresses_id) REFERENCES Address

ALTER TABLE Person_Address
ADD CONSTRAINT FKbn86l24gmxdv2vmekayqcsgup
FOREIGN KEY (owners_id) REFERENCES Person

有了辅助方法,就可以简化同步 Management,如下面的示例所示:

例子 171.双向@ManyToMany生命周期

Person person1 = new Person( "ABC-123" );
Person person2 = new Person( "DEF-456" );

Address address1 = new Address( "12th Avenue", "12A", "4005A" );
Address address2 = new Address( "18th Avenue", "18B", "4007B" );

person1.addAddress( address1 );
person1.addAddress( address2 );

person2.addAddress( address1 );

entityManager.persist( person1 );
entityManager.persist( person2 );

entityManager.flush();

person1.removeAddress( address1 );
INSERT INTO Person ( registrationNumber, id )
VALUES ( 'ABC-123', 1 )

INSERT INTO Address ( number, postalCode, street, id )
VALUES ( '12A', '4005A', '12th Avenue', 2 )

INSERT INTO Address ( number, postalCode, street, id )
VALUES ( '18B', '4007B', '18th Avenue', 3 )

INSERT INTO Person ( registrationNumber, id )
VALUES ( 'DEF-456', 4 )

INSERT INTO Person_Address ( owners_id, addresses_id )
VALUES ( 1, 2 )

INSERT INTO Person_Address ( owners_id, addresses_id )
VALUES ( 1, 3 )

INSERT INTO Person_Address ( owners_id, addresses_id )
VALUES ( 4, 2 )

DELETE FROM Person_Address
WHERE  owners_id = 1

INSERT INTO Person_Address ( owners_id, addresses_id )
VALUES ( 1, 3 )

如果双向@OneToMany关联在删除或更改子元素的 Sequences 时表现更好,则@ManyToMany关系不能从这种优化中受益,因为外键侧不受控制。为了克服此限制,必须直接公开链接表,并将@ManyToMany关联拆分为两个双向@OneToMany关系。

具有链接实体的双向多对多

最自然的@ManyToMany关联遵循数据库模式所采用的相同逻辑,并且链接表具有关联的实体,该实体控制需要连接的双方的关系。

例子 172.具有链接实体的双向多对多

@Entity(name = "Person")
public static class Person implements Serializable {

	@Id
	@GeneratedValue
	private Long id;

	@NaturalId
	private String registrationNumber;

	@OneToMany(
		mappedBy = "person",
		cascade = CascadeType.ALL,
		orphanRemoval = true
	)
	private List<PersonAddress> addresses = new ArrayList<>();

	//Getters and setters are omitted for brevity

	public void addAddress(Address address) {
		PersonAddress personAddress = new PersonAddress( this, address );
		addresses.add( personAddress );
		address.getOwners().add( personAddress );
	}

	public void removeAddress(Address address) {
		PersonAddress personAddress = new PersonAddress( this, address );
		address.getOwners().remove( personAddress );
		addresses.remove( personAddress );
		personAddress.setPerson( null );
		personAddress.setAddress( null );
	}

	@Override
	public boolean equals(Object o) {
		if ( this == o ) {
			return true;
		}
		if ( o == null || getClass() != o.getClass() ) {
			return false;
		}
		Person person = (Person) o;
		return Objects.equals( registrationNumber, person.registrationNumber );
	}

	@Override
	public int hashCode() {
		return Objects.hash( registrationNumber );
	}
}

@Entity(name = "PersonAddress")
public static class PersonAddress implements Serializable {

	@Id
	@ManyToOne
	private Person person;

	@Id
	@ManyToOne
	private Address address;

	//Getters and setters are omitted for brevity

	@Override
	public boolean equals(Object o) {
		if ( this == o ) {
			return true;
		}
		if ( o == null || getClass() != o.getClass() ) {
			return false;
		}
		PersonAddress that = (PersonAddress) o;
		return Objects.equals( person, that.person ) &&
				Objects.equals( address, that.address );
	}

	@Override
	public int hashCode() {
		return Objects.hash( person, address );
	}
}

@Entity(name = "Address")
public static class Address implements Serializable {

	@Id
	@GeneratedValue
	private Long id;

	private String street;

	@Column(name = "`number`")
	private String number;

	private String postalCode;

	@OneToMany(
		mappedBy = "address",
		cascade = CascadeType.ALL,
		orphanRemoval = true
	)
	private List<PersonAddress> owners = new ArrayList<>();

	//Getters and setters are omitted for brevity

	@Override
	public boolean equals(Object o) {
		if ( this == o ) {
			return true;
		}
		if ( o == null || getClass() != o.getClass() ) {
			return false;
		}
		Address address = (Address) o;
		return Objects.equals( street, address.street ) &&
				Objects.equals( number, address.number ) &&
				Objects.equals( postalCode, address.postalCode );
	}

	@Override
	public int hashCode() {
		return Objects.hash( street, number, postalCode );
	}
}

PersonAddress都具有mappedBy @OneToMany侧,而PersonAddress拥有personaddress @ManyToOne关联。因为此 Map 是由两个双向关联构成的,所以辅助方法更加相关。

Note

前述示例对链接实体使用了特定于 Hibernate 的 Map,因为 JPA 不允许从多个@ManyToOne关联中构建复合标识符。

有关更多详细信息,请参见具有关联的复合标识符部分。

与先前的双向@ManyToMany情况相比,可以更好地 Management 实体状态转换。

例子 173.具有链接实体生命周期的双向多对多

Person person1 = new Person( "ABC-123" );
Person person2 = new Person( "DEF-456" );

Address address1 = new Address( "12th Avenue", "12A", "4005A" );
Address address2 = new Address( "18th Avenue", "18B", "4007B" );

entityManager.persist( person1 );
entityManager.persist( person2 );

entityManager.persist( address1 );
entityManager.persist( address2 );

person1.addAddress( address1 );
person1.addAddress( address2 );

person2.addAddress( address1 );

entityManager.flush();

log.info( "Removing address" );
person1.removeAddress( address1 );

仅执行一条 delete 语句,因为这次关联是由@ManyToOne端控制的,该端仅需监视基础外键关系的状态即可触发正确的 DML 语句。

2.7.5. @NotFound 关联 Map

当处理不是由外键强制执行的关联时,如果子记录无法引用父实体,则可能会出现不一致的情况。

默认情况下,每当子关联引用不存在的父记录时,Hibernate 都会进行投诉。但是,您可以配置此行为,以便 Hibernate 可以忽略此类异常,只需将null分配为所引用的父对象。

要忽略不存在的父实体引用,即使不建议这样做,也可以使用值为org.hibernate.annotations.NotFoundAction.IGNORE的 Commentsorg.hibernate.annotation.NotFoundComments。

Note

即使将fetch策略设置为FetchType.LAZY,也会始终热切地获取用@NotFound(action = NotFoundAction.IGNORE)Comments 的@ManyToOne@OneToOne关联。

考虑以下CityPerson实体 Map:

例子 174. @NotFoundMap 例子

@Entity
@Table( name = "Person" )
public static class Person {

	@Id
	private Long id;

	private String name;

	private String cityName;

	@ManyToOne
	@NotFound ( action = NotFoundAction.IGNORE )
	@JoinColumn(
		name = "cityName",
		referencedColumnName = "name",
		insertable = false,
		updatable = false
	)
	private City city;

	//Getters and setters are omitted for brevity

}

@Entity
@Table( name = "City" )
public static class City implements Serializable {

	@Id
	@GeneratedValue
	private Long id;

	private String name;

	//Getters and setters are omitted for brevity

}

如果我们的数据库中包含以下实体:

例子 175. @NotFound坚持的例子

City _NewYork = new City();
_NewYork.setName( "New York" );
entityManager.persist( _NewYork );

Person person = new Person();
person.setId( 1L );
person.setName( "John Doe" );
person.setCityName( "New York" );
entityManager.persist( person );

加载Person实体时,Hibernate 能够找到关联的City父实体:

例子 176. @NotFound查找现有实体的例子

Person person = entityManager.find( Person.class, 1L );
assertEquals( "New York", person.getCity().getName() );

但是,如果我们将cityName属性更改为不存在的城市名称:

例子 177. @NotFound更改为不存在的城市例子

person.setCityName( "Atlantis" );

Hibernate 不会抛出任何异常,它将为不存在的City实体引用分配一个值null

例子 178. @NotFound查找不存在的城市例子

Person person = entityManager.find( Person.class, 1L );

assertEquals( "Atlantis", person.getCityName() );
assertNull( null, person.getCity() );

2.7.6. @任何 Map

当可以有多个目标实体时,@AnyMap 对于模拟单向@ManyToOne关联很有用。

因为@AnyMap 定义了到多个表中的类的多态关联,所以此关联类型需要 FK 列,该列提供关联的父标识符和关联的实体类型的元数据信息。

Note

这不是 Map 多态关联的通常方法,您应该仅在特殊情况下使用此方法(例如,审核日志,用户会话数据等)。

@AnyComments 描述了保存元数据信息的列。为了链接元数据信息的值和实际的实体类型,使用@AnyDef@AnyDefs