Preface

Working with both Object-Oriented software and Relational Databases can be cumbersome and time-consuming. Development costs are significantly higher due to a paradigm mismatch between how data is represented in objects versus relational databases. Hibernate is an Object/Relational Mapping solution for Java environments. The term Object/Relational Mapping refers to the technique of mapping data from an object model representation to a relational data model representation (and vice versa).

Hibernate not only takes care of the mapping from Java classes to database tables (and from Java data types to SQL data types), but also provides data query and retrieval facilities. It can significantly reduce development time otherwise spent with manual data handling in SQL and JDBC. Hibernate’s design goal is to relieve the developer from 95% of common data persistence-related programming tasks by eliminating the need for manual, hand-crafted data processing using SQL and JDBC. However, unlike many other persistence solutions, Hibernate does not hide the power of SQL from you and guarantees that your investment in relational technology and knowledge is as valid as always.

Hibernate may not be the best solution for data-centric applications that only use stored-procedures to implement the business logic in the database, it is most useful with object-oriented domain models and business logic in the Java-based middle-tier. However, Hibernate can certainly help you to remove or encapsulate vendor-specific SQL code and will help with the common task of result set translation from a tabular representation to a graph of objects.

System Requirements

Hibernate 6.5 requires at least Java 11 and JDBC 4.2.

Getting Started

While a strong background in SQL is not required to use Hibernate, a basic understanding of its concepts is useful - especially the principles of data modeling. Understanding the basics of transactions and design patterns such as Unit of Work are important as well.

New users may want to first look at the tutorial-style Quick Start guide.

This User Guide is really more of a reference guide. For a more high-level discussion of the most used features of Hibernate, see the Introduction to Hibernate guide.

There is also a series of topical guides providing deep dives into various topics such as logging, compatibility and support, etc.

Get Involved

  • Use Hibernate and report any bugs or issues you find. See Issue Tracker for details.

  • Try your hand at fixing some bugs or implementing enhancements. Again, see Issue Tracker.

  • Engage with the community using the methods listed in the Community section.

  • Help improve this documentation. Contact us on the developer mailing list or Zulip if you have interest.

  • Spread the word. Let the rest of your organization know about the benefits of Hibernate.

1. Compatibility

1.1. Dependencies

Hibernate 6.5.3.Final requires the following dependencies (among others):

Table 1. Compatible versions of dependencies

Version

Java Runtime

11, 17 or 21

Jakarta Persistence

3.1.0

JDBC (bundled with the Java Runtime)

4.2

Find more information for all versions of Hibernate on our compatibility matrix.

The compatibility policy may also be of interest.

If you get Hibernate from Maven Central, it is recommended to import Hibernate Platform as part of your dependency management to keep all its artifact versions aligned.

Gradle
dependencies {
  implementation platform "org.hibernate.orm:hibernate-platform:6.5.3.Final"

  // use the versions from the platform
  implementation "org.hibernate.orm:hibernate-core"
  implementation "jakarta.transaction:jakarta.transaction-api"
}
Maven
<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.hibernate.orm</groupId>
            <artifactId>hibernate-platform</artifactId>
            <version>6.5.3.Final</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>
<!-- use the versions from the platform -->
<dependencies>
    <dependency>
        <groupId>org.hibernate.orm</groupId>
        <artifactId>hibernate-core</artifactId>
    </dependency>
    <dependency>
        <groupId>jakarta.transaction</groupId>
        <artifactId>jakarta.transaction-api</artifactId>
    </dependency>
</dependencies>

1.2. Database

Hibernate 6.5.3.Final is compatible with the following database versions, provided you use the corresponding dialects:

Dialect Minimum Database Version

CockroachDialect

22.2

DB2Dialect

10.5

DB2iDialect

7.1

DB2zDialect

12.1

DerbyDialect

10.15.2

GenericDialect

0.0

H2Dialect

2.1.214

HANADialect

1.0.120

HSQLDialect

2.6.1

MariaDBDialect

10.4

MySQLDialect

8.0

OracleDialect

19.0

PostgreSQLDialect

12.0

PostgresPlusDialect

12.0

SQLServerDialect

11.0

SpannerDialect

0.0

SybaseASEDialect

16.0

SybaseDialect

16.0

TiDBDialect

5.4

2. Architecture

2.1. Overview

Data Access Layers

Hibernate, as an ORM solution, effectively "sits between" the Java application data access layer and the Relational Database, as can be seen in the diagram above. The Java application makes use of the Hibernate APIs to load, store, query, etc. its domain data. Here we will introduce the essential Hibernate APIs. This will be a brief introduction; we will discuss these contracts in detail later.

As a Jakarta Persistence provider, Hibernate implements the Java Persistence API specifications and the association between Jakarta Persistence interfaces and Hibernate specific implementations can be visualized in the following diagram:

image

SessionFactory (org.hibernate.SessionFactory)

A thread-safe (and immutable) representation of the mapping of the application domain model to a database. Acts as a factory for org.hibernate.Session instances. The EntityManagerFactory is the Jakarta Persistence equivalent of a SessionFactory and basically, those two converge into the same SessionFactory implementation.

A SessionFactory is very expensive to create, so, for any given database, the application should have only one associated SessionFactory. The SessionFactory maintains services that Hibernate uses across all Session(s) such as second level caches, connection pools, transaction system integrations, etc.

Session (org.hibernate.Session)

A single-threaded, short-lived object conceptually modeling a "Unit of Work" (PoEAA). In Jakarta Persistence nomenclature, the Session is represented by an EntityManager.

Behind the scenes, the Hibernate Session wraps a JDBC java.sql.Connection and acts as a factory for org.hibernate.Transaction instances. It maintains a generally "repeatable read" persistence context (first level cache) of the application domain model.

Transaction (org.hibernate.Transaction)

A single-threaded, short-lived object used by the application to demarcate individual physical transaction boundaries. EntityTransaction is the Jakarta Persistence equivalent and both act as an abstraction API to isolate the application from the underlying transaction system in use (JDBC or JTA).

3. Domain Model

The term domain model comes from the realm of data modeling. It is the model that ultimately describes the problem domain you are working in. Sometimes you will also hear the term persistent classes.

Ultimately the application domain model is the central character in an ORM. They make up the classes you wish to map. Hibernate works best if these classes follow the Plain Old Java Object (POJO) / JavaBean programming model. However, none of these rules are hard requirements. Indeed, Hibernate assumes very little about the nature of your persistent objects. You can express a domain model in other ways (using trees of java.util.Map instances, for example).

Historically applications using Hibernate would have used its proprietary XML mapping file format for this purpose. With the coming of Jakarta Persistence, most of this information is now defined in a way that is portable across ORM/Jakarta Persistence providers using annotations (and/or standardized XML format). This chapter will focus on Jakarta Persistence mapping where possible. For Hibernate mapping features not supported by Jakarta Persistence we will prefer Hibernate extension annotations.

This chapter mostly uses "implicit naming" for table names, column names, etc. For details on adjusting these names see Naming strategies.

3.1. Mapping types

Hibernate understands both the Java and JDBC representations of application data. The ability to read/write this data from/to the database is the function of a Hibernate type. A type, in this usage, is an implementation of the org.hibernate.type.Type interface. This Hibernate type also describes various behavioral aspects of the Java type such as how to check for equality, how to clone values, etc.

Usage of the word type

The Hibernate type is neither a Java type nor a SQL data type. It provides information about mapping a Java type to an SQL type as well as how to persist and fetch a given Java type to and from a relational database.

When you encounter the term type in discussions of Hibernate, it may refer to the Java type, the JDBC type, or the Hibernate type, depending on the context.

To help understand the type categorizations, let’s look at a simple table and domain model that we wish to map.

Example 1. A simple table and domain model
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 firstName;

	private String middleName;

	private String lastName;

	// getters and setters omitted
}

In the broadest sense, Hibernate categorizes types into two groups:

3.1.1. Value types

A value type is a piece of data that does not define its own lifecycle. It is, in effect, owned by an entity, which defines its lifecycle.

Looked at another way, all the state of an entity is made up entirely of value types. These state fields or JavaBean properties are termed persistent attributes. The persistent attributes of the Contact class are value types.

Value types are further classified into three sub-categories:

Basic types

in mapping the Contact table, all attributes except for name would be basic types. Basic types are discussed in detail in Basic types

Embeddable types

the name attribute is an example of an embeddable type, which is discussed in details in Embeddable types

Collection types

although not featured in the aforementioned example, collection types are also a distinct category among value types. Collection types are further discussed in Collections

3.1.2. Entity types

Entities, by nature of their unique identifier, exist independently of other objects whereas values do not. Entities are domain model classes which correlate to rows in a database table, using a unique identifier. Because of the requirement for a unique identifier, entities exist independently and define their own lifecycle. The Contact class itself would be an example of an entity.

Mapping entities is discussed in detail in Entity types.

3.2. Basic values

A basic type is a mapping between a Java type and a single database column.

Hibernate can map many standard Java types (Integer, String, etc.) as basic types. The mapping for many come from tables B-3 and B-4 in the JDBC specification[jdbc]. Others (URL as VARCHAR, e.g.) simply make sense.

Additionally, Hibernate provides multiple, flexible ways to indicate how the Java type should be mapped to the database.

The Jakarta Persistence specification strictly limits the Java types that can be marked as basic to the following:

Category Package Types

Java primitive types

boolean, int, double, etc.

Primitive wrappers

java.lang

Boolean, Integer, Double, etc.

Strings

java.lang

String

Arbitrary-precision numeric types

java.math

BigInteger and BigDecimal

Date/time types

java.time

LocalDate, LocalTime, LocalDateTime, OffsetTime, OffsetDateTime, Instant

Deprecated date/time types

java.util

Date and Calendar

Deprecated date/time types from

java.sql

Date, Time, Timestamp

Byte and character arrays

byte[] or Byte[], char[] or Character[]

Java enumerated types

Any enum

Serializable types

Any type that implements java.io.Serializable[1]

If provider portability is a concern, you should stick to just these basic types.

Java Persistence 2.1 introduced the jakarta.persistence.AttributeConverter providing support for handling types beyond those defined in the specification. See AttributeConverters for more on this topic.

3.2.1. @Basic

Strictly speaking, a basic type is denoted by the jakarta.persistence.Basic annotation.

Generally, the @Basic annotation can be ignored as it is assumed by default. Both of the following examples are ultimately the same.

Example 2. @Basic explicit
@Entity(name = "Product")
public class Product {

	@Id
	@Basic
	private Integer id;

	@Basic
	private String sku;

	@Basic
	private String name;

	@Basic
	private String description;
}
Example 3. @Basic implied
@Entity(name = "Product")
public class Product {

	@Id
	private Integer id;

	private String sku;

	private String name;

	private String description;
}

The @Basic annotation defines 2 attributes.

optional - boolean (defaults to true)

Defines whether this attribute allows nulls. Jakarta Persistence defines this as "a hint", which means the provider is free to ignore it. Jakarta Persistence also says that it will be ignored if the type is primitive. As long as the type is not primitive, Hibernate will honor this value. Works in conjunction with @Column#nullable - see @Column.

fetch - FetchType (defaults to EAGER)

Defines whether this attribute should be fetched eagerly or lazily. EAGER indicates that the value will be fetched as part of loading the owner. LAZY values are fetched only when the value is accessed. Jakarta Persistence requires providers to support EAGER, while support for LAZY is optional meaning that a provider is free to not support it. Hibernate supports lazy loading of basic values as long as you are using its bytecode enhancement support.

3.2.2. @Column

Jakarta Persistence defines rules for implicitly determining the name of tables and columns. For a detailed discussion of implicit naming see Naming strategies.

For basic type attributes, the implicit naming rule is that the column name is the same as the attribute name. If that implicit naming rule does not meet your requirements, you can explicitly tell Hibernate (and other providers) the column name to use.

Example 4. Explicit column naming
@Entity(name = "Product")
public class Product {

	@Id
	private Integer id;

	private String sku;

	private String name;

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

Here we use @Column to explicitly map the description attribute to the NOTES column, as opposed to the implicit column name description. See Naming strategies for additional details.

The @Column annotation defines other mapping information as well. See its Javadocs for details.

3.2.3. @Formula

@Formula allows mapping any database computed value as a virtual read-only column.

  • The @Formula annotation takes a native SQL clause which may affect database portability.

  • @Formula is a Hibernate-specific mapping construct and not covered by Jakarta Persistence. Applications interested in portability should avoid its use.

Example 5. @Formula mapping usage
@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

}

When loading the Account entity, Hibernate is going to calculate the interest property using the configured @Formula:

Example 6. Persisting an entity with a @Formula mapping
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

The SQL fragment defined by the @Formula annotation can be as complex as you want, and it can even include sub-selects.

3.2.4. Mapping basic values

To deal with values of basic type, Hibernate needs to understand a few things about the mapping:

  • The capabilities of the Java type. For example:

    • How to compare values

    • How to calculate a hash-code

    • How to coerce values of this type to another type

  • The JDBC type it should use

    • How to bind values to JDBC statements

    • How to extract from JDBC results

  • Any conversion it should perform on the value to/from the database

  • The mutability of the value - whether the internal state can change like java.util.Date or is immutable like java.lang.String

This section covers how Hibernate determines these pieces and how to influence that determination process.

The following sections focus on approaches introduced in version 6 to influence how Hibernate will map basic value to the database.

This includes removal of the following deprecated legacy annotations:

  • @TypeDef

  • @TypeDefs

  • @CollectionId#type

  • @AnyMetaDef#metaType

  • @AnyMetaDef#idType

See the 6.0 migration guide for discussions about migrating uses of these annotations

The new annotations added as part of 6.0 support composing mappings in annotations through "meta-annotations".

Looking at this example, how does Hibernate know what mapping to use for these attributes? The annotations do not really provide much information.

This is an illustration of Hibernate’s implicit basic-type resolution, which is a series of checks to determine the appropriate mapping to use. Describing the complete process for implicit resolution is beyond the scope of this documentation[2].

This is primarily driven by the Java type defined for the basic type, which can generally be determined through reflection. Is the Java type an enum? Is it temporal? These answers can indicate certain mappings be used.

The fallback is to map the value to the "recommended" JDBC type.

Worst case, if the Java type is Serializable Hibernate will try to handle it via binary serialization.

For cases where the Java type is not a standard type or if some specialized handling is desired, Hibernate provides 2 main approaches to influence this mapping resolution:

  • A compositional approach using a combination of one-or-more annotations to describe specific aspects of the mapping. This approach is covered in Compositional basic mapping.

  • The UserType contract, which is covered in Custom type mapping

These 2 approaches should be considered mutually exclusive. A custom UserType will always take precedence over compositional annotations.

The next few sections look at common, standard Java types and discusses various ways to map them. See Case Study : BitSet for examples of mapping BitSet as a basic type using all of these approaches.

3.2.5. Enums

Hibernate supports the mapping of Java enums as basic value types in a number of different ways.

@Enumerated

The original Jakarta Persistence-compliant way to map enums was via the @Enumerated or @MapKeyEnumerated annotations, working on the principle that the enum values are stored according to one of 2 strategies indicated by jakarta.persistence.EnumType:

ORDINAL

stored according to the enum value’s ordinal position within the enum class, as indicated by java.lang.Enum#ordinal

STRING

stored according to the enum value’s name, as indicated by java.lang.Enum#name

Assuming the following enumeration:

Example 7. PhoneType enumeration
public enum PhoneType {
	LAND_LINE,
	MOBILE;
}

In the ORDINAL example, the phone_type column is defined as a (nullable) INTEGER type and would hold:

NULL

For null values

0

For the LAND_LINE enum

1

For the MOBILE enum

Example 8. @Enumerated(ORDINAL) example
@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

}

When persisting this entity, Hibernate generates the following SQL statement:

Example 9. Persisting an entity with an @Enumerated(ORDINAL) mapping
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', 1, 1)

In the STRING example, the phone_type column is defined as a (nullable) VARCHAR type and would hold:

NULL

For null values

LAND_LINE

For the LAND_LINE enum

MOBILE

For the MOBILE enum

Example 10. @Enumerated(STRING) example
@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

}

Persisting the same entity as in the @Enumerated(ORDINAL) example, Hibernate generates the following SQL statement:

Example 11. Persisting an entity with an @Enumerated(STRING) mapping
INSERT INTO Phone (phone_number, phone_type, id)
VALUES ('123-456-78990', 'MOBILE', 1)
Using AttributeConverter

Let’s consider the following Gender enum which stores its values using the 'M' and 'F' codes.

Example 12. Enum with a custom constructor
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;
    }
}

You can map enums in a Jakarta Persistence compliant way using a Jakarta Persistence AttributeConverter.

Example 13. Enum mapping with AttributeConverter example
@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);
	}
}

Here, the gender column is defined as a CHAR type and would hold:

NULL

For null values

'M'

For the MALE enum

'F'

For the FEMALE enum

For additional details on using AttributeConverters, see AttributeConverters section.

Jakarta Persistence explicitly disallows the use of an AttributeConverter with an attribute marked as @Enumerated.

So, when using the AttributeConverter approach, be sure not to mark the attribute as @Enumerated.

Custom type

You can also map enums using a Hibernate custom type mapping. Let’s again revisit the Gender enum example, this time using a custom Type to store the more standardized 'M' and 'F' codes.

Example 14. Enum mapping with custom Type example
@Entity(name = "Person")
public static class Person {

	@Id
	private Long id;

	private String name;

	@Type(GenderType.class)
	@Column(length = 6)
	public Gender gender;

	//Getters and setters are omitted for brevity

}

public class GenderType extends UserTypeSupport<Gender> {
    public GenderType() {
        super(Gender.class, Types.CHAR);
    }
}

public class GenderJavaType extends AbstractClassJavaType<Gender> {

    public static final GenderJavaType INSTANCE =
        new GenderJavaType();

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

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

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

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

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

Again, the gender column is defined as a CHAR type and would hold:

NULL

For null values

'M'

For the MALE enum

'F'

For the FEMALE enum

For additional details on using custom types, see Custom type mapping section.

3.2.6. Boolean

By default, Boolean attributes map to BOOLEAN columns, at least when the database has a dedicated BOOLEAN type. On databases which don’t, Hibernate uses whatever else is available: BIT, TINYINT, or SMALLINT.

Example 15. Implicit boolean mapping
// this will be mapped to BIT or BOOLEAN on the database
@Basic
boolean implicit;

However, it is quite common to find boolean values encoded as a character or as an integer. Such cases are exactly the intention of AttributeConverter. For convenience, Hibernate provides 3 built-in converters for the common boolean mapping cases:

  • YesNoConverter encodes a boolean value as 'Y' or 'N',

  • TrueFalseConverter encodes a boolean value as 'T' or 'F', and

  • NumericBooleanConverter encodes the value as an integer, 1 for true, and 0 for false.

Example 16. Using AttributeConverter
// this will get mapped to CHAR or NCHAR with a conversion
@Basic
@Convert(converter = org.hibernate.type.YesNoConverter.class)
boolean convertedYesNo;

// this will get mapped to CHAR or NCHAR with a conversion
@Basic
@Convert(converter = org.hibernate.type.TrueFalseConverter.class)
boolean convertedTrueFalse;

// this will get mapped to TINYINT with a conversion
@Basic
@Convert(converter = org.hibernate.type.NumericBooleanConverter.class)
boolean convertedNumeric;

If the boolean value is defined in the database as something other than BOOLEAN, character or integer, the value can also be mapped using a custom AttributeConverter - see AttributeConverters.

A UserType may also be used - see Custom type mapping

3.2.7. Byte

By default, Hibernate maps values of Byte / byte to the TINYINT JDBC type.

Example 17. Mapping Byte
// these will both be mapped using TINYINT
Byte wrapper;
byte primitive;

See Byte array for mapping arrays of bytes.

3.2.8. Short

By default, Hibernate maps values of Short / short to the SMALLINT JDBC type.

Example 18. Mapping Short
// these will both be mapped using SMALLINT
Short wrapper;
short primitive;

3.2.9. Integer

By default, Hibernate maps values of Integer / int to the INTEGER JDBC type.

Example 19. Mapping Integer
// these will both be mapped using INTEGER
Integer wrapper;
int primitive;

3.2.10. Long

By default, Hibernate maps values of Long / long to the BIGINT JDBC type.

Example 20. Mapping Long
// these will both be mapped using BIGINT
Long wrapper;
long primitive;

3.2.11. BigInteger

By default, Hibernate maps values of BigInteger to the NUMERIC JDBC type.

Example 21. Mapping BigInteger
// will be mapped using NUMERIC
BigInteger wrapper;

3.2.12. Double

By default, Hibernate maps values of Double to the DOUBLE, FLOAT, REAL or NUMERIC JDBC type depending on the capabilities of the database

Example 22. Mapping Double
// these will be mapped using DOUBLE, FLOAT, REAL or NUMERIC
// depending on the capabilities of the database
Double wrapper;
double primitive;

A specific type can be influenced using any of the JDBC type influencers covered in JdbcType section.

If @JdbcTypeCode is used, the Dialect is still consulted to make sure the database supports the requested type. If not, an appropriate type is selected

3.2.13. Float

By default, Hibernate maps values of Float to the FLOAT, REAL or NUMERIC JDBC type depending on the capabilities of the database.

Example 23. Mapping Float
// these will be mapped using FLOAT, REAL or NUMERIC
// depending on the capabilities of the database
Float wrapper;
float primitive;

A specific type can be influenced using any of the JDBC type influencers covered in Mapping basic values section.

If @JdbcTypeCode is used, the Dialect is still consulted to make sure the database supports the requested type. If not, an appropriate type is selected

3.2.14. BigDecimal

By default, Hibernate maps values of BigDecimal to the NUMERIC JDBC type.

Example 24. Mapping BigDecimal
// will be mapped using NUMERIC
BigDecimal wrapper;

3.2.15. Character

By default, Hibernate maps Character to the CHAR JDBC type.

Example 25. Mapping Character
// these will be mapped using CHAR
Character wrapper;
char primitive;

3.2.16. String

By default, Hibernate maps String to the VARCHAR JDBC type.

Example 26. Mapping String
// will be mapped using VARCHAR
String string;

// will be mapped using CLOB
@Lob
String clobString;

Optionally, you may specify the maximum length of the string using @Column(length=…​), or using the @Size annotation from Hibernate Validator. For very large strings, you can use one of the constant values defined by the class org.hibernate.Length, for example:

@Column(length=Length.LONG)
private String text;

Alternatively, you may explicitly specify the JDBC type LONGVARCHAR, which is treated as a VARCHAR mapping with default length=Length.LONG when no length is explicitly specified:

@JdbcTypeCode(Types.LONGVARCHAR)
private String text;

If you use Hibernate for schema generation, Hibernate will generate DDL with a column type that is large enough to accommodate the maximum length you’ve specified.

If the maximum length you specify is too long to fit in the largest VARCHAR column supported by your database, Hibernate’s schema exporter will automatically upgrade the column type to TEXT, CLOB, or whatever is the equivalent type for your database. Please don’t (ab)use JPA’s @Lob annotation just because you want a TEXT column. The purpose of the @Lob annotation is not to control DDL generation!

See Handling LOB data for details on mapping to a database CLOB.

For databases which support nationalized character sets, you can also store strings as nationalized data.

Example 27. Mapping String as nationalized
// will be mapped using NVARCHAR
@Nationalized
String nstring;

// will be mapped using NCLOB
@Lob
@Nationalized
String nclobString;

See Handling nationalized character data for details on mapping strings using nationalized character sets.

3.2.17. Character arrays

By default, Hibernate maps char[] to the VARCHAR JDBC type. Since Character[] can contain null elements, it is mapped as basic array type instead. Prior to Hibernate 6.2, also Character[] mapped to VARCHAR, yet disallowed null elements. To continue mapping Character[] to the VARCHAR JDBC type, or for LOBs mapping to the CLOB JDBC type, it is necessary to annotate the persistent attribute with @JavaType( CharacterArrayJavaType.class ).

Example 28. Mapping Character
// mapped as VARCHAR
char[] primitive;
Character[] wrapper;
@JavaType( CharacterArrayJavaType.class )
Character[] wrapperOld;

// mapped as CLOB
@Lob
char[] primitiveClob;
@Lob
Character[] wrapperClob;

See Handling LOB data for details on mapping as database LOB.

For databases which support nationalized character sets, you can also store character arrays as nationalized data.

Example 29. Mapping character arrays as nationalized
// mapped as NVARCHAR
@Nationalized
char[] primitiveNVarchar;
@Nationalized
Character[] wrapperNVarchar;
@Nationalized
@JavaType( CharacterArrayJavaType.class )
Character[] wrapperNVarcharOld;

// mapped as NCLOB
@Lob
@Nationalized
char[] primitiveNClob;
@Lob
@Nationalized
Character[] wrapperNClob;

See Handling nationalized character data for details on mapping strings using nationalized character sets.

3.2.18. Clob / NClob

Be sure to check out Handling LOB data which covers basics of LOB handling and Handling nationalized character data which covers basics of nationalized data handling.

By default, Hibernate will map the java.sql.Clob Java type to CLOB and java.sql.NClob to NCLOB.

Considering we have the following database table:

Example 30. CLOB - SQL
CREATE TABLE Product (
  id INTEGER NOT NULL,
  name VARCHAR(255),
  warranty CLOB,
  PRIMARY KEY (id)
)

Let’s first map this using the @Lob Jakarta Persistence annotation and the java.sql.Clob type:

Example 31. CLOB mapped to 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

}

To persist such an entity, you have to create a Clob using the ClobProxy Hibernate utility:

Example 32. Persisting a java.sql.Clob entity
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);

To retrieve the Clob content, you need to transform the underlying java.io.Reader:

Example 33. Returning a java.sql.Clob entity
Product product = entityManager.find(Product.class, productId);

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

We could also map the CLOB in a materialized form. This way, we can either use a String or a char[].

Example 34. CLOB mapped to 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

}

We might even want the materialized data as a char array.

Example 35. CLOB - materialized char[] mapping
@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

}

Just like with CLOB, Hibernate can also deal with NCLOB SQL data types:

Example 36. NCLOB - SQL
CREATE TABLE Product (
    id INTEGER NOT NULL ,
    name VARCHAR(255) ,
    warranty nclob ,
    PRIMARY KEY ( id )
)

Hibernate can map the NCLOB to a java.sql.NClob

Example 37. NCLOB mapped to 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

}

To persist such an entity, you have to create an NClob using the NClobProxy Hibernate utility:

Example 38. Persisting a java.sql.NClob entity
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);

To retrieve the NClob content, you need to transform the underlying java.io.Reader:

Example 39. Returning a java.sql.NClob entity
Product product = entityManager.find(Product.class, 1);

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

We could also map the NCLOB in a materialized form. This way, we can either use a String or a char[].

Example 40. NCLOB mapped to 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

}

We might even want the materialized data as a char array.

Example 41. NCLOB - materialized char[] mapping
@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

}

3.2.19. Byte array

By default, Hibernate maps byte[] to the VARBINARY JDBC type. Since Byte[] can contain null elements, it is mapped as basic array type instead. Prior to Hibernate 6.2, also Byte[] mapped to VARBINARY, yet disallowed null elements. To continue mapping Byte[] to the VARBINARY JDBC type, or for LOBs mapping to the BLOB JDBC type, it is necessary to annotate the persistent attribute with @JavaType( ByteArrayJavaType.class ).

Example 42. Mapping arrays of bytes
// mapped as VARBINARY
private byte[] primitive;
private Byte[] wrapper;
@JavaType( ByteArrayJavaType.class )
private Byte[] wrapperOld;

// mapped as (materialized) BLOB
@Lob
private byte[] primitiveLob;
@Lob
private Byte[] wrapperLob;

Just like with strings, you may specify the maximum length using @Column(length=…​) or the @Size annotation from Hibernate Validator. For very large arrays, you can use the constants defined by org.hibernate.Length. Alternatively @JdbcTypeCode(Types.LONGVARBINARY) is treated as a VARBINARY mapping with default length=Length.LONG when no length is explicitly specified.

If you use Hibernate for schema generation, Hibernate will generate DDL with a column type that is large enough to accommodate the maximum length you’ve specified.

If the maximum length you specify is too long to fit in the largest VARBINARY column supported by your database, Hibernate’s schema exporter will automatically upgrade the column type to IMAGE, BLOB, or whatever is the equivalent type for your database. Please don’t (ab)use JPA’s @Lob annotation for DDL customization.

See Handling LOB data for details on mapping to a database BLOB.

3.2.20. Blob

Be sure to check out Handling LOB data which covers basics of LOB handling.

By default, Hibernate will map the java.sql.Blob Java type to BLOB.

Considering we have the following database table:

Example 43. BLOB - SQL
CREATE TABLE Product (
    id INTEGER NOT NULL ,
    image blob ,
    name VARCHAR(255) ,
    PRIMARY KEY ( id )
)

Let’s first map this using the JDBC java.sql.Blob type.

Example 44. BLOB mapped to 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

}

To persist such an entity, you have to create a Blob using the BlobProxy Hibernate utility:

Example 45. Persisting a java.sql.Blob entity
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);

To retrieve the Blob content, you need to transform the underlying java.io.InputStream:

Example 46. Returning a java.sql.Blob entity
Product product = entityManager.find(Product.class, productId);

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

We could also map the BLOB in a materialized form (e.g. byte[]).

Example 47. BLOB mapped to 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

}

3.2.21. Duration

By default, Hibernate maps Duration to the NUMERIC SQL type.

It’s possible to map Duration to the INTERVAL_SECOND SQL type using @JdbcTypeCode(INTERVAL_SECOND) or by setting hibernate.type.preferred_duration_jdbc_type=INTERVAL_SECOND
Example 48. Mapping Duration
private Duration duration;

3.2.22. Instant

Instant is mapped to the TIMESTAMP_UTC SQL type.

Example 49. Mapping Instant
// mapped as TIMESTAMP
private Instant instant;

See Handling temporal data for basics of temporal mapping

3.2.23. LocalDate

LocalDate is mapped to the DATE JDBC type.

Example 50. Mapping LocalDate
// mapped as DATE
private LocalDate localDate;

See Handling temporal data for basics of temporal mapping

3.2.24. LocalDateTime

LocalDateTime is mapped to the TIMESTAMP JDBC type.

Example 51. Mapping LocalDateTime
// mapped as TIMESTAMP
private LocalDateTime localDateTime;

See Handling temporal data for basics of temporal mapping

3.2.25. LocalTime

LocalTime is mapped to the TIME JDBC type.

Example 52. Mapping LocalTime
// mapped as TIME
private LocalTime localTime;

See Handling temporal data for basics of temporal mapping

3.2.26. OffsetDateTime

OffsetDateTime is mapped to the TIMESTAMP or TIMESTAMP_WITH_TIMEZONE JDBC type depending on the database.

Example 53. Mapping OffsetDateTime
// mapped as TIMESTAMP or TIMESTAMP_WITH_TIMEZONE
private OffsetDateTime offsetDateTime;

See Handling temporal data for basics of temporal mapping See Using a specific time zone for basics of time-zone handling

3.2.27. OffsetTime

OffsetTime is mapped to the TIME or TIME_WITH_TIMEZONE JDBC type depending on the database.

Example 54. Mapping OffsetTime
// mapped as TIME or TIME_WITH_TIMEZONE
private OffsetTime offsetTime;

See Handling temporal data for basics of temporal mapping See Using a specific time zone for basics of time-zone handling

3.2.28. TimeZone

TimeZone is mapped to VARCHAR JDBC type.

Example 55. Mapping OffsetTime
// mapped as VARCHAR
private TimeZone timeZone;

3.2.29. ZonedDateTime

ZonedDateTime is mapped to the TIMESTAMP or TIMESTAMP_WITH_TIMEZONE JDBC type depending on the database.

Example 56. Mapping ZonedDateTime
// mapped as TIMESTAMP or TIMESTAMP_WITH_TIMEZONE
private ZonedDateTime zonedDateTime;

See Handling temporal data for basics of temporal mapping See Using a specific time zone for basics of time-zone handling

3.2.30. ZoneOffset

ZoneOffset is mapped to VARCHAR JDBC type.

Example 57. Mapping ZoneOffset
// mapped as VARCHAR
private ZoneOffset zoneOffset;

3.2.31. Calendar

See Handling temporal data for basics of temporal mapping See Using a specific time zone for basics of time-zone handling

3.2.32. Date

See Handling temporal data for basics of temporal mapping See Using a specific time zone for basics of time-zone handling

3.2.33. Time

See Handling temporal data for basics of temporal mapping See Using a specific time zone for basics of time-zone handling

3.2.34. Timestamp

See Handling temporal data for basics of temporal mapping See Using a specific time zone for basics of time-zone handling

3.2.35. Class

Hibernate maps Class references to VARCHAR JDBC type

Example 58. Mapping Class
// mapped as VARCHAR
private Class<?> clazz;

3.2.36. Currency

Hibernate maps Currency references to VARCHAR JDBC type

Example 59. Mapping Currency
// mapped as VARCHAR
private Currency currency;

3.2.37. Locale

Hibernate maps Locale references to VARCHAR JDBC type

Example 60. Mapping Locale
// mapped as VARCHAR
private Locale locale;

3.2.38. UUID

Hibernate allows mapping UUID values in a number of ways. By default, Hibernate will store UUID values in the native form by using the SQL type UUID or in binary form with the BINARY JDBC type if the database does not have a native UUID type.

The default uses the binary representation because it uses a more efficient column storage.

However, many applications prefer the readability of the character-based column storage.

To switch the default mapping, set the hibernate.type.preferred_uuid_jdbc_type configuration to CHAR.

UUID as binary

As mentioned, the default mapping for UUID attributes. Maps the UUID to a byte[] using java.util.UUID#getMostSignificantBits and java.util.UUID#getLeastSignificantBits and stores that as BINARY data.

Chosen as the default simply because it is generally more efficient from a storage perspective.

UUID as (var)char

Maps the UUID to a String using java.util.UUID#toString and java.util.UUID#fromString and stores that as CHAR or VARCHAR data.

UUID as identifier

Hibernate supports using UUID values as identifiers, and they can even be generated on the user’s behalf. For details, see the discussion of generators in Identifiers.

3.2.39. InetAddress

By default, Hibernate will map InetAddress to the INET SQL type and fallback to BINARY if necessary.

Example 61. Mapping InetAddress
private InetAddress address;

3.2.40. JSON mapping

Hibernate will only use the JSON type if explicitly configured through @JdbcTypeCode( SqlTypes.JSON ). The JSON library used for serialization/deserialization is detected automatically, but can be overridden by setting hibernate.type.json_format_mapper as can be read in the Configurations section.

Example 62. Mapping JSON
@JdbcTypeCode( SqlTypes.JSON )
private Map<String, String> stringMap;

3.2.41. XML mapping

Hibernate will only use the XML type if explicitly configured through @JdbcTypeCode( SqlTypes.SQLXML ). The XML library used for serialization/deserialization is detected automatically, but can be overridden by setting hibernate.type.xml_format_mapper as can be read in the Configurations section.

Example 63. Mapping XML
@JdbcTypeCode( SqlTypes.SQLXML )
private Map<String, StringNode> stringMap;

3.2.42. Basic array mapping

Basic arrays, other than byte[]/Byte[] and char[]/Character[], map to the type code SqlTypes.ARRAY by default, which maps to the SQL standard array type if possible, as determined via the new methods getArrayTypeName and supportsStandardArrays of org.hibernate.dialect.Dialect. If SQL standard array types are not available, data will be modeled as SqlTypes.JSON, SqlTypes.XML or SqlTypes.VARBINARY, depending on the database support as determined via the new method org.hibernate.dialect.Dialect.getPreferredSqlTypeCodeForArray.

Example 64. Mapping basic arrays
Short[] wrapper;
short[] primitive;

3.2.43. Basic collection mapping

Basic collections (only subtypes of Collection), which are not annotated with @ElementCollection, map to the type code SqlTypes.ARRAY by default, which maps to the SQL standard array type if possible, as determined via the new methods getArrayTypeName and supportsStandardArrays of org.hibernate.dialect.Dialect. If SQL standard array types are not available, data will be modeled as SqlTypes.JSON, SqlTypes.XML or SqlTypes.VARBINARY, depending on the database support as determined via the new method org.hibernate.dialect.Dialect.getPreferredSqlTypeCodeForArray.

Example 65. Mapping basic collections
List<Short> list;
SortedSet<Short> sortedSet;

3.2.44. Compositional basic mapping

The compositional approach allows defining how the mapping should work in terms of influencing individual parts that make up a basic-value mapping. This section will look at these individual parts and the specifics of influencing each.

JavaType

Hibernate needs to understand certain aspects of the Java type to handle values properly and efficiently. Hibernate understands these capabilities through its org.hibernate.type.descriptor.java.JavaType contract. Hibernate provides built-in support for many JDK types (Integer, String, e.g.), but also supports the ability for the application to change the handling for any of the standard JavaType registrations as well as add in handling for non-standard types. Hibernate provides multiple ways for the application to influence the JavaType descriptor to use.

The resolution can be influenced locally using the @JavaType annotation on a particular mapping. The indicated descriptor will be used just for that mapping. There are also forms of @JavaType for influencing the keys of a Map (@MapKeyJavaType), the index of a List or array (@ListIndexJavaType), the identifier of an ID-BAG mapping (@CollectionIdJavaType) as well as the discriminator (@AnyDiscriminator) and key (@AnyKeyJavaClass, @AnyKeyJavaType) of an ANY mapping.

The resolution can also be influenced globally by registering the appropriate JavaType descriptor with the JavaTypeRegistry. This approach is able to both "override" the handling for certain Java types or to register new types. See Registries for discussion of JavaTypeRegistry.

See Resolving the composition for a discussion of the process used to resolve the mapping composition.

JdbcType

Hibernate also needs to understand aspects of the JDBC type it should use (how it should bind values, how it should extract values, etc.) which is the role of its org.hibernate.type.descriptor.jdbc.JdbcType contract. Hibernate provides multiple ways for the application to influence the JdbcType descriptor to use.

Locally, the resolution can be influenced using either the @JdbcType or @JdbcTypeCode annotations. There are also annotations for influencing the JdbcType in relation to Map keys (@MapKeyJdbcType, @MapKeyJdbcTypeCode), the index of a List or array (@ListIndexJdbcType, @ListIndexJdbcTypeCode), the identifier of an ID-BAG mapping (@CollectionIdJdbcType, @CollectionIdJdbcTypeCode) as well as the key of an ANY mapping (@AnyKeyJdbcType, @AnyKeyJdbcTypeCode). The @JdbcType specifies a specific JdbcType implementation to use while @JdbcTypeCode specifies a "code" that is then resolved against the JdbcTypeRegistry.

The "type code" relative to a JdbcType generally maps to the corresponding value in java.sql.Types. registers entries in the JdbcTypeRegistry for all the standard java.sql.Types codes (aside from OTHER, which is special). See Registries for more discussion.

Customizing the JdbcTypeRegistry can be accomplished through @JdbcTypeRegistration and TypeContributor. See Registries for discussion of JavaTypeRegistry. See TypeContributor for discussion of TypeContributor.

See the @JdbcTypeCode Javadoc for details.

See Resolving the composition for a discussion of the process used to resolve the mapping composition.

MutabilityPlan

MutabilityPlan is the means by which Hibernate understands how to deal with the domain value in terms of its internal mutability as well as related concerns such as making copies. While it seems like a minor concern, it can have a major impact on performance. See AttributeConverter Mutability Plan for one case where this can manifest. See also Case Study : BitSet for another discussion.

The MutabilityPlan for a mapping can be influenced by any of the following annotations:

  • @Mutability

  • @Immutable

  • @MapKeyMutability

  • @CollectionIdMutability

Hibernate checks the following places for @Mutability and @Immutable, in order of precedence:

  1. Local to the mapping

  2. On the associated AttributeConverter implementation class (if one)

  3. On the value’s Java type

In most cases, the fallback defined by JavaType#getMutabilityPlan is the proper strategy.

Hibernate uses MutabilityPlan to:

  1. Check whether a value is considered dirty

  2. Make deep copies

  3. Marshal values to and from the second-level cache

Generally speaking, immutable values perform better in all of these cases

  1. To check for dirtiness, Hibernate just needs to check object identity (==) as opposed to equality (Object#equals).

  2. The same value instance can be used as the deep copy of itself.

  3. The same value can be used from the second-level cache as well as the value we put into the second-level cache.

If a particular Java type is considered mutable (a Date e.g.), @Immutable or a immutable-specific MutabilityPlan implementation can be specified to have Hibernate treat the value as immutable. This also acts as a contract from the application that the internal state of these objects is not changed by the application. Specifying that a mutable type is immutable and then changing the internal state will lead to problems; so only do this if the application unequivocally does not change the internal state.

See Resolving the composition for a discussion of the process used to resolve the mapping composition.

BasicValueConverter

BasicValueConverter is roughly analogous to AttributeConverter in that it describes a conversion to happen when reading or writing values of a basic-valued model part. In fact, internally Hibernate wraps an applied AttributeConverter in a BasicValueConverter. It also applies implicit BasicValueConverter converters in certain cases such as enum handling, etc.

Hibernate does not provide an explicit facility to influence these conversions beyond AttributeConverter. See AttributeConverters.

See Resolving the composition for a discussion of the process used to resolve the mapping composition.

Resolving the composition

Using this composition approach, Hibernate will need to resolve certain parts of this mapping. Often this involves "filling in the blanks" as it will be configured for just parts of the mapping. This section outlines how this resolution happens.

This is a complicated process and is only covered at a high level for the most common cases here.

For the full specifics, consult the source code for org.hibernate.mapping.BasicValue#buildResolution

First, we look for a custom type. If found, this takes predence. See Custom type mapping for details

If an AttributeConverter is applied, we use it as the basis for the resolution

  1. If @JavaType is also used, that specific JavaType is used for the converter’s "domain type". Otherwise, the Java type defined by the converter as its "domain type" is resolved against the JavaTypeRegistry

  2. If @JdbcType or @JdbcTypeCode is used, the indicated JdbcType is used and the converted "relational Java type" is determined by JdbcType#getJdbcRecommendedJavaTypeMapping. Otherwise, the Java type defined by the converter as its relational type is used and the JdbcType is determined by JdbcType#getRecommendedJdbcType

  3. The MutabilityPlan can be specified using @Mutability or @Immutable on the AttributeConverter implementation, the basic value mapping or the Java type used as the domain-type. Otherwise, JdbcType#getJdbcRecommendedJavaTypeMapping for the conversion’s domain-type is used to determine the mutability-plan.

Next we try to resolve the JavaType to use for the mapping. We check for an explicit @JavaType and use the specified JavaType if found. Next any "implicit" indication is checked; for example, the index for a List has the implicit Java type of Integer. Next, we use reflection if possible. If we are unable to determine the JavaType to use through the preceeding steps, we try to resolve an explicitly specified JdbcType to use and, if found, use its JdbcType#getJdbcRecommendedJavaTypeMapping as the mapping’s JavaType. If we are not able to determine the JavaType by this point, an error is thrown.

The JavaType resolved earlier is then inspected for a number of special cases.

  1. For enum values, we check for an explicit @Enumerated and create an enumeration mapping. Note that this resolution still uses any explicit JdbcType indicators

  2. For temporal values, we check for @Temporal and create an enumeration mapping. Note that this resolution still uses any explicit JdbcType indicators; this includes @JdbcType and @JdbcTypeCode, as well as @TimeZoneStorage and @TimeZoneColumn if appropriate.

The fallback at this point is to use the JavaType and JdbcType determined in earlier steps to create a JDBC-mapping (which encapsulates the JavaType and JdbcType) and combines it with the resolved MutabilityPlan

When using the compositional approach, there are other ways to influence the resolution as covered in Enums, Handling temporal data, Handling LOB data and Handling nationalized character data

See TypeContributor for an alternative to @JavaTypeRegistration and @JdbcTypeRegistration.

3.2.45. Custom type mapping

Another approach is to supply the implementation of the org.hibernate.usertype.UserType contract using @Type.

There are also corresponding, specialized forms of @Type for specific model parts:

  • When mapping a Map, @Type describes the Map value while @MapKeyType describe the Map key

  • When mapping an id-bag, @Type describes the elements while @CollectionIdType describes the collection-id

  • For other collection mappings, @Type describes the elements

  • For discriminated association mappings (@Any and @ManyToAny), @Type describes the discriminator value

@Type allows for more complex mapping concerns; but, AttributeConverter and Compositional basic mapping should generally be preferred as simpler solutions

3.2.46. Handling nationalized character data

How nationalized character data is handled and stored depends on the underlying database.

Most databases support storing nationalized character data through the standardized SQL NCHAR, NVARCHAR, LONGNVARCHAR and NCLOB variants.

Others support storing nationalized data as part of CHAR, VARCHAR, LONGVARCHAR and CLOB. Generally these databases do not support NCHAR, NVARCHAR, LONGNVARCHAR and NCLOB, even as aliased types.

Ultimately Hibernate understands this through Dialect#getNationalizationSupport()

To ensure nationalized character data gets stored and accessed correctly, @Nationalized can be used locally or hibernate.use_nationalized_character_data can be set globally.

@Nationalized and hibernate.use_nationalized_character_data can be used regardless of the specific database support for nationalized data and allows the application to work portably across databases with varying support.

For databases with no NCLOB data type, attributes of type java.sql.NClob are simply unsupported. Use java.sql.Clob (which NClob extends) or a materialized mapping like String or char[] instead.

See also Handling LOB data regarding similar limitation for databases which do not support explicit CLOB data-type.

Considering we have the following database table:

Example 66. NVARCHAR - SQL
CREATE TABLE Product (
    id INTEGER NOT NULL ,
    name VARCHAR(255) ,
    warranty NVARCHAR(255) ,
    PRIMARY KEY ( id )
)

To map a specific attribute to a nationalized variant data type, Hibernate defines the @Nationalized annotation.

Example 67. NVARCHAR mapping
@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

}

3.2.47. Handling LOB data

The @Lob annotation specifies that character or binary data should be written to the database using the special JDBC APIs for handling database LOB (Large OBject) types.

How JDBC deals with LOB data varies from driver to driver. Hibernate tries to take care of all these differences, and protect you as much as possible from inconsistent driver behavior. Sadly, Hibernate is only partially successful at achieving this goal.

Some database drivers (i.e. PostgreSQL) are especially problematic and in such cases you might have to do some extra work to get LOBs functioning. But that’s beyond the scope of this guide.

For databases with no CLOB type, attributes of type java.sql.Clob are simply unsupported. Use a materialized type like String or char[] instead.

There’s two ways a LOB may be represented in the Java domain model:

  • using a special JDBC-defined LOB locator type, or

  • using a regular "materialized" type like String, char[], or byte[].

LOB Locator

The JDBC LOB locator types are:

  • java.sql.Blob

  • java.sql.Clob

  • java.sql.NClob

These types represent references to off-table LOB data. In principle, they allow JDBC drivers to support more efficient access to the LOB data. Some drivers stream parts of the LOB data as needed, potentially consuming less memory.

However, java.sql.Blob and java.sql.Clob can be unnatural to deal with and suffer certain limitations. For example, it’s not portable to access a LOB locator after the end of the transaction in which it was obtained.

Materialized LOB

Alternatively, Hibernate lets you access LOB data via the familiar Java types String, char[], and byte[]. But of course this requires materializing the entire contents of the LOB in memory when the object is first retrieved. Whether this performance cost is acceptable depends on many factors, including the vagaries of the JDBC driver.

You don’t need to use a @Lob mapping for every database column of type BLOB or CLOB. The @Lob annotation is a special-purpose tool that should only be used when a default basic mapping to String would result in unacceptable performance characteristics.

3.2.48. Handling temporal data

Hibernate supports mapping temporal values in numerous ways, though ultimately these strategies boil down to the 3 main Date/Time types defined by the SQL specification:

DATE

Represents a calendar date by storing years, months and days.

TIME

Represents the time of a day by storing hours, minutes and seconds.

TIMESTAMP

Represents both a DATE and a TIME plus nanoseconds.

TIMESTAMP WITH TIME ZONE

Represents both a DATE and a TIME plus nanoseconds and zone id or offset.

The mapping of java.time temporal types to the specific SQL Date/Time types is implied as follows:

DATE

java.time.LocalDate

TIME

java.time.LocalTime, java.time.OffsetTime

TIMESTAMP

java.time.Instant, java.time.LocalDateTime, java.time.OffsetDateTime and java.time.ZonedDateTime

TIMESTAMP WITH TIME ZONE

java.time.OffsetDateTime, java.time.ZonedDateTime

Although Hibernate recommends the use of the java.time package for representing temporal values, it does support using java.sql.Date, java.sql.Time, java.sql.Timestamp, java.util.Date and java.util.Calendar.

The mappings for java.sql.Date, java.sql.Time, java.sql.Timestamp are implicit:

DATE

java.sql.Date

TIME

java.sql.Time

TIMESTAMP

java.sql.Timestamp

Applying @Temporal to java.sql.Date, java.sql.Time, java.sql.Timestamp or any of the java.time types is considered an exception

When using java.util.Date or java.util.Calendar, Hibernate assumes TIMESTAMP. To alter that, use @Temporal.

Example 68. Mapping java.util.Date
// mapped as TIMESTAMP by default
Date dateAsTimestamp;

// explicitly mapped as DATE
@Temporal(TemporalType.DATE)
Date dateAsDate;

// explicitly mapped as TIME
@Temporal(TemporalType.TIME)
Date dateAsTime;
Using a specific time zone

By default, Hibernate is going to use the PreparedStatement.setTimestamp(int parameterIndex, java.sql.Timestamp) or PreparedStatement.setTime(int parameterIndex, java.sql.Time x) when saving a java.sql.Timestamp or a java.sql.Time property.

When the time zone is not specified, the JDBC driver is going to use the underlying JVM default time zone, which might not be suitable if the application is used from all across the globe. For this reason, it is very common to use a single reference time zone (e.g. UTC) whenever saving/loading data from the database.

One alternative would be to configure all JVMs to use the reference time zone:

Declaratively
java -Duser.timezone=UTC ...
Programmatically
TimeZone.setDefault( TimeZone.getTimeZone( "UTC" ) );

However, as explained in this article, this is not always practical, especially for front-end nodes. For this reason, Hibernate offers the hibernate.jdbc.time_zone configuration property which can be configured:

Declaratively, at the SessionFactory level
settings.put(
    AvailableSettings.JDBC_TIME_ZONE,
    TimeZone.getTimeZone( "UTC" )
);
Programmatically, on a per Session basis
Session session = sessionFactory()
    .withOptions()
    .jdbcTimeZone( TimeZone.getTimeZone( "UTC" ) )
    .openSession();

With this configuration property in place, Hibernate is going to call the PreparedStatement.setTimestamp(int parameterIndex, java.sql.Timestamp, Calendar cal) or PreparedStatement.setTime(int parameterIndex, java.sql.Time x, Calendar cal), where the java.util.Calendar references the time zone provided via the hibernate.jdbc.time_zone property.

Handling time zoned temporal data

By default, Hibernate will convert and normalize OffsetDateTime and ZonedDateTime to java.sql.Timestamp in UTC. This behavior can be altered by configuring the hibernate.timezone.default_storage property

settings.put(
    AvailableSettings.TIMEZONE_DEFAULT_STORAGE,
    TimeZoneStorageType.AUTO
);

Other possible storage types are AUTO, COLUMN, NATIVE and NORMALIZE (the default). With COLUMN, Hibernate will save the time zone information into a dedicated column, whereas NATIVE will require the support of database for a TIMESTAMP WITH TIME ZONE data type that retains the time zone information. NORMALIZE doesn’t store time zone information and will simply convert the timestamp to UTC. Hibernate understands what a database/dialect supports through Dialect#getTimeZoneSupport and will abort with a boot error if the NATIVE is used in conjunction with a database that doesn’t support this. For AUTO, Hibernate tries to use NATIVE if possible and falls back to COLUMN otherwise.

3.2.49. @TimeZoneStorage

Hibernate supports defining the storage to use for time zone information for individual properties via the @TimeZoneStorage and @TimeZoneColumn annotations. The storage type can be specified via the @TimeZoneStorage by specifying a org.hibernate.annotations.TimeZoneStorageType. The default storage type is AUTO which will ensure that the time zone information is retained. The @TimeZoneColumn annotation can be used in conjunction with AUTO or COLUMN and allows to define the column details for the time zone information storage.

Storing the zone offset might be problematic for future timestamps as zone rules can change. Due to this, storing the offset is only safe for past timestamps, and we advise sticking to the NORMALIZE strategy by default.

Example 69. @TimeZoneColumn usage
@TimeZoneStorage(TimeZoneStorageType.COLUMN)
@TimeZoneColumn(name = "birthtime_offset_offset")
@Column(name = "birthtime_offset")
private OffsetTime offsetTimeColumn;

@TimeZoneStorage(TimeZoneStorageType.COLUMN)
@TimeZoneColumn(name = "birthday_offset_offset")
@Column(name = "birthday_offset")
private OffsetDateTime offsetDateTimeColumn;

@TimeZoneStorage(TimeZoneStorageType.COLUMN)
@TimeZoneColumn(name = "birthday_zoned_offset")
@Column(name = "birthday_zoned")
private ZonedDateTime zonedDateTimeColumn;

3.2.50. AttributeConverters

With a custom AttributeConverter, the application developer can map a given JDBC type to an entity basic type.

In the following example, the java.time.Period is going to be mapped to a VARCHAR database column.

Example 70. java.time.Period custom 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);
    }
}

To make use of this custom converter, the @Convert annotation must decorate the entity attribute.

Example 71. Entity using the custom java.time.Period AttributeConverter mapping
@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

}

When persisting such entity, Hibernate will do the type conversion based on the AttributeConverter logic:

Example 72. Persisting entity using the custom AttributeConverter
INSERT INTO Event ( span, id )
VALUES ( 'P1Y2M3D', 1 )

An AttributeConverter can be applied globally for (@Converter( autoApply=true )) or locally.

AttributeConverter Java and JDBC types

In cases when the Java type specified for the "database side" of the conversion (the second AttributeConverter bind parameter) is not known, Hibernate will fallback to a java.io.Serializable type.

If the Java type is not known to Hibernate, you will encounter the following message:

HHH000481: Encountered Java type for which we could not locate a JavaType and which does not appear to implement equals and/or hashCode. This can lead to significant performance problems when performing equality/dirty checking involving this Java type. Consider registering a custom JavaType or at least implementing equals/hashCode.

A Java type is "known" if it has an entry in the JavaTypeRegistry. While Hibernate does load many JDK types into the JavaTypeRegistry, an application can also expand the JavaTypeRegistry by adding new JavaType entries as discussed in Compositional basic mapping and TypeContributor.

Mapping an AttributeConverter using HBM mappings

When using HBM mappings, you can still make use of the Jakarta Persistence AttributeConverter because Hibernate supports such mapping via the type attribute as demonstrated by the following example.

Let’s consider we have an application-specific Money type:

Example 73. Application-specific Money type
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;
    }
}

Now, we want to use the Money type when mapping the Account entity:

Example 74. Account entity using the Money type
public class Account {

    private Long id;

    private String owner;

    private Money balance;

    //Getters and setters are omitted for brevity
}

Since Hibernate has no knowledge how to persist the Money type, we could use a Jakarta Persistence AttributeConverter to transform the Money type as a Long. For this purpose, we are going to use the following MoneyConverter utility:

Example 75. MoneyConverter implementing the Jakarta Persistence AttributeConverter interface
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);
    }
}

To map the MoneyConverter using HBM configuration files you need to use the converted:: prefix in the type attribute of the property element.

Example 76. HBM mapping for AttributeConverter
<?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.orm.test.mapping.converter.hbm">
    <class name="org.hibernate.orm.test.mapping.converter.hbm.Account" table="account" >
        <id name="id"/>

        <property name="owner"/>

        <property name="balance"
            type="converted::org.hibernate.orm.test.mapping.converter.hbm.MoneyConverter"/>
        
    </class>
</hibernate-mapping>
AttributeConverter Mutability Plan

A basic type that’s converted by a Jakarta Persistence AttributeConverter is immutable if the underlying Java type is immutable and is mutable if the associated attribute type is mutable as well.

Therefore, mutability is given by the JavaType#getMutabilityPlan of the associated entity attribute type.

This can be adjusted by using @Immutable or @Mutability on any of:

  1. the basic value

  2. the AttributeConverter class

  3. the basic value type

See Mapping basic values for additional details.

Immutable types

If the entity attribute is a String, a primitive wrapper (e.g. Integer, Long), an Enum type, or any other immutable Object type, then you can only change the entity attribute value by reassigning it to a new value.

Considering we have the same Period entity attribute as illustrated in the AttributeConverters section:

@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

}

The only way to change the span attribute is to reassign it to a different value:

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

On the other hand, consider the following example where the Money type is a mutable.

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);
	}
}

A mutable Object allows you to modify its internal structure, and Hibernate’s dirty checking mechanism is going to propagate the change to the database:

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

Although the AttributeConverter types can be mutable so that dirty checking, deep copying, and second-level caching work properly, treating these as immutable (when they really are) is more efficient.

For this reason, prefer immutable types over mutable ones whenever possible.

Using the AttributeConverter entity property as a query parameter

Assuming you have the following entity:

Example 77. Photo entity with AttributeConverter
@Entity(name = "Photo")
public static class Photo {

	@Id
	private Integer id;

	@Column(length = 256)
	private String name;

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

	//Getters and setters are omitted for brevity
}

And the Caption class looks as follows:

Example 78. Caption Java object
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;
	}
}

And we have an AttributeConverter to handle the Caption Java object:

Example 79. Caption Java object 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 );
	}
}

Traditionally, you could only use the DB data Caption representation, which in our case is a String, when referencing the caption entity property.

Example 80. Filtering by the Caption property using the DB data representation
Photo photo = entityManager.createQuery(
				"select p " +
						"from Photo p " +
						"where upper(caption) = upper(:caption) ", Photo.class )
		.setParameter( "caption", "Nicolae Grigorescu" )
		.getSingleResult();

In order to use the Java object Caption representation, you have to get the associated Hibernate Type.

Example 81. Filtering by the Caption property using the Java Object representation
SessionFactoryImplementor sessionFactory = entityManager.getEntityManagerFactory()
		.unwrap( SessionFactoryImplementor.class );
final MappingMetamodelImplementor mappingMetamodel = sessionFactory
		.getRuntimeMetamodels()
		.getMappingMetamodel();

Type captionType = mappingMetamodel
		.getEntityDescriptor( Photo.class )
		.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" ),
				(BindableType) captionType
		)
		.getSingleResult();

By passing the associated Hibernate Type, you can use the Caption object when binding the query parameter value.

3.2.51. Registries

We’ve covered JavaTypeRegistry and JdbcTypeRegistry a few times now, mainly in regards to mapping resolution as discussed in Resolving the composition. But they each also serve additional important roles.

The JavaTypeRegistry is a registry of JavaType references keyed by Java type. In addition to mapping resolution, this registry is used to handle Class references exposed in various APIs such as Query parameter types. JavaType references can be registered through @JavaTypeRegistration.

The JdbcTypeRegistry is a registry of JdbcType references keyed by an integer code. As discussed in JdbcType, these type-codes typically match with the corresponding code from java.sql.Types, but that is not a requirement - integers other than those defined by java.sql.Types can be used. This might be useful for mapping JDBC User Data Types (UDTs) or other specialized database-specific types (PostgreSQL’s UUID type, e.g.). In addition to its use in mapping resolution, this registry is also used as the primary source for resolving "discovered" values in a JDBC ResultSet. JdbcType references can be registered through @JdbcTypeRegistration.

See TypeContributor for an alternative to @JavaTypeRegistration and @JdbcTypeRegistration for registration.

3.2.52. TypeContributor

org.hibernate.boot.model.TypeContributor is a contract for overriding or extending parts of the Hibernate type system.

There are many ways to integrate a TypeContributor. The most common is to define the TypeContributor as a Java service (see java.util.ServiceLoader).

TypeContributor is passed a TypeContributions reference, which allows registration of custom JavaType, JdbcType and BasicType references.

While TypeContributor still exposes the ability to register BasicType references, this is considered deprecated. As of 6.0, these BasicType registrations are only used while interpreting hbm.xml mappings, which are themselves considered deprecated. Use Custom type mapping or Compositional basic mapping instead.

3.2.53. Case Study : BitSet

We’ve covered many ways to specify basic value mappings so far. This section will look at mapping the java.util.BitSet type by applying the different techniques covered so far.

Example 82. Implicit BitSet mapping
@Entity(name = "Product")
public static class Product {
	@Id
	private Integer id;

	private BitSet bitSet;

	//Getters and setters are omitted for brevity
}

As mentioned previously, the worst-case fallback for Hibernate mapping a basic type which implements Serializable is to simply serialize it to the database. BitSet does implement Serializable, so by default Hibernate would handle this mapping by serialization.

That is not an ideal mapping. In the following sections we will look at approaches to change various aspects of how the BitSet gets mapped to the database.

Using AttributeConverter

We’ve seen uses of AttributeConverter previously.

This works well in most cases and is portable across Jakarta Persistence providers.

Example 83. BitSet AttributeConverter
@Entity(name = "Product")
public static class Product {
	@Id
	private Integer id;

	@Convert(converter = BitSetConverter.class)
	private BitSet bitSet;

	//Getters and setters are omitted for brevity
}

@Converter(autoApply = true)
public static class BitSetConverter implements AttributeConverter<BitSet,String> {
	@Override
	public String convertToDatabaseColumn(BitSet attribute) {
		return BitSetHelper.bitSetToString(attribute);
	}

	@Override
	public BitSet convertToEntityAttribute(String dbData) {
		return BitSetHelper.stringToBitSet(dbData);
	}
}

The @Convert annotation was used for illustration. Generally such a converter would be auto-applied instead

See AttributeConverters for details.

This greatly improves the reading and writing performance of dealing with these BitSet values because the AttributeConverter does that more efficiently using a simple externalizable form of the BitSet rather than serializing and deserializing the values.

Using a custom JavaTypeDescriptor

As covered in [basic-mapping-explicit], we will define a JavaType for BitSet that maps values to VARCHAR for storage by default.

Example 84. BitSet JavaTypeDescriptor
public class BitSetJavaType extends AbstractClassJavaType<BitSet> {
    public static final BitSetJavaType INSTANCE = new BitSetJavaType();

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

    @Override
    public MutabilityPlan<BitSet> getMutabilityPlan() {
        return BitSetMutabilityPlan.INSTANCE;
    }

    @Override
    public JdbcType getRecommendedJdbcType(JdbcTypeIndicators indicators) {
        return indicators.getTypeConfiguration()
                .getJdbcTypeRegistry()
                .getDescriptor(Types.VARCHAR);
    }

    @Override
    public String toString(BitSet value) {
        return BitSetHelper.bitSetToString(value);
    }

    @Override
    public BitSet fromString(CharSequence string) {
        return BitSetHelper.stringToBitSet(string.toString());
    }

    @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);
        }
        if (type.isArray()) {
            if (type.getComponentType() == byte.class) {
                return (X) value.toByteArray();
            }
        }
        throw unknownUnwrap(type);
    }

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

}

We can either apply that type locally using @JavaType

Example 85. @JavaType
@Entity(name = "Product")
public static class Product {
	@Id
	private Integer id;

	@JavaType(BitSetJavaType.class)
	private BitSet bitSet;

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

Or we can apply it globally using @JavaTypeRegistration. This allows the registered JavaType to be used as the default whenever we encounter the BitSet type

Example 86. @JavaTypeRegistration
@Entity(name = "Product")
@JavaTypeRegistration(javaType = BitSet.class, descriptorClass = BitSetJavaType.class)
public static class Product {
	@Id
	private Integer id;

	private BitSet bitSet;

	//Constructors, getters, and setters are omitted for brevity
}
Selecting different JdbcTypeDescriptor

Our custom BitSetJavaType maps BitSet values to VARCHAR by default. That was a better option than direct serialization. But as BitSet is ultimately binary data we would probably really want to map this to VARBINARY type instead. One way to do that would be to change BitSetJavaType#getRecommendedJdbcType to instead return VARBINARY descriptor. Another option would be to use a local @JdbcType or @JdbcTypeCode.

The following examples for specifying the JdbcType assume our BitSetJavaType is globally registered.

We will again store the values as VARBINARY in the database. The difference now however is that the coercion methods #wrap and #unwrap will be used to prepare the value rather than relying on serialization.

Example 87. @JdbcTypeCode
@Entity(name = "Product")
public static class Product {
	@Id
	private Integer id;

	@JdbcTypeCode(Types.VARBINARY)
	private BitSet bitSet;

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

In this example, @JdbcTypeCode has been used to indicate that the JdbcType registered for JDBC’s VARBINARY type should be used.

Example 88. @JdbcType
@Entity(name = "Product")
public static class Product {
	@Id
	private Integer id;

	@JdbcType(CustomBinaryJdbcType.class)
	private BitSet bitSet;

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

In this example, @JdbcType has been used to specify our custom BitSetJdbcType descriptor locally for this attribute.

We could instead replace how Hibernate deals with all VARBINARY handling with our custom impl using @JdbcTypeRegistration

Example 89. @JdbcType
@Entity(name = "Product")
@JdbcTypeRegistration(CustomBinaryJdbcType.class)
public static class Product {
	@Id
	private Integer id;

	private BitSet bitSet;

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

3.2.54. SQL quoted identifiers

You can force Hibernate to quote an identifier in the generated SQL by enclosing the table or column name in backticks in the mapping document. While traditionally, Hibernate used backticks for escaping SQL reserved keywords, Jakarta Persistence uses double quotes instead.

Once the reserved keywords are escaped, Hibernate will use the correct quotation style for the SQL Dialect. This is usually double quotes, but SQL Server uses brackets and MySQL uses backticks.

Example 90. Hibernate quoting
@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

}
Example 91. Jakarta Persistence quoting
@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

}

Because name and number are reserved words, the Product entity mapping uses backticks to quote these column names.

When saving the following Product entity, Hibernate generates the following SQL insert statement:

Example 92. Persisting a quoted column name
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 can also quote all identifiers (e.g. table, columns) using the following configuration property:

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

This way, we don’t need to manually quote any identifier:

Example 93. Jakarta Persistence quoting
@Entity(name = "Product")
public static class Product {

	@Id
	private Long id;

	private String name;

	private String number;

	//Getters and setters are omitted for brevity

}

When persisting a Product entity, Hibernate is going to quote all identifiers as in the following example:

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

As you can see, both the table name and all the column have been quoted.

For more about quoting-related configuration properties, check out the Mapping configurations section as well.

3.2.55. Generated properties

NOTE

This section talks about generating values for non-identifier attributes. For discussion of generated identifier values, see Generated identifier values.

Generated attributes have their values generated as part of performing a SQL INSERT or UPDATE. Applications can generate these values in any number of ways (SQL DEFAULT value, trigger, etc). Typically, the application needs to refresh objects that contain any properties for which the database was generating values, which is a major drawback.

Applications can also delegate generation to Hibernate, in which case Hibernate will manage the value generation and (potential[3]) state refresh itself.

Only @Basic and @Version attributes can be marked as generated.

Generated attributes must additionally be non-insertable and non-updateable.

Hibernate supports both in-VM and in-DB generation. A generation that uses the current JVM timestamp as the generated value is an example of an in-VM strategy. A generation that uses the database’s current_timestamp function is an example of an in-DB strategy.

Hibernate supports the following timing (when) for generation:

NEVER (the default)

the given attribute value is not generated

INSERT

the attribute value is generated on insert but is not regenerated on subsequent updates

ALWAYS

the attribute value is generated both on insert and update.

Hibernate supports multiple ways to mark an attribute as generated:

@CurrentTimestamp

The @CurrentTimestamp annotation is an in-DB strategy that can be configured for either INSERT or ALWAYS timing. It uses the database’s current_timestamp function as the generated value

Example 94. @UpdateTimestamp mapping example
@CurrentTimestamp(event = INSERT)
public Instant createdAt;

@CurrentTimestamp(event = {INSERT, UPDATE})
public Instant lastUpdatedAt;
@CreationTimestamp

The @CreationTimestamp annotation is an in-VM INSERT strategy. Hibernate will use the current timestamp of the JVM as the insert value for the attribute.

Supports most temporal types (java.time.Instant, java.util.Date, java.util.Calendar, etc)

Example 95. @CreationTimestamp mapping example
@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
}

While inserting the Event, Hibernate will populate the underlying timestamp column with the current JVM timestamp value

@UpdateTimestamp annotation

The @UpdateTimestamp annotation is an in-VM INSERT strategy. Hibernate will use the current timestamp of the JVM as the insert and update value for the attribute.

Supports most temporal types (java.time.Instant, java.util.Date, java.util.Calendar, etc)

Example 96. @UpdateTimestamp mapping example
@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

}
@Generated annotation

The @Generated annotation is an in-DB strategy that can be configured for either INSERT or ALWAYS timing

This is the legacy mapping for in-DB generated values.

Example 97. @Generated mapping example
@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(event = {INSERT,UPDATE})
	@Column(columnDefinition =
		"AS CONCAT(" +
		"	COALESCE(firstName, ''), " +
		"	COALESCE(' ' + middleName1, ''), " +
		"	COALESCE(' ' + middleName2, ''), " +
		"	COALESCE(' ' + middleName3, ''), " +
		"	COALESCE(' ' + middleName4, ''), " +
		"	COALESCE(' ' + middleName5, ''), " +
		"	COALESCE(' ' + lastName, '') " +
		")")
	private String fullName;

}
Custom generation strategy

Hibernate also supports value generation via a pluggable API using @ValueGenerationType and AnnotationValueGeneration allowing users to define any generation strategy they wish.

Let’s look at an example of generating UUID values. First the attribute mapping

Example 98. Custom generation mapping example
@GeneratedUuidValue( timing = GenerationTiming.INSERT )
public UUID createdUuid;

@GeneratedUuidValue( timing = GenerationTiming.ALWAYS )
   public UUID updatedUuid;

This example makes use of an annotation named @GeneratedUuidValue - but where is that annotation defined? This is a custom annotations provided by the application.

Example 99. Custom generation mapping example
@ValueGenerationType( generatedBy = UuidValueGeneration.class )
@Retention(RetentionPolicy.RUNTIME)
@Target( { ElementType.FIELD, ElementType.METHOD, ElementType.ANNOTATION_TYPE } )
@Inherited
public @interface GeneratedUuidValue {
	GenerationTiming timing();
}

The @ValueGenerationType( generatedBy = UuidValueGeneration.class ) here is the important piece; it tells Hibernate how to generate values for the attribute - here it will use the specified UuidValueGeneration class

Example 100. Custom generation mapping example
public static class UuidValueGeneration implements BeforeExecutionGenerator {
	private final EnumSet<EventType> eventTypes;

	public UuidValueGeneration(GeneratedUuidValue annotation) {
		eventTypes = annotation.timing().getEquivalent().eventTypes();
	}

	@Override
	public EnumSet<EventType> getEventTypes() {
		return eventTypes;
	}

	@Override
	public Object generate(SharedSessionContractImplementor session, Object owner, Object currentValue, EventType eventType) {
		return SafeRandomUUIDGenerator.safeRandomUUID();
	}
}

See @ValueGenerationType and AnnotationValueGeneration for details of each contract

3.2.56. Column transformers: read and write expressions

Hibernate allows you to customize the SQL it uses to read and write the values of columns mapped to @Basic types. For example, if your database provides a set of data encryption functions, you can invoke them for individual columns like in the following example.

Example 101. @ColumnTransformer example
	@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', ?)"
		)
// For H2 2.0.202+ one must use the varbinary DDL type
//		@Column(name = "pswd", columnDefinition = "varbinary")
//		@ColumnTransformer(
//			read = "trim(trailing u&'\\0000' from cast(decrypt('AES', '00', pswd ) as character varying))",
//			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
	}

If a property uses more than one column, you must use the forColumn attribute to specify which column the @ColumnTransformer read and write expressions are targeting.

Example 102. @ColumnTransformer forColumn attribute usage
@Entity(name = "Savings")
public static class Savings {

	@Id
	private Long id;

	@CompositeType(MonetaryAmountUserType.class)
	@AttributeOverrides({
		@AttributeOverride(name = "amount", column = @Column(name = "money")),
		@AttributeOverride(name = "currency", column = @Column(name = "currency"))
	})
	@ColumnTransformer(
			forColumn = "money",
			read = "money / 100",
			write = "? * 100"
	)
	private MonetaryAmount wallet;

	//Getters and setters omitted for brevity

}

Hibernate applies the custom expressions automatically whenever the property is referenced in a query. This functionality is similar to a derived-property @Formula with two differences:

  • The property is backed by one or more columns that are exported as part of automatic schema generation.

  • The property is read-write, not read-only.

The write expression, if specified, must contain exactly one '?' placeholder for the value.

Example 103. Persisting an entity with a @ColumnTransformer and a composite type
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());
	assertEquals(Currency.getInstance(Locale.US), savings.getWallet().getCurrency());
});
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

3.3. Embeddable values

Historically Hibernate called these components. Jakarta Persistence calls them embeddables. Either way, the concept is the same: a composition of values.

For example, we might have a Publisher class that is a composition of name and country, or a Location class that is a composition of country and city.

Usage of the word embeddable

To avoid any confusion with the annotation that marks a given embeddable type, the annotation will be further referred to as @Embeddable.

Throughout this chapter and thereafter, for brevity sake, embeddable types may also be referred to as embeddable.

Example 104. Embeddable type example
@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
}

An embeddable type is another form of a value type, and its lifecycle is bound to a parent entity type, therefore inheriting the attribute access from its parent (for details on attribute access, see Access strategies).

Embeddable types can be made up of basic values as well as associations, with the caveat that, when used as collection elements, they cannot define collections themselves.

3.3.1. Component / Embedded

Most often, embeddable types are used to group multiple basic type mappings and reuse them across several entities.

Example 105. Simple Embeddable
@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)
)

Jakarta Persistence defines two terms for working with an embeddable type: @Embeddable and @Embedded.

@Embeddable is used to describe the mapping type itself (e.g. Publisher).

@Embedded is for referencing a given embeddable type (e.g. book.publisher).

So, the embeddable type is represented by the Publisher class and the parent entity makes use of it through the book#publisher object composition.

The composed values are mapped to the same table as the parent table. Composition is part of good object-oriented data modeling (idiomatic Java). In fact, that table could also be mapped by the following entity type instead.

Example 106. Alternative to embeddable type composition
@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
}

The composition form is certainly more object-oriented, and that becomes more evident as we work with multiple embeddable types.

3.3.2. Overriding Embeddable types

Although from an object-oriented perspective, it’s much more convenient to work with embeddable types, when we reuse the same embeddable multiple times on the same class, the Jakarta Persistence specification requires to set the associated column names explicitly.

This requirement is due to how object properties are mapped to database columns. By default, Jakarta Persistence expects a database column having the same name with its associated object property. When including multiple embeddables, the implicit name-based mapping rule doesn’t work anymore because multiple object properties could end-up being mapped to the same database column.

When an embeddable type is used multiple times, Jakarta Persistence defines the @AttributeOverride and @AssociationOverride annotations to handle this scenario to override the default column names defined by the Embeddable.

See Embeddables and ImplicitNamingStrategy for an alternative to using @AttributeOverride and @AssociationOverride

Considering you have the following Publisher embeddable type which defines a @ManyToOne association with the Country entity:

Example 107. Embeddable type with a @ManyToOne association
@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)

Now, if you have a Book entity which declares two Publisher embeddable types for the ebook and paperback versions, you cannot use the default Publisher embeddable mapping since there will be a conflict between the two embeddable column mappings.

Therefore, the Book entity needs to override the embeddable type mappings for each Publisher attribute:

Example 108. Overriding embeddable type attributes
@Entity(name = "Book")
@AttributeOverrides({
		@AttributeOverride(
				name = "ebookPublisher.name",
				column = @Column(name = "ebook_pub_name")
		),
		@AttributeOverride(
				name = "paperBackPublisher.name",
				column = @Column(name = "paper_back_pub_name")
		)
})
@AssociationOverrides({
		@AssociationOverride(
				name = "ebookPublisher.country",
				joinColumns = @JoinColumn(name = "ebook_pub_country_id")
		),
		@AssociationOverride(
				name = "paperBackPublisher.country",
				joinColumns = @JoinColumn(name = "paper_back_pub_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_pub_name varchar(255),
    paper_back_pub_name varchar(255),
    title varchar(255),
    ebook_pub_country_id bigint,
    paper_back_pub_country_id bigint,
    primary key (id)
)

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

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

3.3.3. Collections of embeddable types

Collections of embeddable types are specifically valued collections (as embeddable types are value types). Value collections are covered in detail in Collections of value types.

3.3.4. Embeddable type as a Map key

Embeddable types can also be used as Map keys. This topic is converted in detail in Map - key.

3.3.5. Embeddable type as identifier

Embeddable types can also be used as entity type identifiers. This usage is covered in detail in Composite identifiers.

Embeddable types that are used as collection entries, map keys or entity type identifiers cannot include their own collection mappings.

3.3.6. @Target mapping

The @Target annotation is used to specify the implementation class of a given association that is mapped via an interface. The @ManyToOne, @OneToOne, @OneToMany, and @ManyToMany feature a targetEntity attribute to specify the actual class of the entity association when an interface is used for the mapping.

The @ElementCollection association has a targetClass attribute for the same purpose.

However, for simple embeddable types, there is no such construct and so you need to use the Hibernate-specific @Target annotation instead.

Example 109. @Target mapping usage
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

}

The coordinates embeddable type is mapped as the Coordinates interface. However, Hibernate needs to know the actual implementation type, which is GPS in this case, hence the @Target annotation is used to provide this information.

Assuming we have persisted the following City entity:

Example 110. @Target persist example
doInJPA(this::entityManagerFactory, entityManager -> {

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

	entityManager.persist(cluj);
});

When fetching the City entity, the coordinates property is mapped by the @Target expression:

Example 111. @Target fetching example
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]

Therefore, the @Target annotation is used to define a custom join association between the parent-child association.

3.3.7. @Parent mapping

The Hibernate-specific @Parent annotation allows you to reference the owner entity from within an embeddable.

Example 112. @Parent mapping usage
@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

}

Assuming we have persisted the following City entity:

Example 113. @Parent persist example
doInJPA(this::entityManagerFactory, entityManager -> {

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

	entityManager.persist(cluj);
});

When fetching the City entity, the city property of the embeddable type acts as a back reference to the owning parent entity:

Example 114. @Parent fetching example
doInJPA(this::entityManagerFactory, entityManager -> {

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

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

Therefore, the @Parent annotation is used to define the association between an embeddable type and the owning entity.

3.3.8. Custom instantiation

Jakarta Persistence requires embeddable classes to follow Java Bean conventions. Part of this is the definition of a non-arg constructor. However, not all value compositions applications might map as embeddable values follow Java Bean conventions - e.g. a struct or Java 15 record.

Hibernate allows the use of a custom instantiator for creating the embeddable instances through the org.hibernate.metamodel.spi.EmbeddableInstantiator contract. For example, consider the following embeddable:

Example 115. EmbeddableInstantiator - Embeddable
@Embeddable
public class Name {
	@Column(name = "first_name")
	private final String first;
	@Column(name = "last_name")
	private final String last;

	private Name() {
		throw new UnsupportedOperationException();
	}

	public Name(String first, String last) {
		this.first = first;
		this.last = last;
	}

	public String getFirstName() {
		return first;
	}

	public String getLastName() {
		return last;
	}
}

Here, Name only allows use of the constructor accepting its state. Because this class does not follow Java Bean conventions, in terms of constructor, a custom strategy for instantiation is needed.

Example 116. EmbeddableInstantiator - Implementation
public class NameInstantiator implements EmbeddableInstantiator {
	@Override
	public Object instantiate(ValueAccess valueAccess, SessionFactoryImplementor sessionFactory) {
		// alphabetical
		final String first = valueAccess.getValue( 0, String.class );
		final String last = valueAccess.getValue( 1, String.class );
		return new Name( first, last );
	}

	// ...

}

There are a few ways to specify the custom instantiator. The @org.hibernate.annotations.EmbeddableInstantiator annotation can be used on the embedded attribute:

Example 117. @EmbeddableInstantiator on attribute
@Entity
public class Person {
	@Id
	public Integer id;
	@Embedded
	@EmbeddableInstantiator( NameInstantiator.class )
	public Name name;
	@ElementCollection
	@Embedded
	@EmbeddableInstantiator( NameInstantiator.class )
	public Set<Name> aliases;

}

@EmbeddableInstantiator may also be specified on the embeddable class:

Example 118. @EmbeddableInstantiator on class
@Embeddable
@EmbeddableInstantiator( NameInstantiator.class )
public class Name {
	@Column(name = "first_name")
	private final String first;
	@Column(name = "last_name")
	private final String last;

	private Name() {
		throw new UnsupportedOperationException();
	}

	public Name(String first, String last) {
		this.first = first;
		this.last = last;
	}

	public String getFirstName() {
		return first;
	}

	public String getLastName() {
		return last;
	}
}

@Entity
public class Person {
	@Id
	public Integer id;
	@Embedded
	public Name name;
	@ElementCollection
	@Embedded
	public Set<Name> aliases;
}

Lastly, @org.hibernate.annotations.EmbeddableInstantiatorRegistration may be used, which is useful when the application developer does not control the embeddable to be able to apply the instantiator on the embeddable.

Example 119. @EmbeddableInstantiatorRegistration
@Entity
@EmbeddableInstantiatorRegistration( embeddableClass = Name.class, instantiator = NameInstantiator.class )
public class Person {
	@Id
	public Integer id;
	@Embedded
	public Name name;
	@ElementCollection
	@Embedded
	public Set<Name> aliases;

}

3.3.9. Custom type mapping

Another approach is to supply the implementation of the org.hibernate.usertype.CompositeUserType contract using @CompositeType, which is an extension to the org.hibernate.metamodel.spi.EmbeddableInstantiator contract.

There are also corresponding, specialized forms of @CompositeType for specific model parts:

  • When mapping a Map, @CompositeType describes the Map value while @MapKeyCompositeType describes the Map key

  • For collection mappings, @CompositeType describes the elements

For example, consider the following custom type:

Example 120. CompositeUserType - Domain type
public class Name {
	private final String first;
	private final String last;

	public Name(String first, String last) {
		this.first = first;
		this.last = last;
	}

	public String firstName() {
		return first;
	}

	public String lastName() {
		return last;
	}
}

Here, Name only allows use of the constructor accepting its state. Because this class does not follow Java Bean conventions, a custom user type for instantiation and state access is needed.

Example 121. CompositeUserType - Implementation
public class NameCompositeUserType implements CompositeUserType<Name> {

	public static class NameMapper {
		String firstName;
		String lastName;
	}

	@Override
	public Class<?> embeddable() {
		return NameMapper.class;
	}

	@Override
	public Class<Name> returnedClass() {
		return Name.class;
	}

	@Override
	public Name instantiate(ValueAccess valueAccess, SessionFactoryImplementor sessionFactory) {
		// alphabetical
		final String first = valueAccess.getValue( 0, String.class );
		final String last = valueAccess.getValue( 1, String.class );
		return new Name( first, last );
	}

	@Override
	public Object getPropertyValue(Name component, int property) throws HibernateException {
		// alphabetical
		switch ( property ) {
			case 0:
				return component.firstName();
			case 1:
				return component.lastName();
		}
		return null;
	}

	@Override
	public boolean equals(Name x, Name y) {
		return x == y || x != null && Objects.equals( x.firstName(), y.firstName() )
				&& Objects.equals( x.lastName(), y.lastName() );
	}

	@Override
	public int hashCode(Name x) {
		return Objects.hash( x.firstName(), x.lastName() );
	}

	@Override
	public Name deepCopy(Name value) {
		return value; // immutable
	}

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

	@Override
	public Serializable disassemble(Name value) {
		return new String[] { value.firstName(), value.lastName() };
	}

	@Override
	public Name assemble(Serializable cached, Object owner) {
		final String[] parts = (String[]) cached;
		return new Name( parts[0], parts[1] );
	}

	@Override
	public Name replace(Name detached, Name managed, Object owner) {
		return detached;
	}

}

A composite user type needs an embeddable mapper class, which represents the embeddable mapping structure of the type i.e. the way the type would look like if you had the option to write a custom @Embeddable class.

In addition to the instantiation logic, a composite user type also has to provide a way to decompose the returned type into the individual components/properties of the embeddable mapper class through getPropertyValue. The property index, just like in the instantiate method, is based on the alphabetical order of the attribute names of the embeddable mapper class.

The composite user type also needs to provide methods to handle the mutability, equals, hashCode and the cache serialization and deserialization of the returned type.

There are a few ways to specify the composite user type. The @org.hibernate.annotations.CompositeType annotation can be used on the embedded and element collection attributes:

Example 122. @CompositeType on attribute
@Entity
public class Person {
	@Id
	public Integer id;

	@Embedded
	@AttributeOverride(name = "firstName", column = @Column(name = "first_name"))
	@AttributeOverride(name = "lastName", column = @Column(name = "last_name"))
	@CompositeType( NameCompositeUserType.class )
	public Name name;

	@ElementCollection
	@AttributeOverride(name = "firstName", column = @Column(name = "first_name"))
	@AttributeOverride(name = "lastName", column = @Column(name = "last_name"))
	@CompositeType( NameCompositeUserType.class )
	public Set<Name> aliases;

}

Or @org.hibernate.annotations.CompositeTypeRegistration may be used, which is useful when the application developer wants to apply the composite user type for all domain type uses.

Example 123. @CompositeTypeRegistration
@Entity
@CompositeTypeRegistration( embeddableClass = Name.class, userType = NameCompositeUserType.class )
public class Person {
	@Id
	public Integer id;

	@Embedded
	@AttributeOverride(name = "firstName", column = @Column(name = "first_name"))
	@AttributeOverride(name = "lastName", column = @Column(name = "last_name"))
	public Name name;

	@ElementCollection
	@AttributeOverride(name = "firstName", column = @Column(name = "first_name"))
	@AttributeOverride(name = "lastName", column = @Column(name = "last_name"))
	public Set<Name> aliases;

}

3.3.10. Embeddables and ImplicitNamingStrategy

The ImplicitNamingStrategyComponentPathImpl is a Hibernate-specific feature. Users concerned with Jakarta Persistence provider portability should instead prefer explicit column naming with @AttributeOverride.

Hibernate naming strategies are covered in detail in Naming. However, for the purposes of this discussion, Hibernate has the capability to interpret implicit column names in a way that is safe for use with multiple embeddable types.

Example 124. Implicit multiple embeddable type mapping
@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
}

To make it work, you need to use the ImplicitNamingStrategyComponentPathImpl naming strategy.

Example 125. Enabling implicit embeddable type mapping using the component path naming strategy
metadataBuilder.applyImplicitNamingStrategy(
	ImplicitNamingStrategyComponentPathImpl.INSTANCE
);

Now the "path" to attributes are used in the implicit column naming:

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)
)

You could even develop your own naming strategy to do other types of implicit naming strategies.

3.3.11. Aggregate embeddable mapping

An embeddable mapping is usually just a way to encapsulate columns of a table into a Java type, but as of Hibernate 6.2, it is also possible to map embeddable types as SQL aggregate types.

Currently, there are three possible SQL aggregate types which can be specified by annotating one of the following annotations on a persistent attribute:

  • @Struct - maps to a named SQL object type

  • @JdbcTypeCode(SqlTypes.JSON) - maps to the SQL type JSON

  • @JdbcTypeCode(SqlTypes.SQLXML) - maps to the SQL type XML

Any read or assignment (in an update statement) expression for an attribute of such an embeddable will resolve to the proper SQL expression to access/update the attribute of the SQL type.

Since object, JSON and XML types are not supported equally on all databases, beware that not every mapping will work on all databases. The following table outlines the current support for the different aggregate types:

Database Struct JSON XML

PostgreSQL

Yes

Yes

No (not yet)

Oracle

Yes

Yes

No (not yet)

DB2

Yes

No (not yet)

No (not yet)

SQL Server

No (not yet)

No (not yet)

No (not yet)

Also note that embeddable types that are used in aggregate mappings do not yet support all kinds of attribute mappings, most notably:

  • Association mappings (@ManyToOne, @OneToOne, @OneToMany, @ManyToMany, @ElementCollection)

  • Basic array mappings

@Struct aggregate embeddable mapping

The @Struct annotation can be placed on either the persistent attribute, or the embeddable type, and requires the specification of a name i.e. the name of the SQL object type that it maps to.

The following example mapping, maps the EmbeddableAggregate type to the SQL object type structType:

Example 126. Mapping embeddable as SQL object type on persistent attribute level
@Entity(name = "StructHolder")
public static class StructHolder {

	@Id
	private Long id;
	@Struct(name = "structType")
	private EmbeddableAggregate aggregate;

}

The schema generation will by default emit DDL for that object type, which looks something along the lines of

create type structType as (
    ...
)
create table StructHolder as (
    id bigint not null primary key,
    aggregate structType
)

The name and the nullability of the column can be refined through applying a @Column on the persistent attribute.

One very important thing to note is that the order of columns in the DDL definition of a type must match the order that Hibernate expects. By default, the order of columns is based on the alphabetical ordering of the embeddable type attribute names.

Consider the following class:

@Embeddable
@Struct(name = "myStruct")
public class MyStruct {
	@Column(name = "b")
	String attr1;
	@Column(name = "a")
	String attr2;
}

The expected ordering of columns will be (b,a), because the name attr1 comes before attr2 in alphabetical ordering. This example aims at showing the importance of the persistent attribute name.

Defining the embeddable type as Java record instead of a class can force a particular ordering through the definition of canonical constructor.

@Embeddable
@Struct(name = "myStruct")
public record MyStruct (
	@Column(name = "a")
	String attr2,
	@Column(name = "b")
	String attr1
) {}

In this particular example, the expected ordering of columns will be (a,b), because the canonical constructor of the record defines a specific ordering of persistent attributes, which Hibernate makes use of for @Struct mappings.

It is not necessary to switch to Java records to configure the order though. The @Struct annotation allows specifying the order through the attributes member, an array of attribute names that the embeddable type declares, which defines the order in columns appear in the SQL object type.

The same ordering as with the Java record can be achieved this way:

@Embeddable
@Struct(name = "myStruct", attributes = {"attr2", "attr1"})
public class MyStruct {
	@Column(name = "b")
	String attr1;
	@Column(name = "a")
	String attr2;
}
JSON/XML aggregate embeddable mapping

The @JdbcTypeCode annotation for JSON and XML mappings can only be placed on the persistent attribute.

The following example mapping, maps the EmbeddableAggregate type to the JSON SQL type:

Example 127. Mapping embeddable as JSON
@Entity(name = "JsonHolder")
public static class JsonHolder {

	@Id
	private Long id;
	@JdbcTypeCode(SqlTypes.JSON)
	private EmbeddableAggregate aggregate;

}

The schema generation will by default emit DDL that ensures the constraints of the embeddable type are respected, which looks something along the lines of

create table JsonHolder as (
    id bigint not null primary key,
    aggregate json,
    check (json_value(aggregate, '$.attribute1') is not null)
)

Again, the name and the nullability of the aggregate column can be refined through applying a @Column on the persistent attribute.

3.3.12. Embeddable mappings containing collections

Mapping collections inside an @Embeddable value is supported in most cases. There are a couple exceptions:

  • If the values of an @ElementCollection is of embeddable type, that embeddable cannot contain nested collections;

  • Explicitly selecting an embeddable that contains collections in a query is currently not supported (we wouldn’t be able to correctly initialize the collection since its owning entity instance would be missing from the Persistence Context).

3.4. Entity types

Usage of the word entity

The entity type describes the mapping between the actual persistable domain model object and a database table row. To avoid any confusion with the annotation that marks a given entity type, the annotation will be further referred to as @Entity.

Throughout this chapter and thereafter, entity types will be simply referred to as entity.

3.4.1. POJO Models

Section 2.1 The Entity Class of the Java Persistence 2.1 specification defines its requirements for an entity class. Applications that wish to remain portable across Jakarta Persistence providers should adhere to these requirements:

  • The entity class must be annotated with the jakarta.persistence.Entity annotation (or be denoted as such in XML mapping).

  • The entity class must have a public or protected no-argument constructor. It may define additional constructors as well.

  • The entity class must be a top-level class.

  • An enum or interface may not be designated as an entity.

  • The entity class must not be final. No methods or persistent instance variables of the entity class may be final.

  • If an entity instance is to be used remotely as a detached object, the entity class must implement the Serializable interface.

  • Both abstract and concrete classes can be entities. Entities may extend non-entity classes as well as entity classes, and non-entity classes may extend entity classes.

  • The persistent state of an entity is represented by instance variables, which may correspond to JavaBean-style properties. An instance variable must be directly accessed only from within the methods of the entity by the entity instance itself. The state of the entity is available to clients only through the entity’s accessor methods (getter/setter methods) or other business methods.

Hibernate, however, is not as strict in its requirements. The differences from the list above include:

  • The entity class must have a no-argument constructor, which may be public, protected or package visibility. It may define additional constructors as well.

  • The entity class need not be a top-level class.

  • Technically Hibernate can persist final classes or classes with final persistent state accessor (getter/setter) methods. However, it is generally not a good idea as doing so will stop Hibernate from being able to generate proxies for lazy-loading the entity.

  • Hibernate does not restrict the application developer from exposing instance variables and referencing them from outside the entity class itself. The validity of such a paradigm, however, is debatable at best.

Let’s look at each requirement in detail.

3.4.2. Prefer non-final classes

A central feature of Hibernate is the ability to load lazily certain entity instance variables (attributes) via runtime proxies. This feature depends upon the entity class being non-final or else implementing an interface that declares all the attribute getters/setters. You can still persist final classes that do not implement such an interface with Hibernate, but you will not be able to use proxies for fetching lazy associations, therefore limiting your options for performance tuning. For the very same reason, you should also avoid declaring persistent attribute getters and setters as final.

Starting with 5.0, Hibernate offers a more robust version of bytecode enhancement as another means for handling lazy loading. Hibernate had some bytecode re-writing capabilities prior to 5.0 but they were very rudimentary. See the Bytecode Enhancement for additional information on fetching and on bytecode enhancement.

3.4.3. Implement a no-argument constructor

The entity class should have a no-argument constructor. Both Hibernate and Jakarta Persistence require this.

Jakarta Persistence requires that this constructor be defined as public or protected. Hibernate, for the most part, does not care about the constructor visibility, as long as the system SecurityManager allows overriding the visibility setting. That said, the constructor should be defined with at least package visibility if you wish to leverage runtime proxy generation.

3.4.4. Declare getters and setters for persistent attributes

The Jakarta Persistence specification requires this, otherwise, the model would prevent accessing the entity persistent state fields directly from outside the entity itself.

Although Hibernate does not require it, it is recommended to follow the JavaBean conventions and define getters and setters for entity persistent attributes. Nevertheless, you can still tell Hibernate to directly access the entity fields.

Attributes (whether fields or getters/setters) need not be declared public. Hibernate can deal with attributes declared with the public, protected, package or private visibility. Again, if wanting to use runtime proxy generation for lazy loading, the getter/setter should grant access to at least package visibility.

3.4.5. Providing identifier attribute(s)

Historically, providing identifier attributes was considered optional.

However, not defining identifier attributes on the entity should be considered a deprecated feature that will be removed in an upcoming release.

The identifier attribute does not necessarily need to be mapped to the column(s) that physically define the primary key. However, it should map to column(s) that can uniquely identify each row.

We recommend that you declare consistently-named identifier attributes on persistent classes and that you use a wrapper (i.e., non-primitive) type (e.g. Long or Integer).

The placement of the @Id annotation marks the persistence state access strategy.

Example 128. Identifier mapping
@Id
private Long id;

Hibernate offers multiple identifier generation strategies, see the Identifier Generators chapter for more about this topic.

3.4.6. Mapping the entity

The main piece in mapping the entity is the jakarta.persistence.Entity annotation.

The @Entity annotation defines just the name attribute which is used to give a specific entity name for use in JPQL queries.

By default, if the name attribute of the @Entity annotation is missing, the unqualified name of the entity class itself will be used as the entity name.

Because the entity name is given by the unqualified name of the class, Hibernate does not allow registering multiple entities with the same name even if the entity classes reside in different packages.

Without imposing this restriction, Hibernate would not know which entity class is referenced in a JPQL query if the unqualified entity name is associated with more then one entity classes.

In the following example, the entity name (e.g. Book) is given by the unqualified name of the entity class name.

Example 129. @Entity mapping with an implicit name
@Entity
public class Book {

	@Id
	private Long id;

	private String title;

	private String author;

	//Getters and setters are omitted for brevity
}

However, the entity name can also be set explicitly as illustrated by the following example.

Example 130. @Entity mapping with an explicit name
@Entity(name = "Book")
public static class Book {

	@Id
	private Long id;

	private String title;

	private String author;

	//Getters and setters are omitted for brevity
}

An entity models a database table. The identifier uniquely identifies each row in that table. By default, the name of the table is assumed to be the same as the name of the entity. To explicitly give the name of the table or to specify other information about the table, we would use the jakarta.persistence.Table annotation.

Example 131. Simple @Entity with @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
 }
Mapping the catalog of the associated table

Without specifying the catalog of the associated database table a given entity is mapped to, Hibernate will use the default catalog associated with the current database connection.

However, if your database hosts multiple catalogs, you can specify the catalog where a given table is located using the catalog attribute of the Jakarta Persistence @Table annotation.

Let’s assume we are using MySQL and want to map a Book entity to the book table located in the public catalog which looks as follows.

Example 132. The book table located in the public catalog
create table public.book (
  id bigint not null,
  author varchar(255),
  title varchar(255),
  primary key (id)
) engine=InnoDB

Now, to map the Book entity to the book table in the public catalog we can use the catalog attribute of the @Table Jakarta Persistence annotation.

Example 133. Specifying the database catalog using the @Table annotation
@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
}
Mapping the schema of the associated table

Without specifying the schema of the associated database table a given entity is mapped to, Hibernate will use the default schema associated with the current database connection.

However, if your database supports schemas, you can specify the schema where a given table is located using the schema attribute of the Jakarta Persistence @Table annotation.

Let’s assume we are using PostgreSQL and want to map a Book entity to the book table located in the library schema which looks as follows.

Example 134. The book table located in the library schema
create table library.book (
  id int8 not null,
  author varchar(255),
  title varchar(255),
  primary key (id)
)

Now, to map the Book entity to the book table in the library schema we can use the schema attribute of the @Table Jakarta Persistence annotation.

Example 135. Specifying the database schema using the @Table annotation
@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
}

The schema attribute of the @Table annotation works only if the underlying database supports schemas (e.g. PostgreSQL).

Therefore, if you’re using MySQL or MariaDB, which do not support schemas natively (schemas being just an alias for catalog), you need to use the catalog attribute, and not the schema one.

3.4.7. Implementing equals() and hashCode()

Much of the discussion in this section deals with the relation of an entity to a Hibernate Session, whether the entity is managed, transient or detached. If you are unfamiliar with these topics, they are explained in the Persistence Context chapter.

Whether to implement equals() and hashCode() methods in your domain model, let alone how to implement them, is a surprisingly tricky discussion when it comes to ORM.

There is really just one absolute case: a class that acts as an identifier must implement equals/hashCode based on the id value(s). Generally, this is pertinent for user-defined classes used as composite identifiers. Beyond this one very specific use case and few others we will discuss below, you may want to consider not implementing equals/hashCode altogether.

So what’s all the fuss? Normally, most Java objects provide a built-in equals() and hashCode() based on the object’s identity, so each new object will be different from all others. This is generally what you want in ordinary Java programming. Conceptually, however, this starts to break down when you start to think about the possibility of multiple instances of a class representing the same data.

This is, in fact, exactly the case when dealing with data coming from a database. Every time we load a specific Person from the database we would naturally get a unique instance. Hibernate, however, works hard to make sure that does not happen within a given Session. In fact, Hibernate guarantees equivalence of persistent identity (database row) and Java identity inside a particular session scope. So if we ask a Hibernate Session to load that specific Person multiple times we will actually get back the same instance:

Example 136. Scope of identity
Book book1 = entityManager.find(Book.class, 1L);
Book book2 = entityManager.find(Book.class, 1L);

assertTrue(book1 == book2);

Consider we have a Library parent entity which contains a java.util.Set of Book entities:

Example 137. Library entity mapping
@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
}
Example 138. Set usage with Session-scoped identity
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());

However, the semantic changes when we mix instances loaded from different Sessions:

Example 139. Mixed Sessions
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());
});

Specifically, the outcome in this last example will depend on whether the Book class implemented equals/hashCode, and, if so, how.

If the Book class did not override the default equals/hashCode, then the two Book object references are not going to be equal since their references are different.

Consider yet another case:

Example 140. Sets with transient entities
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());

In cases where you will be dealing with entities outside of a Session (whether they be transient or detached), especially in cases where you will be using them in Java collections, you should consider implementing equals/hashCode.

A common initial approach is to use the entity’s identifier attribute as the basis for equals/hashCode calculations:

Example 141. Naive equals/hashCode implementation
@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 instanceof Book)) {
			return false;
		}
		Book book = (Book) o;
		return Objects.equals(id, book.getId());
	}

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

It turns out that this still breaks when adding transient instance of Book to a set as we saw in the last example:

Example 142. Auto-generated identifiers with Sets and naive 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));

The issue here is a conflict between the use of the generated identifier, the contract of Set, and the equals/hashCode implementations. Set says that the equals/hashCode value for an object should not change while the object is part of the Set. But that is exactly what happened here because the equals/hasCode are based on the (generated) id, which was not set until the Jakarta Persistence transaction is committed.

Note that this is just a concern when using generated identifiers. If you are using assigned identifiers this will not be a problem, assuming the identifier value is assigned prior to adding to the Set.

Another option is to force the identifier to be generated and set prior to adding to the Set:

Example 143. Forcing the flush before adding to the Set
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));

But this is often not feasible.

The final approach is to use a "better" equals/hashCode implementation, making use of a natural-id or business-key.

Example 144. Natural Id 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;

	@NaturalId
	private String isbn;

	//Getters and setters are omitted for brevity

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

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

This time, when adding a Book to the Library Set, you can retrieve the Book even after it’s being persisted:

Example 145. Natural Id equals/hashCode persist example
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));

As you can see the question of equals/hashCode is not trivial, nor is there a one-size-fits-all solution.

Although using a natural-id is best for equals and hashCode, sometimes you only have the entity identifier that provides a unique constraint.

It’s possible to use the entity identifier for equality check, but it needs a workaround:

  • you need to provide a constant value for hashCode so that the hash code value does not change before and after the entity is flushed.

  • you need to compare the entity identifier equality only for non-transient entities.

For details on mapping the identifier, see the Identifiers chapter.

3.4.8. Mapping the entity to a SQL query

You can map an entity to a SQL query using the @Subselect annotation.

Example 146. @Subselect entity mapping
@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

}

In the example above, the Account entity does not retain any balance since every account operation is registered as an AccountTransaction. To find the Account balance, we need to query the AccountSummary which shares the same identifier with the Account entity.

However, the AccountSummary is not mapped to a physical table, but to an SQL query.

So, if we have the following AccountTransaction record, the AccountSummary balance will match the proper amount of money in this Account.

Example 147. Finding a @Subselect entity
scope.inTransaction(
		(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());
		}
);

If we add a new AccountTransaction entity and refresh the AccountSummary entity, the balance is updated accordingly:

Example 148. Refreshing a @Subselect entity
scope.inTransaction(
		(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());
		}
);

The goal of the @Synchronize annotation in the AccountSummary entity mapping is to instruct Hibernate which database tables are needed by the underlying @Subselect SQL query. This is because, unlike JPQL and HQL queries, Hibernate cannot parse the underlying native SQL query.

With the @Synchronize annotation in place, when executing an HQL or JPQL which selects from the AccountSummary entity, Hibernate will trigger a Persistence Context flush if there are pending Account, Client or AccountTransaction entity state transitions.

3.4.9. Define a custom entity proxy

By default, when it needs to use a proxy instead of the actual POJO, Hibernate is going to use a Bytecode manipulation library like Byte Buddy.

However, if the entity class is final, a proxy will not be created; you will get a POJO even when you only need a proxy reference. In this case, you could proxy an interface that this particular entity implements, as illustrated by the following example.

Example 149. Final entity class implementing the Identifiable interface
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
}

The @Proxy annotation is used to specify a custom proxy implementation for the current annotated entity.

When loading the Book entity proxy, Hibernate is going to proxy the Identifiable interface instead as illustrated by the following example:

Example 150. Proxying the final entity class implementing the Identifiable interface
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]

As you can see in the associated SQL snippet, Hibernate issues no SQL SELECT query since the proxy can be constructed without needing to fetch the actual entity POJO.

3.4.10. Define a custom entity persister

The @Persister annotation is used to specify a custom entity or collection persister.

For entities, the custom persister must implement the EntityPersister interface.

For collections, the custom persister must implement the CollectionPersister interface.

Supplying a custom persister has been allowed historically, but has never been fully supported. Hibernate 6 provides better, alternative ways to accomplish the use cases for a custom persister. As of 6.2 @Persister has been formally deprecated.
Example 151. Entity persister mapping
@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
}

By providing your own EntityPersister and CollectionPersister implementations, you can control how entities and collections are persisted into the database.

3.5. Naming strategies

Part of the mapping of an object model to the relational database is mapping names from the object model to the corresponding database names. Hibernate looks at this as 2-stage process:

  • The first stage is determining a proper logical name from the domain model mapping. A logical name can be either explicitly specified by the user (e.g., using @Column or @Table) or it can be implicitly determined by Hibernate through an ImplicitNamingStrategy contract.

  • Second is the resolving of this logical name to a physical name which is defined by the PhysicalNamingStrategy contract.

Historical NamingStrategy contract

Historically Hibernate defined just a single org.hibernate.cfg.NamingStrategy. That singular NamingStrategy contract actually combined the separate concerns that are now modeled individually as ImplicitNamingStrategy and PhysicalNamingStrategy.

Also, the NamingStrategy contract was often not flexible enough to properly apply a given naming "rule", either because the API lacked the information to decide or because the API was honestly not well defined as it grew.

Due to these limitations, org.hibernate.cfg.NamingStrategy has been deprecated in favor of ImplicitNamingStrategy and PhysicalNamingStrategy.

At the core, the idea behind each naming strategy is to minimize the amount of repetitive information a developer must provide for mapping a domain model.

Jakarta Persistence Compatibility

Jakarta Persistence defines inherent rules about implicit logical name determination. If Jakarta Persistence provider portability is a major concern, or if you really just like the Jakarta Persistence-defined implicit naming rules, be sure to stick with ImplicitNamingStrategyJpaCompliantImpl (the default).

Also, Jakarta Persistence defines no separation between logical and physical name. Following the Jakarta Persistence specification, the logical name is the physical name. If Jakarta Persistence provider portability is important, applications should prefer not to specify a PhysicalNamingStrategy.

3.5.1. ImplicitNamingStrategy

When an entity does not explicitly name the database table that it maps to, we need to implicitly determine that table name. Or when a particular attribute does not explicitly name the database column that it maps to, we need to implicitly determine that column name. There are examples of the role of the org.hibernate.boot.model.naming.ImplicitNamingStrategy contract to determine a logical name when the mapping did not provide an explicit name.

Implicit Naming Strategy Diagram

Hibernate defines multiple ImplicitNamingStrategy implementations out-of-the-box. Applications are also free to plug in custom implementations.

There are multiple ways to specify the ImplicitNamingStrategy to use. First, applications can specify the implementation using the hibernate.implicit_naming_strategy configuration setting which accepts:

  • pre-defined "short names" for the out-of-the-box implementations

    default

    for org.hibernate.boot.model.naming.ImplicitNamingStrategyJpaCompliantImpl - an alias for jpa

    jpa

    for org.hibernate.boot.model.naming.ImplicitNamingStrategyJpaCompliantImpl - the Jakarta Persistence compliant naming strategy

    legacy-hbm

    for org.hibernate.boot.model.naming.ImplicitNamingStrategyLegacyHbmImpl - compliant with the original Hibernate NamingStrategy

    legacy-jpa

    for org.hibernate.boot.model.naming.ImplicitNamingStrategyLegacyJpaImpl - compliant with the legacy NamingStrategy developed for Java Persistence 1.0, which was unfortunately unclear in many respects regarding implicit naming rules

    component-path

    for org.hibernate.boot.model.naming.ImplicitNamingStrategyComponentPathImpl - mostly follows ImplicitNamingStrategyJpaCompliantImpl rules, except that it uses the full composite paths, as opposed to just the ending property part

  • reference to a Class that implements the org.hibernate.boot.model.naming.ImplicitNamingStrategy contract

  • FQN of a class that implements the org.hibernate.boot.model.naming.ImplicitNamingStrategy contract

Secondly, applications and integrations can leverage org.hibernate.boot.MetadataBuilder#applyImplicitNamingStrategy to specify the ImplicitNamingStrategy to use. See Bootstrap for additional details on bootstrapping.

3.5.2. PhysicalNamingStrategy

Many organizations define rules around the naming of database objects (tables, columns, foreign keys, etc). The idea of a PhysicalNamingStrategy is to help implement such naming rules without having to hard-code them into the mapping via explicit names.

While the purpose of an ImplicitNamingStrategy is to determine that an attribute named accountNumber maps to a logical column name of accountNumber when not explicitly specified, the purpose of a PhysicalNamingStrategy would be, for example, to say that the physical column name should instead be abbreviated to acct_num.

It is true that the resolution to acct_num could have been handled using an ImplicitNamingStrategy in this case.

But the point here is the separation of concerns. The PhysicalNamingStrategy will be applied regardless of whether the attribute explicitly specified the column name or whether we determined that implicitly. The ImplicitNamingStrategy would only be applied if an explicit name was not given. So, it all depends on needs and intent.

The default implementation is to simply use the logical name as the physical name. However applications and integrations can define custom implementations of this PhysicalNamingStrategy contract. Here is an example PhysicalNamingStrategy for a fictitious company named Acme Corp whose naming standards are to:

  • prefer underscore-delimited words rather than camel casing

  • replace certain words with standard abbreviations

Example 152. Example PhysicalNamingStrategy implementation
/*
 * 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.orm.test.naming;

import java.util.Arrays;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.TreeMap;
import java.util.stream.Collectors;

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

import org.junit.platform.commons.util.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
 * @author Nathan Xu
 */
public class AcmeCorpPhysicalNamingStrategy extends PhysicalNamingStrategyStandardImpl {
	private static final Map<String, String> ABBREVIATIONS;

	static {
		ABBREVIATIONS = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
		ABBREVIATIONS.put("account", "acct");
		ABBREVIATIONS.put("number", "num");
	}

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

	@Override
	public Identifier toPhysicalSequenceName(Identifier logicalName, JdbcEnvironment jdbcEnvironment) {
		final List<String> parts = splitAndReplace( logicalName.getText());
		// Acme Corp says all sequences should end with _seq
		if (!"seq".equals(parts.get(parts.size() - 1))) {
			parts.add("seq");
		}
		return jdbcEnvironment.getIdentifierHelper().toIdentifier(
				String.join("_", parts),
				logicalName.isQuoted()
		);
	}

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

	private List<String> splitAndReplace(String name) {
		return Arrays.stream(splitByCharacterTypeCamelCase(name))
				.filter(StringUtils::isNotBlank)
				.map(p -> ABBREVIATIONS.getOrDefault(p, p).toLowerCase(Locale.ROOT))
				.collect(Collectors.toList());
	}

	private String[] splitByCharacterTypeCamelCase(String s) {
		return s.split( "(?<!(^|[A-Z]))(?=[A-Z])|(?<!^)(?=[A-Z][a-z])" );
	}
}

There are multiple ways to specify the PhysicalNamingStrategy to use. First, applications can specify the implementation using the hibernate.physical_naming_strategy configuration setting which accepts:

  • reference to a Class that implements the org.hibernate.boot.model.naming.PhysicalNamingStrategy contract

  • FQN of a class that implements the org.hibernate.boot.model.naming.PhysicalNamingStrategy contract

Secondly, applications and integrations can leverage org.hibernate.boot.MetadataBuilder#applyPhysicalNamingStrategy. See Bootstrap for additional details on bootstrapping.

3.6. Access strategies

As a Jakarta Persistence provider, Hibernate can introspect both the entity attributes (instance fields) or the accessors (instance properties). By default, the placement of the @Id annotation gives the default access strategy. When placed on a field, Hibernate will assume field-based access. When placed on the identifier getter, Hibernate will use property-based access.

To avoid issues such as HCANN-63 - Property name beginning with at least two uppercase characters has odd functionality in HQL, you should pay attention to Java Bean specification in regard to naming properties.

Embeddable types inherit the access strategy from their parent entities.

3.6.1. Field-based access

Example 153. Field-based access
@Entity(name = "Book")
public static class Book {

	@Id
	private Long id;

	private String title;

	private String author;

	//Getters and setters are omitted for brevity
}

When using field-based access, adding other entity-level methods is much more flexible because Hibernate won’t consider those part of the persistence state. To exclude a field from being part of the entity persistent state, the field must be marked with the @Transient annotation.

Another advantage of using field-based access is that some entity attributes can be hidden from outside the entity.

An example of such attribute is the entity @Version field, which, usually, does not need to be manipulated by the data access layer.

With field-based access, we can simply omit the getter and the setter for this version field, and Hibernate can still leverage the optimistic concurrency control mechanism.

3.6.2. Property-based access

Example 154. Property-based access
@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;
	}
}

When using property-based access, Hibernate uses the accessors for both reading and writing the entity state. Every other method that will be added to the entity (e.g. helper methods for synchronizing both ends of a bidirectional one-to-many association) will have to be marked with the @Transient annotation.

3.6.3. Overriding the default access strategy

The default access strategy mechanism can be overridden with the Jakarta Persistence @Access annotation. In the following example, the @Version attribute is accessed by its field and not by its getter, like the rest of entity attributes.

Example 155. Overriding access strategy
@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;
	}
}

3.6.4. Embeddable types and access strategy

Because embeddables are managed by their owning entities, the access strategy is therefore inherited from the entity too. This applies to both simple embeddable types as well as for collection of embeddables.

The embeddable types can overrule the default implicit access strategy (inherited from the owning entity). In the following example, the embeddable uses property-based access, no matter what access strategy the owning entity is choosing:

Example 156. Embeddable with exclusive access strategy
@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;
	}
}

The owning entity can use field-based access while the embeddable uses property-based access as it has chosen explicitly:

Example 157. Entity including a single embeddable type
@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
}

This works also for collection of embeddable types:

Example 158. Entity including a collection of embeddable types
@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
}

3.7. Identifiers

Identifiers model the primary key of an entity. They are used to uniquely identify each specific entity.

Hibernate and Jakarta Persistence both make the following assumptions about the corresponding database column(s):

UNIQUE

The values must uniquely identify each row.

NOT NULL

The values cannot be null. For composite ids, no part can be null.

IMMUTABLE

The values, once inserted, can never be changed. In cases where the values for the PK you have chosen will be updated, Hibernate recommends mapping the mutable value as a natural id, and use a surrogate id for the PK. See Natural Ids.

Technically the identifier does not have to map to the column(s) physically defined as the table primary key. They just need to map to column(s) that uniquely identify each row. However, this documentation will continue to use the terms identifier and primary key interchangeably.

Every entity must define an identifier. For entity inheritance hierarchies, the identifier must be defined just on the entity that is the root of the hierarchy.

An identifier may be simple or composite.

3.7.1. Simple identifiers

Simple identifiers map to a single basic attribute, and are denoted using the jakarta.persistence.Id annotation.

According to Jakarta Persistence, only the following types are portably supported for use as identifier attribute types:

  • any Java primitive type

  • any primitive wrapper type

  • java.lang.String

  • java.util.Date (TemporalType#DATE)

  • java.sql.Date

  • java.math.BigDecimal

  • java.math.BigInteger

Hibernate, however, supports a more broad set of types to be used for identifiers (UUID, e.g.).

Assigned identifiers

Values for simple identifiers can be assigned, which simply means that the application itself will assign the value to the identifier attribute prior to persisting the entity.

Example 159. Simple assigned entity identifier
@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

Values for simple identifiers can be generated. To denote that an identifier attribute is generated, it is annotated with jakarta.persistence.GeneratedValue

Example 160. Simple generated identifier
@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
}

When an entity with an identifier defined as generated is persisted, Hibernate will generate the value based on an associated generation strategy. Identifier value generations strategies are discussed in detail in the Generated identifier values section.

While Hibernate supports almost any valid basic type be used for generated identifier values, Jakarta Persistence restricts the allowable types to just integer types.

3.7.2. Composite identifiers

Composite identifiers correspond to one or more persistent attributes. Here are the rules governing composite identifiers, as defined by the Jakarta Persistence specification:

  • The composite identifier must be represented by a "primary key class". The primary key class may be defined using the jakarta.persistence.EmbeddedId annotation (see Composite identifiers with @EmbeddedId), or defined using the jakarta.persistence.IdClass annotation (see Composite identifiers with @IdClass).

  • The primary key class must be public and must have a public no-arg constructor.

  • The primary key class must be serializable.

  • The primary key class must define equals and hashCode methods, consistent with equality for the underlying database types to which the primary key is mapped.

The restriction that a composite identifier has to be represented by a "primary key class" (e.g. @EmbeddedId or @IdClass) is only Jakarta Persistence-specific.

Hibernate does allow composite identifiers to be defined without a "primary key class" via multiple @Id attributes, although that is generally considered poor design.

The attributes making up the composition can be either basic, composite or @ManyToOne. Note especially that collection and one-to-one are never appropriate.

3.7.3. Composite identifiers with @EmbeddedId

Modeling a composite identifier using an EmbeddedId simply means defining an embeddable to be a composition for the attributes making up the identifier, and then exposing an attribute of that embeddable type on the entity.

Example 161. Basic @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);
	}
}

As mentioned before, EmbeddedIds can even contain @ManyToOne attributes:

Example 162. @EmbeddedId with @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);
	}
}

Hibernate supports directly modeling @ManyToOne associations in the Primary Key class, whether @EmbeddedId or @IdClass.

However, that is not portably supported by the Jakarta Persistence specification. In Jakarta Persistence terms, one would use "derived identifiers". For more details, see Derived Identifiers.

3.7.4. Composite identifiers with @IdClass

Modeling a composite identifier using an IdClass differs from using an EmbeddedId in that the entity defines each individual attribute making up the composition. The IdClass is used as the representation of the identifier for load-by-id operations.

Example 163. Basic @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);
	}
}

Non-aggregated composite identifiers can also contain ManyToOne attributes as we saw with aggregated mappings, though still non-portably.

Example 164. IdClass with @ManyToOne
@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
}

With non-aggregated composite identifiers, Hibernate also supports "partial" generation of the composite values.

Example 165. @IdClass with partial identifier generation using @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);
	}
}

This feature which allows auto-generated values in composite identifiers exists because of a highly questionable interpretation of the Jakarta Persistence specification made by the SpecJ committee.

Hibernate does not feel that Jakarta Persistence defines support for this, but added the feature simply to be usable in SpecJ benchmarks. Use of this feature may or may not be portable from a Jakarta Persistence perspective.

3.7.5. Composite identifiers with associations

Hibernate allows defining a composite identifier out of entity associations. In the following example, the Book entity identifier is formed of two @ManyToOne associations.

Example 166. Composite identifiers with associations
@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);
	}
}

Although the mapping is much simpler than using an @EmbeddedId or an @IdClass, there’s no separation between the entity instance and the actual identifier. To query this entity, an instance of the entity itself must be supplied to the persistence context.

Example 167. Fetching with composite identifiers
Book book = entityManager.find(Book.class, new Book(
	author,
	publisher,
	"High-Performance Java Persistence"
));

assertEquals("Vlad Mihalcea", book.getAuthor().getName());

3.7.6. Composite identifiers with generated properties

When using composite identifiers, the underlying identifier properties must be manually assigned by the user.

Automatically generated properties are not supported to be used to generate the value of an underlying property that makes the composite identifier.

Therefore, you cannot use any of the automatic property generator described by the generated properties section like @Generated, @CreationTimestamp or @ValueGenerationType or database-generated values.

Nevertheless, you can still generate the identifier properties prior to constructing the composite identifier, as illustrated by the following examples.

Assuming we have the following EventId composite identifier and an Event entity which uses the aforementioned composite identifier.

Example 168. The Event entity and EventId composite identifier
@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);
	}
}
In-memory generated composite identifier properties

If you want to generate the composite identifier properties in-memory, you need to do that as follows:

Example 169. In-memory generated composite identifier properties example
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);

Notice that the createdOn property of the EventId composite identifier was generated by the data access code and assigned to the identifier prior to persisting the Event entity.

Database generated composite identifier properties

If you want to generate the composite identifier properties using a database function or stored procedure, you could to do it as illustrated by the following example.

Example 170. Database generated composite identifier properties example
OffsetDateTime currentTimestamp = (OffsetDateTime) entityManager
.createNativeQuery(
	"SELECT CURRENT_TIMESTAMP", OffsetDateTime.class)
.getSingleResult();

EventId id = new EventId();
id.setCategory(1);
id.setCreatedOn(Timestamp.from(currentTimestamp.toInstant()));

Event event = new Event();
event.setId(id);
event.setKey("Temperature");
event.setValue("9");

entityManager.persist(event);

Notice that the createdOn property of the EventId composite identifier was generated by calling the CURRENT_TIMESTAMP database function, and we assigned it to the composite identifier prior to persisting the Event entity.

3.7.7. Generated identifier values

Hibernate supports identifier value generation across a number of different types. Remember that Jakarta Persistence portably defines identifier value generation just for integer types.

You can also auto-generate values for non-identifier attributes. For more details, see the Generated properties section.

Identifier value generation is indicated using the jakarta.persistence.GeneratedValue annotation. The most important piece of information here is the specified jakarta.persistence.GenerationType which indicates how values will be generated.

AUTO (the default)

Indicates that the persistence provider (Hibernate) should choose an appropriate generation strategy. See Interpreting AUTO.

IDENTITY

Indicates that database IDENTITY columns will be used for primary key value generation. See Using IDENTITY columns.

SEQUENCE

Indicates that database sequence should be used for obtaining primary key values. See Using sequences.

TABLE

Indicates that a database table should be used for obtaining primary key values. See Using the table identifier generator.

3.7.8. Interpreting AUTO

How a persistence provider interprets the AUTO generation type is left up to the provider.

The default behavior is to look at the Java type of the identifier attribute, plus what the underlying database supports.

If the identifier type is UUID, Hibernate is going to use a UUID identifier.

If the identifier type is numeric (e.g. Long, Integer), then Hibernate will use its SequenceStyleGenerator which resolves to a SEQUENCE generation if the underlying database supports sequences and a table-based generation otherwise.

3.7.9. Using sequences

For implementing database sequence-based identifier value generation Hibernate makes use of its org.hibernate.id.enhanced.SequenceStyleGenerator id generator. It is important to note that SequenceStyleGenerator is capable of working against databases that do not support sequences by transparently switching to a table as the underlying backing, which gives Hibernate a huge degree of portability across databases while still maintaining consistent id generation behavior (versus say choosing between SEQUENCE and IDENTITY).

Example 171. Implicit sequence
@Entity(name = "Product")
public static class Product {

	@Id
	@GeneratedValue( strategy = SEQUENCE )
	private Long id;

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

	//Getters and setters are omitted for brevity

}

Notice that the mapping does not specify the name of the sequence to use. In such cases, Hibernate will assume a sequence name based on the name of the table to which the entity is mapped. Here, since the entity is mapped to a table named product, Hibernate will use a sequence named product_seq.

When using @Subselect mappings, using the "table name" is not valid so Hibernate falls back to using the entity name as the base along with the _seq suffix.

To specify the sequence name explicitly, the simplest form is to specify @GeneratedValue#generator.

Example 172. Named sequence
@Entity(name = "Product")
public static class Product {

	@Id
	@GeneratedValue(
		strategy = SEQUENCE,
		generator = "explicit_product_sequence"
	)
	private Long id;

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

	//Getters and setters are omitted for brevity

}

For this mapping, Hibernate will use explicit_product_sequence as the name of the sequence.

For more advanced configuration, Jakarta Persistence defines the @SequenceGenerator annotation.

Example 173. Simple @SequenceGenerator
@Entity(name = "Product")
public static class Product {

	@Id
	@GeneratedValue(
		strategy = SEQUENCE,
		generator = "sequence-generator"
	)
	@SequenceGenerator(
		name = "sequence-generator",
		sequenceName = "explicit_product_sequence"
	)
	private Long id;

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

	//Getters and setters are omitted for brevity

}

This is simply a more verbose form of the mapping in Named sequence. However, the jakarta.persistence.SequenceGenerator annotation allows you to specify additional configurations as well.

Example 174. Sequence configuration
@Entity(name = "Product")
public static class Product {

	@Id
	@GeneratedValue(
		strategy = GenerationType.SEQUENCE,
		generator = "sequence-generator"
	)
	@SequenceGenerator(
		name = "sequence-generator",
		sequenceName = "explicit_product_sequence",
		allocationSize = 5
	)
	private Long id;

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

	//Getters and setters are omitted for brevity

}

Again the mapping specifies explicit_product_sequence as the physical sequence name, but it also specifies an explicit allocation-size ("increment by").

3.7.10. Using IDENTITY columns

For implementing identifier value generation based on IDENTITY columns, Hibernate makes use of its org.hibernate.id.IdentityGenerator id generator which expects the identifier to be generated by INSERT into the table. IdentityGenerator understands 3 different ways that the INSERT-generated value might be retrieved:

  • If Hibernate believes the JDBC environment supports java.sql.Statement#getGeneratedKeys, then that approach will be used for extracting the IDENTITY generated keys.

  • Otherwise, if Dialect#supportsInsertSelectIdentity reports true, Hibernate will use the Dialect specific INSERT+SELECT statement syntax.

  • Otherwise, Hibernate will expect that the database supports some form of asking for the most recently inserted IDENTITY value via a separate SQL command as indicated by Dialect#getIdentitySelectString.

It is important to realize that using IDENTITY columns imposes a runtime behavior where the entity row must be physically inserted prior to the identifier value being known.

This can mess up extended persistence contexts (long conversations). Because of the runtime imposition/inconsistency, Hibernate suggests other forms of identifier value generation be used (e.g. SEQUENCE) with extended contexts.

In Hibernate 5.3, Hibernate attempts to delay the insert of entities if the flush-mode does not equal AUTO. This was slightly problematic for entities that used IDENTITY or SEQUENCE generated identifiers that were also involved in some form of association with another entity in the same transaction.

In Hibernate 5.4, Hibernate attempts to remedy the problem using an algorithm to decide if the insert should be delayed or if it requires immediate insertion. We wanted to restore the behavior prior to 5.3 only for very specific use cases where it made sense.

Entity mappings can sometimes be complex and it is possible a corner case was overlooked. Hibernate offers a way to completely disable the 5.3 behavior in the event problems occur with DelayedPostInsertIdentifier. To enable the legacy behavior, set hibernate.id.disable_delayed_identity_inserts=true.

This configuration option is meant to act as a temporary fix and bridge the gap between the changes in this behavior across Hibernate 5.x releases. If this configuration setting is necessary for a mapping, please open a JIRA and report the mapping so that the algorithm can be reviewed.

There is yet another important runtime impact of choosing IDENTITY generation: Hibernate will not be able to batch INSERT statements for the entities using the IDENTITY generation.

The importance of this depends on the application-specific use cases. If the application is not usually creating many new instances of a given entity type using the IDENTITY generator, then this limitation will be less important since batching would not have been very helpful anyway.

3.7.11. Using the table identifier generator

Hibernate achieves table-based identifier generation based on its org.hibernate.id.enhanced.TableGenerator which defines a table capable of holding multiple named value segments for any number of entities.

The basic idea is that a given table-generator table (hibernate_sequences for example) can hold multiple segments of identifier generation values.

Example 175. Unnamed table generator
@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)
)

If no table name is given Hibernate assumes an implicit name of hibernate_sequences.

Additionally, because no jakarta.persistence.TableGenerator#pkColumnValue is specified, Hibernate will use the default segment (sequence_name='default') from the hibernate_sequences table.

However, you can configure the table identifier generator using the @TableGenerator annotation.

Example 176. Configured table generator
@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)
)

Now, when inserting 3 Product entities, Hibernate generates the following statements:

Example 177. Configured table generator persist example
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]

3.7.12. Using UUID generation

As mentioned above, Hibernate supports UUID identifier value generation. This is supported through its org.hibernate.id.UUIDGenerator id generator.

NOTE

org.hibernate.id.UUIDGenerator is an example of @IdGeneratorType discussed in Using @IdGeneratorType

UUIDGenerator supports pluggable strategies for exactly how the UUID is generated. These strategies are defined by the org.hibernate.id.UUIDGenerationStrategy contract. The default strategy is a version 4 (random) strategy according to IETF RFC 4122. Hibernate does ship with an alternative strategy which is a RFC 4122 version 1 (time-based) strategy (using IP address rather than mac address).

Example 178. Implicitly using the random UUID strategy
@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
}

To specify an alternative generation strategy, we’d have to define some configuration via @GenericGenerator. Here we choose the RFC 4122 version 1 compliant strategy named org.hibernate.id.uuid.CustomVersionOneStrategy.

Example 179. Implicitly using the random UUID strategy
@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
}

3.7.13. Optimizers

Most of the Hibernate generators that separately obtain identifier values from database structures support the use of pluggable optimizers. Optimizers help manage the number of times Hibernate has to talk to the database in order to generate identifier values. For example, with no optimizer applied to a sequence-generator, every time the application asked Hibernate to generate an identifier it would need to grab the next sequence value from the database. But if we can minimize the number of times we need to communicate with the database here, the application will be able to perform better, which is, in fact, the role of these optimizers.

none

No optimization is performed. We communicate with the database each and every time an identifier value is needed from the generator.

pooled-lo

The pooled-lo optimizer works on the principle that the increment-value is encoded into the database table/sequence structure. In sequence-terms, this means that the sequence is defined with a greater-than-1 increment size.

For example, consider a brand new sequence defined as create sequence m_sequence start with 1 increment by 20. This sequence essentially defines a "pool" of 20 usable id values each and every time we ask it for its next-value. The pooled-lo optimizer interprets the next-value as the low end of that pool.

So when we first ask it for next-value, we’d get 1. We then assume that the valid pool would be the values from 1-20 inclusive.

The next call to the sequence would result in 21, which would define 21-40 as the valid range. And so on. The "lo" part of the name indicates that the value from the database table/sequence is interpreted as the pool lo(w) end.

pooled

Just like pooled-lo, except that here the value from the table/sequence is interpreted as the high end of the value pool.

hilo; legacy-hilo

Define a custom algorithm for generating pools of values based on a single value from a table or sequence.

These optimizers are not recommended for use. They are maintained (and mentioned) here simply for use by legacy applications that used these strategies previously.

Applications can also implement and use their own optimizer strategies, as defined by the org.hibernate.id.enhanced.Optimizer contract.

3.7.14. Using @IdGeneratorType

@IdGeneratorType is a meta-annotation that allows the creation of custom annotations that support simple, concise and type-safe definition and configuration of custom org.hibernate.id.IdentifierGenerator implementations.

Example 180. @IdGeneratorType
public class CustomSequenceGenerator implements IdentifierGenerator {

	public CustomSequenceGenerator(
			Sequence config,
			Member annotatedMember,
			CustomIdGeneratorCreationContext context) {
		//...
	}

	@Override
	public Object generate(
			SharedSessionContractImplementor session,
			Object object) {
		//...
}

@IdGeneratorType( CustomSequenceGenerator.class )
@Target({METHOD, FIELD})
@Retention(RUNTIME)
public @interface Sequence {
	String name();
	int startWith() default 1;
	int incrementBy() default 50;
	Class<? extends Optimizer> optimizer() default Optimizer.class;
}

The example illustrates using @IdGeneratorType to define a custom sequence-based annotation @Sequence to apply and configure a custom IdentifierGenerator implementation CustomSequenceGenerator.

Notice the CustomSequenceGenerator constructor. Custom generator defined through @IdGeneratorType receive the following arguments:

  1. The configuration annotation - here, @Sequence. This is the type-safety aspect, rather than relying on untyped configuration properties in a Map, etc.

  2. The Member to which annotation was applied. This allows access to the Java type of the identifier attribute, etc.

  3. CustomIdGeneratorCreationContext is a "parameter object" providing access to things often useful for identifier generators.

3.7.15. Using @GenericGenerator

@GenericGenerator is generally considered deprecated in favor of @IdGeneratorType

@GenericGenerator allows integration of any Hibernate org.hibernate.id.IdentifierGenerator implementation, including any of the specific ones discussed here and any custom ones.

Example 181. Pooled-lo optimizer mapping using @GenericGenerator mapping
@Entity(name = "Product")
public static class Product {

	@Id
	@GeneratedValue(
		strategy = GenerationType.SEQUENCE,
		generator = "product_generator"
	)
	@GenericGenerator(
		name = "product_generator",
		type = org.hibernate.id.enhanced.SequenceStyleGenerator.class,
		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

}

Now, when saving 5 Person entities and flushing the Persistence Context after every 3 entities:

Example 182. Pooled-lo optimizer mapping using @GenericGenerator mapping
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]

As you can see from the list of generated SQL statements, you can insert 3 entities with just one database sequence call. This way, the pooled and the pooled-lo optimizers allow you to reduce the number of database round trips, therefore reducing the overall transaction response time.

3.7.16. Derived Identifiers

Java Persistence 2.0 added support for derived identifiers which allow an entity to borrow the identifier from a many-to-one or one-to-one association.

Example 183. Derived identifier with @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
}

In the example above, the PersonDetails entity uses the id column for both the entity identifier and for the one-to-one association to the Person entity. The value of the PersonDetails entity identifier is "derived" from the identifier of its parent Person entity.

Example 184. Derived identifier with @MapsId persist example
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());
});

The @MapsId annotation can also reference columns from an @EmbeddedId identifier as well.

The previous example can also be mapped using @PrimaryKeyJoinColumn.

Example 185. Derived identifier @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
}

Unlike @MapsId, the application developer is responsible for ensuring that the entity identifier and the many-to-one (or one-to-one) association are in sync, as you can see in the PersonDetails#setPerson method.

3.7.17. @RowId

If you annotate a given entity with the @RowId annotation and the underlying database supports fetching a record by ROWID (e.g. Oracle), then Hibernate can use the ROWID pseudo-column for CRUD operations.

Example 186. @RowId entity mapping
@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

}

Now, when fetching an entity and modifying it, Hibernate uses the ROWID pseudo-column for the UPDATE SQL statement.

Example 187. @RowId example
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]

3.8. Associations

Associations describe how two or more entities form a relationship based on a database joining semantics.

3.8.1. @ManyToOne

@ManyToOne is the most common association, having a direct equivalent in the relational database as well (e.g. foreign key), and so it establishes a relationship between a child entity and a parent.

Example 188. @ManyToOne association
@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

Each entity has a lifecycle of its own. Once the @ManyToOne association is set, Hibernate will set the associated database foreign key column.

Example 189. @ManyToOne association lifecycle
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

3.8.2. @OneToMany

The @OneToMany association links a parent entity with one or more child entities. If the @OneToMany doesn’t have a mirroring @ManyToOne association on the child side, the @OneToMany association is unidirectional. If there is a @ManyToOne association on the child side, the @OneToMany association is bidirectional and the application developer can navigate this relationship from both ends.

Unidirectional @OneToMany

When using a unidirectional @OneToMany association, Hibernate resorts to using a link table between the two joining entities.

Example 190. Unidirectional @OneToMany association
@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

The @OneToMany association is by definition a parent association, regardless of whether it’s a unidirectional or a bidirectional one. Only the parent side of an association makes sense to cascade its entity state transitions to children.

Example 191. Cascading @OneToMany association
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

When persisting the Person entity, the cascade will propagate the persist operation to the underlying Phone children as well. Upon removing a Phone from the phones collection, the association row is deleted from the link table, and the orphanRemoval attribute will trigger a Phone removal as well.

The unidirectional associations are not very efficient when it comes to removing child entities. In the example above, upon flushing the persistence context, Hibernate deletes all database rows from the link table (e.g. Person_Phone) that are associated with the parent Person entity and reinserts the ones that are still found in the @OneToMany collection.

On the other hand, a bidirectional @OneToMany association is much more efficient because the child entity controls the association.

Bidirectional @OneToMany

The bidirectional @OneToMany association also requires a @ManyToOne association on the child side. Although the Domain Model exposes two sides to navigate this association, behind the scenes, the relational database has only one foreign key for this relationship.

Every bidirectional association must have one owning side only (the child side), the other one being referred to as the inverse (or the mappedBy) side.

Example 192. @OneToMany association mappedBy the @ManyToOne side
@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

Whenever a bidirectional association is formed, the application developer must make sure both sides are in-sync at all times.

The addPhone() and removePhone() are utility methods that synchronize both ends whenever a child element is added or removed.

Because the Phone class has a @NaturalId column (the phone number being unique), the equals() and the hashCode() can make use of this property, and so the removePhone() logic is reduced to the remove() Java Collection method.

Example 193. Bidirectional @OneToMany with an owner @ManyToOne side lifecycle
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

Unlike the unidirectional @OneToMany, the bidirectional association is much more efficient when managing the collection persistence state. Every element removal only requires a single update (in which the foreign key column is set to NULL), and, if the child entity lifecycle is bound to its owning parent so that the child cannot exist without its parent, then we can annotate the association with the orphanRemoval attribute and dissociating the child will trigger a delete statement on the actual child table row as well.

3.8.3. @OneToOne

The @OneToOne association can either be unidirectional or bidirectional. A unidirectional association follows the relational database foreign key semantics, the client-side owning the relationship. A bidirectional association features a mappedBy @OneToOne parent side too.

Unidirectional @OneToOne
Example 194. Unidirectional @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

From a relational database point of view, the underlying schema is identical to the unidirectional @ManyToOne association, as the client-side controls the relationship based on the foreign key column.

But then, it’s unusual to consider the Phone as a client-side and the PhoneDetails as the parent-side because the details cannot exist without an actual phone. A much more natural mapping would be the Phone were the parent-side, therefore pushing the foreign key into the PhoneDetails table. This mapping requires a bidirectional @OneToOne association as you can see in the following example:

Bidirectional @OneToOne
Example 195. Bidirectional @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

This time, the PhoneDetails owns the association, and, like any bidirectional association, the parent-side can propagate its lifecycle to the child-side through cascading.

Example 196. Bidirectional @OneToOne lifecycle
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 )

When using a bidirectional @OneToOne association, Hibernate enforces the unique constraint upon fetching the child-side. If there are more than one children associated with the same parent, Hibernate will throw a org.hibernate.exception.ConstraintViolationException. Continuing the previous example, when adding another PhoneDetails, Hibernate validates the uniqueness constraint when reloading the Phone object.

Example 197. Bidirectional @OneToOne unique constraint
PhoneDetails otherDetails = new PhoneDetails("T-Mobile", "CDMA");
otherDetails.setPhone(phone);
entityManager.persist(otherDetails);
entityManager.flush();
entityManager.clear();

//throws jakarta.persistence.PersistenceException: org.hibernate.HibernateException: More than one row with the given identifier was found: 1
phone = entityManager.find(Phone.class, phone.getId());
Bidirectional @OneToOne lazy association

Although you might annotate the parent-side association to be fetched lazily, Hibernate cannot honor this request since it cannot know whether the association is null or not.

The only way to figure out whether there is an associated record on the child side is to fetch the child association using a secondary query. Because this can lead to N+1 query issues, it’s much more efficient to use unidirectional @OneToOne associations with the @MapsId annotation in place.

However, if you really need to use a bidirectional association and want to make sure that this is always going to be fetched lazily, then you need to enable lazy state initialization bytecode enhancement.

Example 198. Bidirectional @OneToOne lazy parent-side association
@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

}

For more about how to enable Bytecode enhancement, see the Bytecode Enhancement chapter.

3.8.4. @ManyToMany

The @ManyToMany association requires a link table that joins two entities. Like the @OneToMany association, @ManyToMany can be either unidirectional or bidirectional.

Unidirectional @ManyToMany
Example 199. Unidirectional @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

Just like with unidirectional @OneToMany associations, the link table is controlled by the owning side.

When an entity is removed from the @ManyToMany collection, Hibernate simply deletes the joining record in the link table. Unfortunately, this operation requires removing all entries associated with a given parent and recreating the ones that are listed in the current running persistent context.

Example 200. Unidirectional @ManyToMany lifecycle
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 )

For @ManyToMany associations, the REMOVE entity state transition doesn’t make sense to be cascaded because it will propagate beyond the link table. Since the other side might be referenced by other entities on the parent-side, the automatic removal might end up in a ConstraintViolationException.

For example, if @ManyToMany(cascade = CascadeType.ALL) was defined and the first person would be deleted, Hibernate would throw an exception because another person is still associated with the address that’s being deleted.

Person person1 = entityManager.find(Person.class, personId);
entityManager.remove(person1);

Caused by: jakarta.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

By simply removing the parent-side, Hibernate can safely remove the associated link records as you can see in the following example:

Example 201. Unidirectional @ManyToMany entity removal
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

A bidirectional @ManyToMany association has an owning and a mappedBy side. To preserve synchronicity between both sides, it’s good practice to provide helper methods for adding or removing child entities.

Example 202. Bidirectional @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

With the helper methods in place, the synchronicity management can be simplified, as you can see in the following example:

Example 203. Bidirectional @ManyToMany lifecycle
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 )

If a bidirectional @OneToMany association performs better when removing or changing the order of child elements, the @ManyToMany relationship cannot benefit from such an optimization because the foreign key side is not in control. To overcome this limitation, the link table must be directly exposed and the @ManyToMany association split into two bidirectional @OneToMany relationships.

To most natural @ManyToMany association follows the same logic employed by the database schema, and the link table has an associated entity which controls the relationship for both sides that need to be joined.

Both the Person and the Address have a mappedBy @OneToMany side, while the PersonAddress owns the person and the address @ManyToOne associations. Because this mapping is formed out of two bidirectional associations, the helper methods are even more relevant.

The aforementioned example uses a Hibernate-specific mapping for the link entity since Jakarta Persistence doesn’t allow building a composite identifier out of multiple @ManyToOne associations.

For more details, see the composite identifiers with associations section.

The entity state transitions are better managed than in the previous bidirectional @ManyToMany case.

There is only one delete statement executed because, this time, the association is controlled by the @ManyToOne side which only has to monitor the state of the underlying foreign key relationship to trigger the right DML statement.

3.8.5. @NotFound

When dealing with associations which are not enforced by a physical foreign-key, it is possible for a non-null foreign-key value to point to a non-existent value on the associated entity’s table.

Not enforcing physical foreign-keys at the database level is highly discouraged.

Hibernate provides support for such models using the @NotFound annotation, which accepts a NotFoundAction value which indicates how Hibernate should behave when such broken foreign-keys are encountered -

EXCEPTION

(default) Hibernate will throw an exception (FetchNotFoundException)

IGNORE

the association will be treated as null

Both @NotFound(IGNORE) and @NotFound(EXCEPTION) cause Hibernate to assume that there is no physical foreign-key.

@ManyToOne and @OneToOne associations annotated with @NotFound are always fetched eagerly even if the fetch strategy is set to FetchType.LAZY.

If the application itself manages the referential integrity and can guarantee that there are no broken foreign-keys, jakarta.persistence.ForeignKey(NO_CONSTRAINT) can be used instead. This will force Hibernate to not export physical foreign-keys, but still behave as if there is in terms of avoiding the downsides to @NotFound.

Considering the following City and Person entity mappings:

Example 206. @NotFound mapping example
@Entity(name = "Person")
@Table(name = "Person")
public static class Person {

	@Id
	private Integer id;
	private String name;

	@ManyToOne
	@NotFound(action = NotFoundAction.IGNORE)
	@JoinColumn(name = "city_fk", referencedColumnName = "id")
	private City city;

	//Getters and setters are omitted for brevity

}

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

	@Id
	private Integer id;

	private String name;

	//Getters and setters are omitted for brevity

}

If we have the following entities in our database:

Example 207. @NotFound persist example
City newYork = new City( 1, "New York" );
entityManager.persist( newYork );

Person person = new Person( 1, "John Doe", newYork );
entityManager.persist( person );

When loading the Person entity, Hibernate is able to locate the associated City parent entity:

Example 208. @NotFound - find existing entity example
Person person = entityManager.find( Person.class, 1 );
assertEquals( "New York", person.getCity().getName() );

However, if we break the foreign-key:

Example 209. Break the foreign-key
// the database allows this because there is no physical foreign-key
entityManager.createQuery( "delete City" ).executeUpdate();

Hibernate is not going to throw any exception, and it will assign a value of null for the non-existing City entity reference:

Example 210. @NotFound - find non-existing City example
Person person = entityManager.find( Person.class, 1 );

assertNull( person.getCity(), "person.getCity() should be null" );

@NotFound also affects how the association is treated as "implicit joins" in HQL and Criteria. When there is a physical foreign-key, Hibernate can safely assume that the value in the foreign-key’s key-column(s) will match the value in the target-column(s) because the database makes sure that is the case. However, @NotFound forces Hibernate to perform a physical join for implicit joins when it might not be needed otherwise.

Using the Person / City model, consider the query from Person p where p.city.id is null.

Normally Hibernate would not need the join between the Person table and the City table because a physical foreign-key would ensure that any non-null value in the Person.cityName column has a matching non-null value in the City.name column.

However, with @NotFound mappings it is possible to have a broken association because there is no physical foreign-key enforcing the relation. As seen in Break the foreign-key, the Person.cityName column for John Doe has been changed from "New York" to "Atlantis" even though there is no City in the database named "Atlantis". Hibernate is not able to trust the referring foreign-key value ("Atlantis") has a matching target value, so it must join to the City table to resolve the city.id value.

Example 211. Implicit join example
final List<Person> nullResults = entityManager
		.createQuery( "from Person p where p.city.id is null", Person.class )
		.getResultList();
assertThat( nullResults ).isEmpty();

final List<Person> nonNullResults = entityManager
		.createQuery( "from Person p where p.city.id is not null", Person.class )
		.getResultList();
assertThat( nonNullResults ).isEmpty();

Neither result includes a match for "John Doe" because the inner-join filters out that row.

Hibernate does support a means to refer specifically to the key column (Person.cityName) in a query using the special fk(..) function. E.g.

Example 212. Implicit join example
final List<String> nullResults = entityManager
		.createQuery( "select p.name from Person p where fk( p.city ) is null", String.class )
		.getResultList();

assertThat( nullResults ).isEmpty();

final List<String> nonNullResults = entityManager
		.createQuery( "select p.name from Person p where fk( p.city ) is not null", String.class )
		.getResultList();
assertThat( nonNullResults ).hasSize( 1 );
assertThat( nonNullResults.get( 0 ) ).isEqualTo( "John Doe" );

3.8.6. @Any mapping

The @Any mapping is useful to emulate a unidirectional @ManyToOne association when there can be multiple target entities.

Because the @Any mapping defines a polymorphic association to classes from multiple tables, this association type requires the FK column which provides the associated parent identifier and a metadata information for the associated entity type.

This is not the usual way of mapping polymorphic associations and you should use this only in special cases (e.g. audit logs, user session data, etc).

To map such an association, Hibernate needs to understand 3 things:

  1. The column and mapping for the discriminator

  2. The column and mapping for the key

  3. The mapping between discriminator values and entity classes

The discriminator

The discriminator of an any-style association holds the value that indicates which entity is referred to by a row.

Its "column" can be specified with either @Column or @Formula. The mapping type can be influenced by any of:

  1. @AnyDiscriminator allows re-using the DiscriminatorType simplified mappings from Jakarta Persistence for the common cases

  2. @JavaType

  3. @JdbcType

  4. @JdbcTypeCode

The key

The key of an any-style association holds the matching key for the row

Its "column" can be specified with either @JoinColumn (@JoinFormula not supported). The mapping type can be influenced by any of:

  1. @AnyKeyJavaClass

  2. @AnyKeyJavaType

  3. @AnyKeyJdbcType

  4. @AnyKeyJdbcTypeCode

The discriminator value mappings

@AnyDiscriminatorValue is used to map the discriminator values to the corresponding entity classes

3.8.7. Example using @Any mapping

For this example, consider the following Property class hierarchy:

Example 213. Property class hierarchy
public interface Property<T> {

    String getName();

    T getValue();
}


@Entity
@Table(name="integer_property")
public class IntegerProperty implements Property<Integer> {

    @Id
    private Long id;

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

    @Column(name = "`value`")
    private Integer value;

    @Override
    public String getName() {
        return name;
    }

    @Override
    public Integer getValue() {
        return value;
    }

    //Getters and setters omitted for brevity
}


@Entity
@Table(name="string_property")
public class StringProperty implements Property<String> {

    @Id
    private Long id;

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

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

    @Override
    public String getName() {
        return name;
    }

    @Override
    public String getValue() {
        return value;
    }

    //Getters and setters omitted for brevity
}

A PropertyHolder entity defines an attribute of type Property:

Example 214. @Any mapping usage
@Entity
@Table(name = "property_holder")
public class PropertyHolder {

    @Id
    private Long id;

    @Any
    @AnyDiscriminator(DiscriminatorType.STRING)
    @AnyDiscriminatorValue(discriminator = "S", entity = StringProperty.class)
    @AnyDiscriminatorValue(discriminator = "I", entity = IntegerProperty.class)
    @AnyKeyJavaClass(Long.class)
    @Column(name = "property_type")
    @JoinColumn(name = "property_id")
    private Property property;

    //Getters and setters are omitted for brevity

}
CREATE TABLE property_holder (
    id BIGINT NOT NULL,
    property_type VARCHAR(255),
    property_id BIGINT,
    PRIMARY KEY ( id )
)

PropertyHolder#property can refer to either StringProperty or IntegerProperty references, as indicated by the associated discriminator according to the @DiscriminatorValue annotations.

As you can see, there are two columns used to reference a Property instance: property_id and property_type. The property_id is used to match the id column of either the string_property or integer_property tables, while the property_type is used to match the string_property or the integer_property table.

To see the @Any annotation in action, consider the next examples.

If we persist an IntegerProperty as well as a StringProperty entity, and associate the StringProperty entity with a PropertyHolder, Hibernate will generate the following SQL queries:

Example 215. @Any mapping persist example
IntegerProperty ageProperty = new IntegerProperty();
ageProperty.setId(1L);
ageProperty.setName("age");
ageProperty.setValue(23);

session.persist(ageProperty);

StringProperty nameProperty = new StringProperty();
nameProperty.setId(1L);
nameProperty.setName("name");
nameProperty.setValue("John Doe");

session.persist(nameProperty);

PropertyHolder namePropertyHolder = new PropertyHolder();
namePropertyHolder.setId(1L);
namePropertyHolder.setProperty(nameProperty);

session.persist(namePropertyHolder);
INSERT INTO integer_property
       ( "name", "value", id )
VALUES ( 'age', 23, 1 )

INSERT INTO string_property
       ( "name", "value", id )
VALUES ( 'name', 'John Doe', 1 )

INSERT INTO property_holder
       ( property_type, property_id, id )
VALUES ( 'S', 1, 1 )

When fetching the PropertyHolder entity and navigating its property association, Hibernate will fetch the associated StringProperty entity like this:

Example 216. @Any mapping query example
PropertyHolder propertyHolder = session.get(PropertyHolder.class, 1L);

assertEquals("name", propertyHolder.getProperty().getName());
assertEquals("John Doe", propertyHolder.getProperty().getValue());
SELECT ph.id AS id1_1_0_,
       ph.property_type AS property2_1_0_,
       ph.property_id AS property3_1_0_
FROM   property_holder ph
WHERE  ph.id = 1


SELECT sp.id AS id1_2_0_,
       sp."name" AS name2_2_0_,
       sp."value" AS value3_2_0_
FROM   string_property sp
WHERE  sp.id = 1
Using meta-annotations

As mentioned in Mapping basic values, Hibernate’s ANY-related annotations can be composed using meta-annotations to re-use ANY mapping details.

Looking back at @Any mapping usage, we can see how cumbersome it would be to duplicate that information every time Property is mapped in the domain model. This description can also be moved into a single annotation that we can apply in each usage.

Example 217. @Any mapping usage
@Entity
@Table(name = "property_holder2")
public class PropertyHolder2 {

    @Id
    private Long id;

    @Any
    @PropertyDiscriminationDef
    @Column(name = "property_type")
    @JoinColumn(name = "property_id")
    private Property property;

    //Getters and setters are omitted for brevity

}

Though the mapping has been "simplified", the mapping works exactly as shown in @Any mapping usage.

@ManyToAny mapping

While the @Any mapping is useful to emulate a @ManyToOne association when there can be multiple target entities, to emulate a @OneToMany association, the @ManyToAny annotation must be used.

The mapping details are the same between @Any and @ManyToAny except for:

  1. The use of @ManyToAny instead of @Any

  2. The use of @JoinTable, @JoinTable#joinColumns and @JoinTable#inverseJoinColumns instead of just @JoinColumn

In the following example, the PropertyRepository entity has a collection of Property entities.

The repository_properties link table holds the associations between PropertyRepository and Property entities.

Example 218. @ManyToAny mapping usage
@Entity
@Table(name = "property_repository")
public class PropertyRepository {

    @Id
    private Long id;

    @ManyToAny
    @AnyDiscriminator(DiscriminatorType.STRING)
    @Column(name = "property_type")
    @AnyKeyJavaClass(Long.class)
    @AnyDiscriminatorValue(discriminator = "S", entity = StringProperty.class)
    @AnyDiscriminatorValue(discriminator = "I", entity = IntegerProperty.class)
    @Cascade(ALL)
    @JoinTable(name = "repository_properties",
            joinColumns = @JoinColumn(name = "repository_id"),
            inverseJoinColumns = @JoinColumn(name = "property_id")
   )
    private List<Property<?>> properties = new ArrayList<>();

    //Getters and setters are omitted for brevity

}
CREATE TABLE property_repository (
    id BIGINT NOT NULL,
    PRIMARY KEY ( id )
)

CREATE TABLE repository_properties (
    repository_id BIGINT NOT NULL,
    property_type VARCHAR(255),
    property_id BIGINT NOT NULL
)

To see the @ManyToAny annotation in action, consider the next examples.

If we persist an IntegerProperty as well as a StringProperty entity, and associate both of them with a PropertyRepository parent entity, Hibernate will generate the following SQL queries:

Example 219. @ManyToAny mapping persist example
IntegerProperty ageProperty = new IntegerProperty();
ageProperty.setId(1L);
ageProperty.setName("age");
ageProperty.setValue(23);

session.persist(ageProperty);

StringProperty nameProperty = new StringProperty();
nameProperty.setId(1L);
nameProperty.setName("name");
nameProperty.setValue("John Doe");

session.persist(nameProperty);

PropertyRepository propertyRepository = new PropertyRepository();
propertyRepository.setId(1L);

propertyRepository.getProperties().add(ageProperty);
propertyRepository.getProperties().add(nameProperty);

session.persist(propertyRepository);
INSERT INTO integer_property
       ( "name", "value", id )
VALUES ( 'age', 23, 1 )

INSERT INTO string_property
       ( "name", "value", id )
VALUES ( 'name', 'John Doe', 1 )

INSERT INTO property_repository ( id )
VALUES ( 1 )

INSERT INTO repository_properties
    ( repository_id , property_type , property_id )
VALUES
    ( 1 , 'I' , 1 )

When fetching the PropertyRepository entity and navigating its properties association, Hibernate will fetch the associated IntegerProperty and StringProperty entities like this:

Example 220. @ManyToAny mapping query example
PropertyRepository propertyRepository = session.get(PropertyRepository.class, 1L);

assertEquals(2, propertyRepository.getProperties().size());

for(Property property : propertyRepository.getProperties()) {
    assertNotNull(property.getValue());
}
SELECT pr.id AS id1_1_0_
FROM   property_repository pr
WHERE  pr.id = 1

SELECT ip.id AS id1_0_0_ ,
       ip."name" AS name2_0_0_ ,
       ip."value" AS value3_0_0_
FROM   integer_property ip
WHERE  ip.id = 1

SELECT sp.id AS id1_3_0_ ,
       sp."name" AS name2_3_0_ ,
       sp."value" AS value3_3_0_
FROM   string_property sp
WHERE  sp.id = 1

3.8.8. @JoinFormula mapping

The @JoinFormula annotation is used to customize the join between a child Foreign Key and a parent row Primary Key.

Example 221. @JoinFormula mapping usage
@Entity(name = "User")
@Table(name = "users")
public static class User {

	@Id
	private Long id;

	private String firstName;

	private String lastName;

	private String phoneNumber;

	@ManyToOne
	@JoinFormula("REGEXP_REPLACE(phoneNumber, '\\+(\\d+)-.*', '\\1')::int")
	private Country country;

	//Getters and setters omitted for brevity

}

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

	@Id
	private Integer id;

	private String name;

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

}
CREATE TABLE countries (
    id int4 NOT NULL,
    name VARCHAR(255),
    PRIMARY KEY ( id )
)

CREATE TABLE users (
    id int8 NOT NULL,
    firstName VARCHAR(255),
    lastName VARCHAR(255),
    phoneNumber VARCHAR(255),
    PRIMARY KEY ( id )
)

The country association in the User entity is mapped by the country identifier provided by the phoneNumber property.

Considering we have the following entities:

Example 222. @JoinFormula mapping usage
Country US = new Country();
US.setId(1);
US.setName("United States");

Country Romania = new Country();
Romania.setId(40);
Romania.setName("Romania");

doInJPA(this::entityManagerFactory, entityManager -> {
	entityManager.persist(US);
	entityManager.persist(Romania);
});

doInJPA(this::entityManagerFactory, entityManager -> {
	User user1 = new User();
	user1.setId(1L);
	user1.setFirstName("John");
	user1.setLastName("Doe");
	user1.setPhoneNumber("+1-234-5678");
	entityManager.persist(user1);

	User user2 = new User();
	user2.setId(2L);
	user2.setFirstName("Vlad");
	user2.setLastName("Mihalcea");
	user2.setPhoneNumber("+40-123-4567");
	entityManager.persist(user2);
});

When fetching the User entities, the country property is mapped by the @JoinFormula expression:

Example 223. @JoinFormula mapping usage
doInJPA(this::entityManagerFactory, entityManager -> {
	log.info("Fetch User entities");

	User john = entityManager.find(User.class, 1L);
	assertEquals(US, john.getCountry());

	User vlad = entityManager.find(User.class, 2L);
	assertEquals(Romania, vlad.getCountry());
});
-- Fetch User entities

SELECT
    u.id as id1_1_0_,
    u.firstName as firstNam2_1_0_,
    u.lastName as lastName3_1_0_,
    u.phoneNumber as phoneNum4_1_0_,
    REGEXP_REPLACE(u.phoneNumber, '\+(\d+)-.*', '\1')::int as formula1_0_,
    c.id as id1_0_1_,
    c.name as name2_0_1_
FROM
    users u
LEFT OUTER JOIN
    countries c
        ON REGEXP_REPLACE(u.phoneNumber, '\+(\d+)-.*', '\1')::int = c.id
WHERE
    u.id=?

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

SELECT
    u.id as id1_1_0_,
    u.firstName as firstNam2_1_0_,
    u.lastName as lastName3_1_0_,
    u.phoneNumber as phoneNum4_1_0_,
    REGEXP_REPLACE(u.phoneNumber, '\+(\d+)-.*', '\1')::int as formula1_0_,
    c.id as id1_0_1_,
    c.name as name2_0_1_
FROM
    users u
LEFT OUTER JOIN
    countries c
        ON REGEXP_REPLACE(u.phoneNumber, '\+(\d+)-.*', '\1')::int = c.id
WHERE
    u.id=?

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

Therefore, the @JoinFormula annotation is used to define a custom join association between the parent-child association.

3.8.9. @JoinColumnOrFormula mapping

The @JoinColumnOrFormula annotation is used to customize the join between a child Foreign Key and a parent row Primary Key when we need to take into consideration a column value as well as a @JoinFormula.

Example 224. @JoinColumnOrFormula mapping usage
@Entity(name = "User")
@Table(name = "users")
public static class User {

	@Id
	private Long id;

	private String firstName;

	private String lastName;

	private String language;

	@ManyToOne
	@JoinColumnOrFormula(column =
		@JoinColumn(
			name = "language",
			referencedColumnName = "primaryLanguage",
			insertable = false,
			updatable = false
		)
	)
	@JoinColumnOrFormula(formula =
		@JoinFormula(
			value = "true",
			referencedColumnName = "is_default"
		)
	)
	private Country country;

	//Getters and setters omitted for brevity

}

@Entity(name = "Country")
@Table(name = "countries")
public static class Country implements Serializable {

	@Id
	private Integer id;

	private String name;

	private String primaryLanguage;

	@Column(name = "is_default")
	private boolean _default;

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

}
CREATE TABLE countries (
    id INTEGER NOT NULL,
    is_default boolean,
    name VARCHAR(255),
    primaryLanguage VARCHAR(255),
    PRIMARY KEY ( id )
)

CREATE TABLE users (
    id BIGINT NOT NULL,
    firstName VARCHAR(255),
    language VARCHAR(255),
    lastName VARCHAR(255),
    PRIMARY KEY ( id )
)

The country association in the User entity is mapped by the language property value and the associated Country is_default column value.

Considering we have the following entities:

Example 225. @JoinColumnOrFormula persist example
Country US = new Country();
US.setId(1);
US.setDefault(true);
US.setPrimaryLanguage("English");
US.setName("United States");

Country Romania = new Country();
Romania.setId(40);
Romania.setDefault(true);
Romania.setName("Romania");
Romania.setPrimaryLanguage("Romanian");

doInJPA(this::entityManagerFactory, entityManager -> {
	entityManager.persist(US);
	entityManager.persist(Romania);
});

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

	User user2 = new User();
	user2.setId(2L);
	user2.setFirstName("Vlad");
	user2.setLastName("Mihalcea");
	user2.setLanguage("Romanian");
	entityManager.persist(user2);

});

When fetching the User entities, the country property is mapped by the @JoinColumnOrFormula expression:

Example 226. @JoinColumnOrFormula fetching example
doInJPA(this::entityManagerFactory, entityManager -> {
	log.info("Fetch User entities");

	User john = entityManager.find(User.class, 1L);
	assertEquals(US, john.getCountry());

	User vlad = entityManager.find(User.class, 2L);
	assertEquals(Romania, vlad.getCountry());
});
SELECT
    u.id as id1_1_0_,
    u.language as language3_1_0_,
    u.firstName as firstNam2_1_0_,
    u.lastName as lastName4_1_0_,
    1 as formula1_0_,
    c.id as id1_0_1_,
    c.is_default as is_defau2_0_1_,
    c.name as name3_0_1_,
    c.primaryLanguage as primaryL4_0_1_
FROM
    users u
LEFT OUTER JOIN
    countries c
        ON u.language = c.primaryLanguage
        AND 1 = c.is_default
WHERE
    u.id = ?
        
-- binding parameter [1] as [BIGINT] - [1]

SELECT
    u.id as id1_1_0_,
    u.language as language3_1_0_,
    u.firstName as firstNam2_1_0_,
    u.lastName as lastName4_1_0_,
    1 as formula1_0_,
    c.id as id1_0_1_,
    c.is_default as is_defau2_0_1_,
    c.name as name3_0_1_,
    c.primaryLanguage as primaryL4_0_1_
FROM
    users u
LEFT OUTER JOIN
    countries c
        ON u.language = c.primaryLanguage
        AND 1 = c.is_default
WHERE
    u.id = ?

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

Therefore, the @JoinColumnOrFormula annotation is used to define a custom join association between the parent-child association.

3.9. Collections

Hibernate supports mapping collections (java.util.Collection and java.util.Map subtypes) in a variety of ways.

Hibernate even allows mapping a collection as @Basic, but that should generally be avoided. See Collections as basic value type for details of such a mapping.

This section is limited to discussing @ElementCollection, @OneToMany and @ManyToMany.

Two entities cannot share a reference to the same collection instance.

Collection-valued properties do not support null value semantics.

Collections cannot be nested, meaning Hibernate does not support mapping List<List<?>>, for example.

Embeddables which are used as a collection element, Map value or Map key may not themselves define collections

3.9.1. Collection Semantics

The semantics of a collection describes how to handle the collection, including

  • the collection subtype to use - java.util.List, java.util.Set, java.util.SortedSet, etc.

  • how to access elements of the collection

  • how to create instances of the collection - both "raw" and "wrapper" forms.

Hibernate supports the following semantics:

ARRAY

Object and primitive arrays. See Mapping Arrays.

BAG

A collection that may contain duplicate entries and has no defined ordering. See Mapping Collections.

ID_BAG

A bag that defines a per-element identifier to uniquely identify elements in the collection. See Mapping Collections.

LIST

Follows the semantics defined by java.util.List. See Ordered Lists.

SET

Follows the semantics defined by java.util.Set. See Mapping Sets.

ORDERED_SET

A set that is ordered by a SQL fragment defined on its mapping. See Mapping Sets.

SORTED_SET

A set that is sorted according to a Comparator defined on its mapping. See Mapping Sets.

MAP

Follows the semantics defined by java.util.Map. See Mapping Maps.

ORDERED_MAP

A map that is ordered by keys according to a SQL fragment defined on its mapping. See Mapping Maps.

SORTED_MAP

A map that is sorted by keys according to a Comparator defined on its mapping. See Mapping Maps.

By default, Hibernate interprets the defined type of the plural attribute and makes an interpretation as to which classification it fits in to, using the following checks:

  1. if an array → ARRAY

  2. if a List → LIST

  3. if a SortedSet → SORTED_SET

  4. if a Set → SET

  5. if a SortedMap → SORTED_MAP

  6. if a Map → MAP

  7. else Collection → BAG

3.9.2. Mapping Lists

java.util.List defines a collection of ordered, non-unique elements.

Example 227. Basic List Mapping
@Entity
public class EntityWithList {
	// ...
	@ElementCollection
	private List<Name> names;
}

Contrary to natural expectations, the ordering of a list is by default not maintained. To maintain the order, it is necessary to explicitly use the jakarta.persistence.OrderColumn annotation.

Starting in 6.0, Hibernate allows to configure the default semantics of List without @OrderColumn via the hibernate.mapping.default_list_semantics setting. To switch to the more natural LIST semantics with an implicit order-column, set the setting to LIST. Beware that default LIST semantics only affects owned collection mappings. Unowned mappings like @ManyToMany(mappedBy = "…​") and @OneToMany(mappedBy = "…​") do not retain the element order by default, and explicitly annotating @OrderColumn for @ManyToMany(mappedBy = "…​") mappings is illegal.

To retain the order of elements of a @OneToMany(mappedBy = "…​") the @OrderColumn annotation must be applied explicitly. In addition to that, it is important that both sides of the relationship, the @OneToMany(mappedBy = "…​") and the @ManyToOne, must be kept in sync. Otherwise, the element position will not be updated accordingly.

The default column name that stores the index is derived from the attribute name, by suffixing _ORDER.

Example 228. @OrderColumn
@Entity
public class EntityWithOrderColumnList {
	// ...
	@ElementCollection
	@OrderColumn( name = "name_index" )
	private List<Name> names;
}

Now, a column named name_index will be used.

Hibernate stores index values into the order-column based on the element’s position in the list with no adjustment. The element at names[0] is stored with name_index=0 and so on. That is to say that the list index is considered 0-based just as list indexes themselves are 0-based. Some legacy schemas might map the position as 1-based, or any base really. Hibernate also defines support for such cases using its @ListIndexBase annotation.

Example 229. @ListIndexBase
@Entity
public class EntityWithIndexBasedList {
	// ...
	@ElementCollection
	@OrderColumn(name = "name_index")
	@ListIndexBase(1)
	private List<Name> names;
}

3.9.3. Mapping Sets

java.util.Set defines a collection of unique, though unordered elements. Hibernate supports mapping sets according to the requirements of the java.util.Set.

Example 230. Basic Set Mapping
@Entity
public class EntityWithSet {
	// ...
	@ElementCollection
	private Set<Name> names;
}

Hibernate also has the ability to map sorted and ordered sets. A sorted set orders its elements in memory via an associated Comparator; an ordered set is ordered via SQL when the set is loaded.

TIP

An ordered set does not perform any sorting in-memory. If an element is added after the collection is loaded, the collection would need to be refreshed to re-order the elements. For this reason, ordered sets are not recommended - if the application needs ordering of the set elements, a sorted set should be preferred. For this reason, it is not covered in the User Guide. See the javadocs for jakarta.persistence.OrderBy or org.hibernate.annotations.OrderBy for details.

There are 2 options for sorting a set - naturally or using an explicit comparator.

A set is naturally sorted using the natural sort comparator for its elements. Generally this implies that the element type is Comparable. E.g.

Example 231. @SortNatural
@Embeddable
@Access( AccessType.FIELD )
public class Name implements Comparable<Name> {
	private String first;
	private String last;

	// ...
}

@Entity
public class EntityWithNaturallySortedSet {
	// ...
	@ElementCollection
	@SortNatural
	private SortedSet<Name> names;
}

Because Name is defined as Comparable, its #compare method will be used to sort the elements in this set.

But Hibernate also allows sorting based on a specific Comparator implementation. Here, e.g., we map the Names as sorted by a NameComparator:

Example 232. @SortComparator
public class NameComparator implements Comparator<Name> {
	static final Comparator<Name> comparator = Comparator.comparing( Name::getLast ).thenComparing( Name::getFirst );

	@Override
	public int compare(Name o1, Name o2) {
		return comparator.compare( o1, o2 );
	}
}

@Entity
public class EntityWithSortedSet {
	// ...
	@ElementCollection
	@SortComparator( NameComparator.class )
	private SortedSet<Name> names;
}

Here, instead of Name#compare being use for the sorting, the explicit NameComparator will be used instead.

3.9.4. Mapping Maps

A java.util.Map is a collection of key/value pairs.

Example 233. Simple MAP mapping
@Entity
public class EntityWithMap {
	// ...
	@ElementCollection
	private Map<Name, Status> names;
}

Hibernate has the ability to map sorted and ordered maps - the ordering and sorting applies to the Map key. As we saw with Sets, the use of ordered Maps is generally discouraged.

Maps may be sorted naturally -

Example 234. Naturally sorted MAP mapping
@Entity
public class EntityWithNaturallySortedMap {
	// ...
	@ElementCollection
	@SortNatural
	private Map<Name, Status> names;
}

or via a Comparator -

Example 235. Comparator sorted MAP mapping
@Entity
public class EntityWithSortedMap {
	// ...
	@ElementCollection
	@SortComparator( NameComparator.class )
	private Map<Name, Status> names;
}

3.9.5. Mapping Collections

Without any other mapping influencers, java.util.Collection is interpreted using BAG semantics which means a collection that may contain duplicate entries and has no defined ordering.

Jakarta Persistence does not define support for BAG (nor ID_BAG) classification per-se. The specification does allow mapping of java.util.Collection attributes, but how such attributes are handled is largely undefined.

Example 236. Simple BAG mapping
@Entity
public class EntityWithBagAsCollection {
	// ..
	@ElementCollection
	private Collection<Name> names;
}

Some apps map BAG collections using java.util.List instead. Hibernate provides 2 ways to handle lists as bags. First an explicit annotation

Example 237. @Bag
@Entity
public class EntityWithBagAsList {
	// ..
	@ElementCollection
	@Bag
	private List<Name> names;
}

Specifically, the usage of @Bag forces the classification as BAG. Even though the names attribute is defined as List, Hibernate will treat it using the BAG semantics.

Additionally, as discussed in Mapping Lists, the hibernate.mapping.default_list_semantics setting is available to have Hibernate interpret a List with no @OrderColumn and no @ListIndexBase as a BAG.

An ID_BAG is similar to a BAG, except that it maps a generated, per-row identifier into the collection table. @CollectionId is the annotation to configure this identifier

3.9.6. Mapping Arrays

Hibernate is able to map Object and primitive arrays as collections. Mapping an array is essentially the same as mapping a list.

There is a major limitation of mapping arrays to be aware of - the array cannot be lazy using wrappers. It can, however, be lazy via bytecode enhancement of its owner.

Note that Jakarta Persistence does not define support for arrays as plural attributes; according to the specification, these would be mapped as binary data.

3.9.7. @ElementCollection

Element collections may contain values of either basic or embeddable types. They have a similar lifecycle to basic/embedded attributes in that their persistence is completely managed as part of the owner - they are created when referenced from an owner and automatically deleted when unreferenced. The specifics of how this lifecycle manifests in terms of database calls depends on the semantics of the mapping.

This section will discuss these lifecycle aspects using the example of mapping a collection of phone numbers. The examples use embeddable values, but the same aspects apply to collections of basic values as well.

The embeddable used in the examples is a PhoneNumber -

Example 238. PhoneNumber
@Embeddable
public class Phone {

	private String type;

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

	//Getters and setters are omitted for brevity

}

First, a BAG mapping -

Example 239. Elemental BAG mapping
@Entity(name = "Person")
public static class Person {

	@Id
	private Integer id;

	@ElementCollection
	private Collection<String> phones = new ArrayList<>();

	//Getters and setters are omitted for brevity

}
Example 240. Elemental BAG lifecycle
// Clear element collection and add element
person.getPhones().clear();
person.getPhones().add( "123-456-7890" );
person.getPhones().add( "456-000-1234" );
delete from Person_phones where Person_id=1

INSERT INTO Person_phones ( Person_id, phones )
VALUES ( 1, '123-456-7890' )

INSERT INTO Person_phones  (Person_id, phones)
VALUES  ( 1, '456-000-1234' )
Collections of entities

If value type collections can only form a one-to-many association between an owner entity and multiple basic or embeddable types, entity collections can represent both @OneToMany and @ManyToMany associations.

From a relational database perspective, associations are defined by the foreign key side (the child-side). With value type collections, only the entity can control the association (the parent-side), but for a collection of entities, both sides of the association are managed by the persistence context.

For this reason, entity collections can be devised into two main categories: unidirectional and bidirectional associations. Unidirectional associations are very similar to value type collections since only the parent side controls this relationship. Bidirectional associations are more tricky since, even if sides need to be in-sync at all times, only one side is responsible for managing the association. A bidirectional association has an owning side and an inverse (mappedBy) side.

3.9.8. @CollectionType

The @CollectionType annotation provides the ability to use a custom UserCollectionType implementation to influence how the collection for a plural attribute behaves.

As an example, consider a requirement for a collection with the semantics of a "unique list" - a cross between the ordered-ness of a List and the uniqueness of a Set. First the entity:

Example 241. @CollectionType
@Entity
public class TheEntityWithUniqueList {
	@ElementCollection
	@CollectionType(type = UniqueListType.class)
	private List<String> strings;

	// ...
}

The mapping says to use the UniqueListType class for the mapping of the plural attribute.

Example 242. UniqueListType
public class UniqueListType implements UserCollectionType {
	@Override
	public CollectionClassification getClassification() {
		return CollectionClassification.LIST;
	}

	@Override
	public Class<?> getCollectionClass() {
		return List.class;
	}

	@Override
	public PersistentCollection instantiate(
			SharedSessionContractImplementor session,
			CollectionPersister persister) {
		return new UniqueListWrapper( session );
	}

	@Override
	public PersistentCollection wrap(
			SharedSessionContractImplementor session,
			Object collection) {
		return new UniqueListWrapper( session, (List) collection );
	}

	@Override
	public Iterator getElementsIterator(Object collection) {
		return ( (List) collection ).iterator();
	}

	@Override
	public boolean contains(Object collection, Object entity) {
		return ( (List) collection ).contains( entity );
	}

	@Override
	public Object indexOf(Object collection, Object entity) {
		return ( (List) collection ).indexOf( entity );
	}

	@Override
	public Object replaceElements(
			Object original,
			Object target,
			CollectionPersister persister,
			Object owner,
			Map copyCache,
			SharedSessionContractImplementor session) {
		List result = (List) target;
		result.clear();
		result.addAll( (List) original );
		return result;
	}

	@Override
	public Object instantiate(int anticipatedSize) {
		return new ArrayList<>();
	}
}

Most custom UserCollectionType implementations will want their own PersistentCollection implementation.

Example 243. UniqueListWrapper
public class UniqueListWrapper<E> extends PersistentList<E> {
	public UniqueListWrapper(SharedSessionContractImplementor session) {
		super( session );
	}

	public UniqueListWrapper(SharedSessionContractImplementor session, List<E> list) {
		super( session, list );
	}

	// ...
}

UniqueListWrapper is the PersistentCollection implementation for the "unique list" semantic. See Wrappers for more details.

3.9.9. @CollectionTypeRegistration

For cases where an application wants to apply the same custom type to all plural attributes of a given classification, Hibernate also provides the @CollectionTypeRegistration:

Example 244. UniqueListType Registration
@Entity
@CollectionTypeRegistration( type = UniqueListType.class, classification = CollectionClassification.LIST )
public class TheEntityWithUniqueListRegistration {
	@ElementCollection
	private List<String> strings;

	// ...
}

This example behaves exactly as in @CollectionType.

3.9.10. Wrappers

As mentioned in Collection Semantics, Hibernate provides its own implementations of the Java collection types. These are called wrappers as they wrap an underlying collection and provide support for things like lazy loading, queueing add/remove operations while detached, etc. Hibernate defines the following PersistentCollection implementations for each of its collection classifications -

  • PersistentArrayHolder

  • PersistentBag

  • PersistentIdentifierBag

  • PersistentList

  • PersistentMap

  • PersistentSet

  • PersistentSortedMap

  • PersistentSortedSet

ORDERED_SET uses PersistentSet for its wrapper and ORDERED_MAP uses PersistentMap.

The collections they wrap are called "raw" collections, which are generally the standard Java implementations (java.util.ArrayList, etc)

Original content below

3.9.11. Bags

Bags are unordered lists, and we can have unidirectional bags or bidirectional ones.

Unidirectional bags

The unidirectional bag is mapped using a single @OneToMany annotation on the parent side of the association. Behind the scenes, Hibernate requires an association table to manage the parent-child relationship, as we can see in the following example:

Example 245. Unidirectional bag
@Entity(name = "Person")
public static class Person {

	@Id
	private Long id;

	@OneToMany(cascade = CascadeType.ALL)
	private List<Phone> phones = new ArrayList<>();

	//Getters and setters are omitted for brevity

}

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

	@Id
	private Long id;

	private String type;

	@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) ,
    type 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

Because both the parent and the child sides are entities, the persistence context manages each entity separately.

The cascading mechanism allows you to propagate an entity state transition from a parent entity to its children.

By marking the parent side with the CascadeType.ALL attribute, the unidirectional association lifecycle becomes very similar to that of a value type collection.

Example 246. Unidirectional bag lifecycle
Person person = new Person(1L);
person.getPhones().add(new Phone(1L, "landline", "028-234-9876"));
person.getPhones().add(new Phone(2L, "mobile", "072-122-9876"));
entityManager.persist(person);
INSERT INTO Person ( id )
VALUES ( 1 )

INSERT INTO Phone ( number, type, id )
VALUES ( '028-234-9876', 'landline', 1 )

INSERT INTO Phone ( number, type, id )
VALUES ( '072-122-9876', 'mobile', 2 )

INSERT INTO Person_Phone ( Person_id, phones_id )
VALUES ( 1, 1 )

INSERT INTO Person_Phone ( Person_id, phones_id )
VALUES ( 1, 2 )

In the example above, once the parent entity is persisted, the child entities are going to be persisted as well.

Just like value type collections, unidirectional bags are not as efficient when it comes to modifying the collection structure (removing or reshuffling elements).

Because the parent-side cannot uniquely identify each individual child, Hibernate deletes all link table rows associated with the parent entity and re-adds the remaining ones that are found in the current collection state.

Bidirectional bags

The bidirectional bag is the most common type of entity collection. The @ManyToOne side is the owning side of the bidirectional bag association, while the @OneToMany is the inverse side, being marked with the mappedBy attribute.

Example 247. Bidirectional bag
@Entity(name = "Person")
public static class Person {

	@Id
	private Long id;

	@OneToMany(mappedBy = "person", cascade = CascadeType.ALL)
	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
	private Long id;

	private String type;

	@Column(name = "`number`", unique = true)
	@NaturalId
	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),
    type 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
Example 248. Bidirectional bag lifecycle
person.addPhone(new Phone(1L, "landline", "028-234-9876"));
person.addPhone(new Phone(2L, "mobile", "072-122-9876"));
entityManager.flush();
person.removePhone(person.getPhones().get(0));
INSERT INTO Phone (number, person_id, type, id) 
VALUES ( '028-234-9876', 1, 'landline', 1 )

INSERT INTO Phone (number, person_id, type, id) 
VALUES ( '072-122-9876', 1, 'mobile', 2 )

UPDATE Phone
SET person_id = NULL, type = 'landline' where id = 1
Example 249. Bidirectional bag with orphan removal
@OneToMany(mappedBy = "person", cascade = CascadeType.ALL, orphanRemoval = true)
private List<Phone> phones = new ArrayList<>();
DELETE FROM Phone WHERE id = 1

When rerunning the previous example, the child will get removed because the parent-side propagates the removal upon dissociating the child entity reference.

3.9.12. Ordered Lists

Although they use the List interface on the Java side, bags don’t retain element order. To preserve the collection element order, there are two possibilities:

@OrderBy

the collection is ordered upon retrieval using a child entity property

@OrderColumn

the collection uses a dedicated order column in the collection link table

Unidirectional ordered lists

When using the @OrderBy annotation, the mapping looks as follows:

Example 250. Unidirectional @OrderBy list
@Entity(name = "Person")
public static class Person {

	@Id
	private Long id;

	@OneToMany(cascade = CascadeType.ALL)
	@OrderBy("number")
	private List<Phone> phones = new ArrayList<>();

	//Getters and setters are omitted for brevity

}

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

	@Id
	private Long id;

	private String type;

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

	//Getters and setters are omitted for brevity

}

The database mapping is the same as with the Unidirectional bags example, so it won’t be repeated. Upon fetching the collection, Hibernate generates the following select statement:

Example 251. Unidirectional @OrderBy list select statement
SELECT
   phones0_.Person_id AS Person_i1_1_0_,
   phones0_.phones_id AS phones_i2_1_0_,
   unidirecti1_.id AS id1_2_1_,
   unidirecti1_."number" AS number2_2_1_,
   unidirecti1_.type AS type3_2_1_ 
FROM
   Person_Phone phones0_ 
INNER JOIN
   Phone unidirecti1_ ON phones0_.phones_id=unidirecti1_.id
WHERE
   phones0_.Person_id = 1
ORDER BY
   unidirecti1_."number"

The child table column is used to order the list elements.

The @OrderBy annotation can take multiple entity properties, and each property can take an ordering direction too (e.g. @OrderBy("name ASC, type DESC")).

If no property is specified (e.g. @OrderBy), the primary key of the child entity table is used for ordering.

Another ordering option is to use the @OrderColumn annotation:

Example 252. Unidirectional @OrderColumn list
@OneToMany(cascade = CascadeType.ALL)
@OrderColumn(name = "order_id")
private List<Phone> phones = new ArrayList<>();
CREATE TABLE Person_Phone (
    Person_id BIGINT NOT NULL ,
    phones_id BIGINT NOT NULL ,
    order_id INTEGER NOT NULL ,
    PRIMARY KEY ( Person_id, order_id )
)

This time, the link table takes the order_id column and uses it to materialize the collection element order. When fetching the list, the following select query is executed:

Example 253. Unidirectional @OrderColumn list select statement
select
   phones0_.Person_id as Person_i1_1_0_,
   phones0_.phones_id as phones_i2_1_0_,
   phones0_.order_id as order_id3_0_,
   unidirecti1_.id as id1_2_1_,
   unidirecti1_.number as number2_2_1_,
   unidirecti1_.type as type3_2_1_
from
   Person_Phone phones0_
inner join
   Phone unidirecti1_
      on phones0_.phones_id=unidirecti1_.id
where
   phones0_.Person_id = 1

With the order_id column in place, Hibernate can order the list in-memory after it’s being fetched from the database.

Bidirectional ordered lists

The mapping is similar with the Bidirectional bags example, just that the parent side is going to be annotated with either @OrderBy or @OrderColumn.

Example 254. Bidirectional @OrderBy list
@OneToMany(mappedBy = "person", cascade = CascadeType.ALL)
@OrderBy("number")
private List<Phone> phones = new ArrayList<>();

Just like with the unidirectional @OrderBy list, the number column is used to order the statement on the SQL level.

When using the @OrderColumn annotation, the order_id column is going to be embedded in the child table:

Example 255. Bidirectional @OrderColumn list
@OneToMany(mappedBy = "person", cascade = CascadeType.ALL)
@OrderColumn(name = "order_id")
private List<Phone> phones = new ArrayList<>();
CREATE TABLE Phone (
    id BIGINT NOT NULL ,
    number VARCHAR(255) ,
    type VARCHAR(255) ,
    person_id BIGINT ,
    order_id INTEGER ,
    PRIMARY KEY ( id )
)

When fetching the collection, Hibernate will use the fetched ordered columns to sort the elements according to the @OrderColumn mapping.

Customizing ordered list ordinal

You can customize the ordinal of the underlying ordered list by using the @ListIndexBase annotation.

Example 256. @ListIndexBase mapping example
@OneToMany(mappedBy = "person", cascade = CascadeType.ALL)
@OrderColumn(name = "order_id")
@ListIndexBase(100)
private List<Phone> phones = new ArrayList<>();

When inserting two Phone records, Hibernate is going to start the List index from 100 this time.

Example 257. @ListIndexBase persist example
Person person = new Person(1L);
entityManager.persist(person);
person.addPhone(new Phone(1L, "landline", "028-234-9876"));
person.addPhone(new Phone(2L, "mobile", "072-122-9876"));
INSERT INTO Phone("number", person_id, type, id)
VALUES ('028-234-9876', 1, 'landline', 1)

INSERT INTO Phone("number", person_id, type, id)
VALUES ('072-122-9876', 1, 'mobile', 2)

UPDATE Phone
SET order_id = 100
WHERE id = 1

UPDATE Phone
SET order_id = 101
WHERE id = 2
Customizing ORDER BY SQL clause

While the Jakarta Persistence @OrderBy annotation allows you to specify the entity attributes used for sorting when fetching the current annotated collection, the Hibernate specific @OrderBy annotation is used to specify a SQL clause instead.

In the following example, the @OrderBy annotation uses the CHAR_LENGTH SQL function to order the Article entities by the number of characters of the name attribute.

Example 258. @OrderBy mapping example
@Entity(name = "Person")
public static class Person {

	@Id
	private Long id;

	private String name;

	@OneToMany(
		mappedBy = "person",
		cascade = CascadeType.ALL
	)
	@SQLOrder("CHAR_LENGTH(name) DESC")
	private List<Article> articles = new ArrayList<>();

	//Getters and setters are omitted for brevity
}

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

	@Id
	@GeneratedValue
	private Long id;

	private String name;

	private String content;

	@ManyToOne(fetch = FetchType.LAZY)
	private Person person;

	//Getters and setters are omitted for brevity
}

When fetching the articles collection, Hibernate uses the ORDER BY SQL clause provided by the mapping:

Example 259. @OrderBy fetching example
Person person = entityManager.find(Person.class, 1L);
assertEquals(
	"High-Performance Hibernate",
	person.getArticles().get(0).getName()
);
select
    a.person_id as person_i4_0_0_,
    a.id as id1_0_0_,
    a.content as content2_0_1_,
    a.name as name3_0_1_,
    a.person_id as person_i4_0_1_ 
from
    Article a 
where
    a.person_id = ?
order by
    CHAR_LENGTH(a.name) desc

3.9.13. Sets

Sets are collections that don’t allow duplicate entries and Hibernate supports both the unordered Set and the natural-ordering SortedSet.

Unidirectional sets

The unidirectional set uses a link table to hold the parent-child associations and the entity mapping looks as follows:

Example 260. Unidirectional set
@Entity(name = "Person")
public static class Person {

	@Id
	private Long id;

	@OneToMany(cascade = CascadeType.ALL)
	private Set<Phone> phones = new HashSet<>();

	//Getters and setters are omitted for brevity
}

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

	@Id
	private Long id;

	private String type;

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

	//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);
	}
}

The unidirectional set lifecycle is similar to that of the Unidirectional bags, so it can be omitted. The only difference is that Set doesn’t allow duplicates, but this constraint is enforced by the Java object contract rather than the database mapping.

When using Sets, it’s very important to supply proper equals/hashCode implementations for child entities.

In the absence of a custom equals/hashCode implementation logic, Hibernate will use the default Java reference-based object equality which might render unexpected results when mixing detached and managed object instances.

Bidirectional sets

Just like bidirectional bags, the bidirectional set doesn’t use a link table, and the child table has a foreign key referencing the parent table primary key. The lifecycle is just like with bidirectional bags except for the duplicates which are filtered out.

Example 261. Bidirectional set
@Entity(name = "Person")
public static class Person {

	@Id
	private Long id;

	@OneToMany(mappedBy = "person", cascade = CascadeType.ALL)
	private Set<Phone> phones = new HashSet<>();

	//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
	private Long id;

	private String type;

	@Column(name = "`number`", unique = true)
	@NaturalId
	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);
	}
}

3.9.14. Sorted sets

For sorted sets, the entity mapping must use the SortedSet interface instead. According to the SortedSet contract, all elements must implement the Comparable interface and therefore provide the sorting logic.

Unidirectional sorted sets

A SortedSet that relies on the natural sorting order given by the child element Comparable implementation logic might be annotated with the @SortNatural Hibernate annotation.

Example 262. Unidirectional natural sorted set
@Entity(name = "Person")
public static class Person {

	@Id
	private Long id;

	@OneToMany(cascade = CascadeType.ALL)
	@SortNatural
	private SortedSet<Phone> phones = new TreeSet<>();

	//Getters and setters are omitted for brevity

}

@Entity(name = "Phone")
public static class Phone implements Comparable<Phone> {

	@Id
	private Long id;

	private String type;

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

	//Getters and setters are omitted for brevity

	@Override
	public int compareTo(Phone o) {
		return number.compareTo(o.getNumber());
	}

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

The lifecycle and the database mapping are identical to the Unidirectional bags, so they are intentionally omitted.

To provide a custom sorting logic, Hibernate also provides a @SortComparator annotation:

Example 263. Unidirectional custom comparator sorted set
@Entity(name = "Person")
public static class Person {

	@Id
	private Long id;

	@OneToMany(cascade = CascadeType.ALL)
	@SortComparator(ReverseComparator.class)
	private SortedSet<Phone> phones = new TreeSet<>();

	//Getters and setters are omitted for brevity

}

public static class ReverseComparator implements Comparator<Phone> {

	@Override
	public int compare(Phone o1, Phone o2) {
		return o2.compareTo(o1);
	}
}

@Entity(name = "Phone")
public static class Phone implements Comparable<Phone> {

	@Id
	private Long id;

	private String type;

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

	//Getters and setters are omitted for brevity

	@Override
	public int compareTo(Phone o) {
		return number.compareTo(o.getNumber());
	}

	@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);
	}
}
Bidirectional sorted sets

The @SortNatural and @SortComparator work the same for bidirectional sorted sets too:

Example 264. Bidirectional natural sorted set
@OneToMany(mappedBy = "person", cascade = CascadeType.ALL)
@SortNatural
private SortedSet<Phone> phones = new TreeSet<>();


//Getters and setters are omitted for brevity

Before v6, @SortNatural must be used if collection element’s natural ordering is relied upon for sorting. Starting from v6, we can omit @SortNatural as it will take effect by default.

3.9.15. Maps

A java.util.Map is a ternary association because it requires a parent entity, a map key, and a value. An entity can either be a map key or a map value, depending on the mapping. Hibernate allows using the following map keys:

MapKeyColumn

for value type maps, the map key is a column in the link table that defines the grouping logic

MapKey

the map key is either the primary key or another property of the entity stored as a map entry value

MapKeyEnumerated

the map key is an Enum of the target child entity

MapKeyTemporal

the map key is a Date or a Calendar of the target child entity

MapKeyJoinColumn

the map key is an entity mapped as an association in the child entity that’s stored as a map entry key

Value type maps

A map of value type must use the @ElementCollection annotation, just like value type lists, bags or sets.

Example 265. Value type map with an entity as a map key
public enum PhoneType {
	LAND_LINE,
	MOBILE
}

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

	@Id
	private Long id;

	@Temporal(TemporalType.TIMESTAMP)
	@ElementCollection
	@CollectionTable(name = "phone_register")
	@Column(name = "since")
	private Map<Phone, Date> phoneRegister = new HashMap<>();

	//Getters and setters are omitted for brevity

}

@Embeddable
public static class Phone {

	private PhoneType type;

	@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 phone_register (
    Person_id BIGINT NOT NULL ,
    since TIMESTAMP ,
    number VARCHAR(255) NOT NULL ,
    type INTEGER NOT NULL ,
    PRIMARY KEY ( Person_id, number, type )
)

ALTER TABLE phone_register
ADD CONSTRAINT FKrmcsa34hr68of2rq8qf526mlk
FOREIGN KEY (Person_id) REFERENCES Person

Adding entries to the map generates the following SQL statements:

Example 266. Adding value type map entries
person.getPhoneRegister().put(
	new Phone(PhoneType.LAND_LINE, "028-234-9876"), new Date()
);
person.getPhoneRegister().put(
	new Phone(PhoneType.MOBILE, "072-122-9876"), new Date()
);
INSERT INTO phone_register (Person_id, number, type, since)
VALUES (1, '072-122-9876', 1, '2015-12-15 17:16:45.311')

INSERT INTO phone_register (Person_id, number, type, since)
VALUES (1, '028-234-9876', 0, '2015-12-15 17:16:45.311')
Maps with a custom key type

Hibernate defines the @MapKeyType annotation which you can use to customize the Map key type.

Considering you have the following tables in your database:

create table person (
    id int8 not null,
    primary key (id)
)

create table call_register (
    person_id int8 not null,
    phone_number int4,
    call_timestamp_epoch int8 not null,
    primary key (person_id, call_timestamp_epoch)
)

alter table if exists call_register
    add constraint FKsn58spsregnjyn8xt61qkxsub
    foreign key (person_id)
    references person

The call_register records the call history for every person. The call_timestamp_epoch column stores the phone call timestamp as a Unix timestamp since the Unix epoch.

The @MapKeyColumn annotation is used to define the table column holding the key while the @Column mapping gives the value of the java.util.Map in question.

Since we want to map all the calls by their associated java.util.Date, not by their timestamp since epoch which is a number, the entity mapping looks as follows:

Example 267. @MapKeyType mapping example
@Entity
@Table(name = "person")
public static class Person {

	@Id
	private Long id;

	@ElementCollection
	@CollectionTable(
		name = "call_register",
		joinColumns = @JoinColumn(name = "person_id")
	)
	@MapKeyJdbcTypeCode(Types.BIGINT)
	@MapKeyJavaType(JdbcTimestampJavaType.class)
	@MapKeyColumn(name = "call_timestamp_epoch")
	@Column(name = "phone_number")
	private Map<Date, Integer> callRegister = new HashMap<>();

	//Getters and setters are omitted for brevity

}
Maps having an interface type as the key

Considering you have the following PhoneNumber interface with an implementation given by the MobilePhone class type:

Example 268. PhoneNumber interface and the MobilePhone class type
public interface PhoneNumber {

	String get();
}

@Embeddable
public static class MobilePhone
		implements PhoneNumber {

	static PhoneNumber fromString(String phoneNumber) {
		String[] tokens = phoneNumber.split("-");
		if (tokens.length != 3) {
			throw new IllegalArgumentException("invalid phone number: " + phoneNumber);
		}
		int i = 0;
		return new MobilePhone(
			tokens[i++],
			tokens[i++],
			tokens[i]
		);
	}

	private MobilePhone() {
	}

	public MobilePhone(
			String countryCode,
			String operatorCode,
			String subscriberCode) {
		this.countryCode = countryCode;
		this.operatorCode = operatorCode;
		this.subscriberCode = subscriberCode;
	}

	@Column(name = "country_code")
	private String countryCode;

	@Column(name = "operator_code")
	private String operatorCode;

	@Column(name = "subscriber_code")
	private String subscriberCode;

	@Override
	public String get() {
		return String.format(
			"%s-%s-%s",
			countryCode,
			operatorCode,
			subscriberCode
		);
	}

	@Override
	public boolean equals(Object o) {
		if (this == o) {
			return true;
		}
		if (o == null || getClass() != o.getClass()) {
			return false;
		}
		MobilePhone that = (MobilePhone) o;
		return Objects.equals(countryCode, that.countryCode) &&
				Objects.equals(operatorCode, that.operatorCode) &&
				Objects.equals(subscriberCode, that.subscriberCode);
	}

	@Override
	public int hashCode() {
		return Objects.hash(countryCode, operatorCode, subscriberCode);
	}
}

If you want to use the PhoneNumber interface as a java.util.Map key, then you need to supply the @MapKeyClass annotation as well.

Example 269. @MapKeyClass mapping example
@Entity
@Table(name = "person")
public static class Person {

	@Id
	private Long id;

	@ElementCollection
	@CollectionTable(
		name = "call_register",
		joinColumns = @JoinColumn(name = "person_id")
	)
	@MapKeyColumn(name = "call_timestamp_epoch")
	@MapKeyClass(MobilePhone.class)
	@Column(name = "call_register")
	private Map<PhoneNumber, Integer> callRegister = new HashMap<>();

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

create table call_register (
    person_id bigint not null,
    call_register integer,
    country_code varchar(255) not null,
    operator_code varchar(255) not null,
    subscriber_code varchar(255) not null,
    primary key (person_id, country_code, operator_code, subscriber_code)
)

alter table call_register
    add constraint FKqyj2at6ik010jqckeaw23jtv2
    foreign key (person_id)
    references person

When inserting a Person with a callRegister containing 2 MobilePhone references, Hibernate generates the following SQL statements:

Example 270. @MapKeyClass persist example
Person person = new Person();
person.setId(1L);
person.getCallRegister().put(new MobilePhone("01", "234", "567"), 101);
person.getCallRegister().put(new MobilePhone("01", "234", "789"), 102);

entityManager.persist(person);
insert into person (id) values (?)

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

insert into call_register(
    person_id,
    country_code,
    operator_code,
    subscriber_code,
    call_register
)
values
    (?, ?, ?, ?, ?)

-- binding parameter [1] as [BIGINT]  - [1]
-- binding parameter [2] as [VARCHAR] - [01]
-- binding parameter [3] as [VARCHAR] - [234]
-- binding parameter [4] as [VARCHAR] - [789]
-- binding parameter [5] as [INTEGER] - [102]

insert into call_register(
    person_id,
    country_code,
    operator_code,
    subscriber_code,
    call_register
)
values
    (?, ?, ?, ?, ?)

-- binding parameter [1] as [BIGINT]  - [1]
-- binding parameter [2] as [VARCHAR] - [01]
-- binding parameter [3] as [VARCHAR] - [234]
-- binding parameter [4] as [VARCHAR] - [567]
-- binding parameter [5] as [INTEGER] - [101]

When fetching a Person and accessing the callRegister Map, Hibernate generates the following SQL statements:

Example 271. @MapKeyClass fetch example
Person person = entityManager.find(Person.class, 1L);
assertEquals(2, person.getCallRegister().size());

assertEquals(
	Integer.valueOf(101),
	person.getCallRegister().get(MobilePhone.fromString("01-234-567"))
);

assertEquals(
	Integer.valueOf(102),
	person.getCallRegister().get(MobilePhone.fromString("01-234-789"))
);
select
    cr.person_id as person_i1_0_0_,
    cr.call_register as call_reg2_0_0_,
    cr.country_code as country_3_0_,
    cr.operator_code as operator4_0_,
    cr.subscriber_code as subscrib5_0_
from
    call_register cr
where
    cr.person_id = ?

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

-- extracted value ([person_i1_0_0_] : [BIGINT])  - [1]
-- extracted value ([call_reg2_0_0_] : [INTEGER]) - [101]
-- extracted value ([country_3_0_]   : [VARCHAR]) - [01]
-- extracted value ([operator4_0_]   : [VARCHAR]) - [234]
-- extracted value ([subscrib5_0_]   : [VARCHAR]) - [567]

-- extracted value ([person_i1_0_0_] : [BIGINT])  - [1]
-- extracted value ([call_reg2_0_0_] : [INTEGER]) - [102]
-- extracted value ([country_3_0_]   : [VARCHAR]) - [01]
-- extracted value ([operator4_0_]   : [VARCHAR]) - [234]
-- extracted value ([subscrib5_0_]   : [VARCHAR]) - [789]
Unidirectional maps

A unidirectional map exposes a parent-child association from the parent-side only.

The following example shows a unidirectional map which also uses a @MapKeyTemporal annotation. The map key is a timestamp, and it’s taken from the child entity table.

The @MapKey annotation is used to define the entity attribute used as a key of the java.util.Map in question.

Example 272. Unidirectional Map
public enum PhoneType {
	LAND_LINE,
	MOBILE
}

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

	@Id
	private Long id;

	@OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
	@JoinTable(
		name = "phone_register",
		joinColumns = @JoinColumn(name = "phone_id"),
		inverseJoinColumns = @JoinColumn(name = "person_id"))
	@MapKey(name = "since")
	@MapKeyTemporal(TemporalType.TIMESTAMP)
	private Map<Date, Phone> phoneRegister = new HashMap<>();

	//Getters and setters are omitted for brevity

	public void addPhone(Phone phone) {
		phoneRegister.put(phone.getSince(), phone);
	}
}

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

	@Id
	@GeneratedValue
	private Long id;

	private PhoneType type;

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

	private Date since;

	//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) ,
    since TIMESTAMP ,
    type INTEGER ,
    PRIMARY KEY ( id )
)

CREATE TABLE phone_register (
    phone_id BIGINT NOT NULL ,
    person_id BIGINT NOT NULL ,
    PRIMARY KEY ( phone_id, person_id )
)

ALTER TABLE phone_register
ADD CONSTRAINT FKc3jajlx41lw6clbygbw8wm65w
FOREIGN KEY (person_id) REFERENCES Phone

ALTER TABLE phone_register
ADD CONSTRAINT FK6npoomh1rp660o1b55py9ndw4
FOREIGN KEY (phone_id) REFERENCES Person
Bidirectional maps

Like most bidirectional associations, this relationship is owned by the child-side while the parent is the inverse side and can propagate its own state transitions to the child entities.

In the following example, you can see that @MapKeyEnumerated was used so that the Phone enumeration becomes the map key.

Example 273. Bidirectional Map
@Entity(name = "Person")
public static class Person {

	@Id
	private Long id;

	@OneToMany(mappedBy = "person", cascade = CascadeType.ALL, orphanRemoval = true)
	@MapKey(name = "type")
	@MapKeyEnumerated
	private Map<PhoneType, Phone> phoneRegister = new HashMap<>();

	//Getters and setters are omitted for brevity

	public void addPhone(Phone phone) {
		phone.setPerson(this);
		phoneRegister.put(phone.getType(), phone);
	}
}

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

	@Id
	@GeneratedValue
	private Long id;

	private PhoneType type;

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

	private Date since;

	@ManyToOne
	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) ,
    since TIMESTAMP ,
    type INTEGER ,
    person_id BIGINT ,
    PRIMARY KEY ( id )
)

ALTER TABLE Phone
ADD CONSTRAINT FKmw13yfsjypiiq0i1osdkaeqpg
FOREIGN KEY (person_id) REFERENCES Person

3.9.16. Arrays

When discussing arrays, it is important to understand the distinction between SQL array types and Java arrays that are mapped as part of the application’s domain model.

Not all databases implement the SQL-99 ARRAY type and, for this reason, the SQL type used by Hibernate for arrays varies depending on the database support.

It is impossible for Hibernate to offer lazy-loading for arrays of entities and, for this reason, it is strongly recommended to map a "collection" of entities using a List or Set rather than an array.

3.9.17. Arrays as basic value type

By default, Hibernate will choose a type for the array based on Dialect.getPreferredSqlTypeCodeForArray(). Prior to Hibernate 6.1, the default was to always use the BINARY type, as supported by the current Dialect, but now, Hibernate will leverage the native array data types if possible.

To force the BINARY type, the persistent attribute has to be annotated with @JdbcTypeCode(SqlTypes.VARBINARY).

Example 274. Arrays stored as SQL array
@Entity(name = "Person")
public static class Person {

	@Id
	private Long id;

	private String[] phones;

	//Getters and setters are omitted for brevity

}
CREATE TABLE Person (
    id BIGINT NOT NULL,
    phones VARCHAR(255) ARRAY,
    PRIMARY KEY ( id )
)

3.9.18. Collections as basic value type

Notice how all the previous examples explicitly mark the collection attribute as either @ElementCollection, @OneToMany or @ManyToMany.

Attributes of collection or array type without any of those annotations are considered basic types and by default mapped like basic arrays as depicted in the previous section.

Example 275. Collections stored as SQL array
@Entity(name = "Person")
public static class Person {

	@Id
	private Long id;

	private List<String> phones;

	//Getters and setters are omitted for brevity

}
CREATE TABLE Person (
    id BIGINT NOT NULL,
    phones VARCHAR(255) ARRAY,
    PRIMARY KEY ( id )
)

Prior to Hibernate 6.1, it was common to use an AttributeConverter to map the elements into e.g. a comma separated list which is still a viable option. Just note that it is not required anymore.

Example 276. Comma delimited collection
public class CommaDelimitedStringsConverter implements AttributeConverter<List<String>,String> {
	@Override
	public String convertToDatabaseColumn(List<String> attributeValue) {
		if ( attributeValue == null ) {
			return null;
		}
		return join( ",", attributeValue );
	}

	@Override
	public List<String> convertToEntityAttribute(String dbData) {
		if ( dbData == null ) {
			return null;
		}
		return listOf( dbData.split( "," ) );
	}
}

@Entity( name = "Person" )
public static class Person {
    @Id
    private Integer id;
    @Basic
	private String name;
	@Basic
	@Convert( converter = CommaDelimitedStringsConverter.class )
	private List<String> nickNames;

	// ...

}

3.10. Natural Ids

Natural ids represent domain model unique identifiers that have a meaning in the real world too. Even if a natural id does not make a good primary key (surrogate keys being usually preferred), it’s still useful to tell Hibernate about it. As we will see later, Hibernate provides a dedicated, efficient API for loading an entity by its natural id much like it offers for loading by identifier (PK).

All values used in a natural id must be non-nullable.

For natural id mappings using a to-one association, this precludes the use of not-found mappings which effectively define a nullable mapping.

3.10.1. Natural Id Mapping

Natural ids are defined in terms of one or more persistent attributes.

Example 277. Natural id using single basic attribute
@Entity(name = "Book")
public static class Book {

	@Id
	private Long id;

	private String title;

	private String author;

	@NaturalId
	private String isbn;

	//Getters and setters are omitted for brevity
}
Example 278. Natural id using single embedded attribute
@Entity(name = "Book")
public static class Book {

	@Id
	private Long id;

	private String title;

	private String author;

	@NaturalId
	@Embedded
	private Isbn isbn;

	//Getters and setters are omitted for brevity
}

@Embeddable
public static class Isbn implements Serializable {

	private String isbn10;

	private String isbn13;

	//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;
		}
		Isbn isbn = (Isbn) o;
		return Objects.equals(isbn10, isbn.isbn10) &&
				Objects.equals(isbn13, isbn.isbn13);
	}

	@Override
	public int hashCode() {
		return Objects.hash(isbn10, isbn13);
	}
}
Example 279. Natural id using multiple persistent attributes
@Entity(name = "Book")
public static class Book {

	@Id
	private Long id;

	private String title;

	private String author;

	@NaturalId
	private String productNumber;

	@NaturalId
	@ManyToOne(fetch = FetchType.LAZY)
	private Publisher publisher;

	//Getters and setters are omitted for brevity
}

@Entity(name = "Publisher")
public static class Publisher implements Serializable {

	@Id
	private Long 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(id, publisher.id) &&
				Objects.equals(name, publisher.name);
	}

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

3.10.2. Natural Id API

As stated before, Hibernate provides an API for loading entities by their associated natural id. This is represented by the org.hibernate.NaturalIdLoadAccess contract obtained via Session#byNaturalId.

If the entity does not define a natural id, trying to load an entity by its natural id will throw an exception.

Example 280. Using NaturalIdLoadAccess
Book book = entityManager
	.unwrap(Session.class)
	.byNaturalId(Book.class)
	.using("isbn", "978-9730228236")
	.load();
Book book = entityManager
	.unwrap(Session.class)
	.byNaturalId(Book.class)
	.using(
		"isbn",
		new Isbn(
			"973022823X",
			"978-9730228236"
		))
	.load();
Book book = entityManager
	.unwrap(Session.class)
	.byNaturalId(Book.class)
	.using("productNumber", "973022823X")
	.using("publisher", publisher)
	.load();

NaturalIdLoadAccess offers 2 distinct methods for obtaining the entity:

load()

obtains a reference to the entity, making sure that the entity state is initialized.

getReference()

obtains a reference to the entity. The state may or may not be initialized. If the entity is already associated with the current running Session, that reference (loaded or not) is returned. If the entity is not loaded in the current Session and the entity supports proxy generation, an uninitialized proxy is generated and returned, otherwise the entity is loaded from the database and returned.

NaturalIdLoadAccess allows loading an entity by natural id and at the same time applies a pessimistic lock. For additional details on locking, see the Locking chapter.

We will discuss the last method available on NaturalIdLoadAccess ( setSynchronizationEnabled() ) in Natural Id - Mutability and Caching.

Because the Book entities in the first two examples define "simple" natural ids, we can load them as follows:

Example 281. Loading by simple natural id
Book book = entityManager
	.unwrap(Session.class)
	.bySimpleNaturalId(Book.class)
	.load("978-9730228236");
Book book = entityManager
	.unwrap(Session.class)
	.bySimpleNaturalId(Book.class)
	.load(
		new Isbn(
			"973022823X",
			"978-9730228236"
		)
	);

Here we see the use of the org.hibernate.SimpleNaturalIdLoadAccess contract, obtained via Session#bySimpleNaturalId().

SimpleNaturalIdLoadAccess is similar to NaturalIdLoadAccess except that it does not define the using method. Instead, because these simple natural ids are defined based on just one attribute we can directly pass the corresponding natural id attribute value directly to the load() and getReference() methods.

If the entity does not define a natural id, or if the natural id is not of a "simple" type, an exception will be thrown there.

3.10.3. Natural Id - Mutability and Caching

A natural id may be mutable or immutable. By default the @NaturalId annotation marks an immutable natural id attribute. An immutable natural id is expected to never change its value.

If the value(s) of the natural id attribute(s) change, @NaturalId(mutable = true) should be used instead.

Example 282. Mutable natural id mapping
@Entity(name = "Author")
public static class Author {

	@Id
	private Long id;

	private String name;

	@NaturalId(mutable = true)
	private String email;

	//Getters and setters are omitted for brevity
}

Within the Session, Hibernate maintains a mapping from natural id values to entity identifiers (PK) values. If natural ids values changed, it is possible for this mapping to become out of date until a flush occurs.

To work around this condition, Hibernate will attempt to discover any such pending changes and adjust them when the load() or getReference() methods are executed. To be clear: this is only pertinent for mutable natural ids.

This discovery and adjustment have a performance impact. If you are certain that none of the mutable natural ids already associated with the current Session have changed, you can disable this checking by calling setSynchronizationEnabled(false) (the default is true). This will force Hibernate to circumvent the checking of mutable natural ids.

Example 283. Mutable natural id synchronization use-case
Author author = entityManager
	.unwrap(Session.class)
	.bySimpleNaturalId(Author.class)
	.load("john@acme.com");

author.setEmail("john.doe@acme.com");

assertNull(
	entityManager
		.unwrap(Session.class)
		.bySimpleNaturalId(Author.class)
		.setSynchronizationEnabled(false)
		.load("john.doe@acme.com")
);

assertSame(author,
	entityManager
		.unwrap(Session.class)
		.bySimpleNaturalId(Author.class)
		.setSynchronizationEnabled(true)
		.load("john.doe@acme.com")
);

Not only can this NaturalId-to-PK resolution be cached in the Session, but we can also have it cached in the second-level cache if second level caching is enabled.

Example 284. Natural id caching
@Entity(name = "Book")
@NaturalIdCache
public static class Book {

	@Id
	private Long id;

	private String title;

	private String author;

	@NaturalId
	private String isbn;

	//Getters and setters are omitted for brevity
}

3.11. Partitioning

In data management, it is sometimes necessary to split data of a table into various (physical) partitions, based on partition keys and a partitioning scheme.

Due to the nature of partitioning, it is vital for the database to know the partition key of a row for certain operations, like SQL update and delete statements. If a database doesn’t know the partition of a row that should be updated or deleted, then it must look for the row in all partitions, leading to poor performance.

The @PartitionKey annotation is a way to tell Hibernate about the column, such that it can include a column restriction as predicate into SQL update and delete statements for entity state changes.

3.11.1. Partition Key Mapping

Partition keys are defined in terms of one or more persistent attributes.

Example 285. Partition key using single basic attribute
@Entity(name = "User")
public static class User {

	@Id
	private Long id;

	private String firstname;

	private String lastname;

	@PartitionKey
	private String tenantKey;

	//Getters and setters are omitted for brevity
}

When updating or deleting an entity, Hibernate will include a partition key constraint similar to this

update user_tbl set firstname=?,lastname=?,tenantKey=? where id=? and tenantKey=?
delete from user_tbl where id=? and tenantKey=?

3.12. Soft Delete

An occasional requirement seen in the wild is to never physically remove rows from the database, but to instead perform a "soft delete" where a column is updated to indicate that the row is no longer active. Hibernate offers first-class support for this behavior through its @SoftDelete annotation.

The @SoftDelete annotation is new in 6.4.

It was possible to hack together support for soft deletes in previous versions using a combination of filters, @Where and custom delete event handling. However, that approach was tedious and did not work in all cases. @SoftDelete should be highly preferred.

Hibernate supports soft delete for both entities and collections.

Soft delete support is defined by 3 main parts -

  1. A strategy for interpreting the stored indicator values.

  2. The column which contains the indicator.

  3. A conversion from Boolean indicator value to the proper database type

3.12.1. Strategy - SoftDeleteType

Given truth values, there are 2 valid ways to interpret the values stored in the database. This interpretation is defined by the SoftDeleteType enumeration and can be configured per-usage using @SoftDelete(…​, strategy=ACTIVE) or @SoftDelete(…​, strategy=DELETED) -

ACTIVE

Tracks rows which are active. A true value in the database indicates that the row is active (non-deleted); a false value indicates inactive (deleted).

DELETED

Tracks rows which are deleted. A true value in the database indicates that the row is deleted; a false value indicates that the row is non-deleted.

3.12.2. Indicator column

The column where the indicator value is stored is defined using @SoftDelete#columnName attribute.

The default column name depends on the strategy being used -

ACTIVE

The default column name is active.

DELETED

The default column name is deleted.

See Basic entity soft-delete for an example of customizing the column name.

Depending on the conversion type, an appropriate check constraint may be applied to the column.

3.12.3. Indicator conversion

The conversion is defined using a Jakarta Persistence AttributeConverter. The domain-type is always boolean. The relational-type can be any type, as defined by the converter; generally BOOLEAN, BIT, INTEGER or CHAR.

An explicit conversion can be specified using @SoftDelete#converter. See Basic entity soft-delete for an example of specifying an explicit conversion. Explicit conversions can specify a custom converter or leverage Hibernate-provided converters for the 3 most common cases -

NumericBooleanConverter

Defines conversion using 0 for false and 1 for true

YesNoConverter

Defines conversion using 'N' for false and 'Y' for true

TrueFalseConverter

Defines conversion using 'F' for false and 'T' for true

If an explicit converter is not specified, Hibernate will follow the same resolution steps defined in Boolean to determine the proper database type -

boolean (and bit)

the underlying type is boolean / bit and no conversion is applied

numeric

the underlying type is integer and values are converted according to NumericBooleanConverter

character

the underlying type is char and values are converted according to TrueFalseConverter

The converter should simply convert the true and false, irrespective of the strategy used. Hibernate will handle applying the strategy.

3.12.4. Entity soft delete

Hibernate supports the soft delete of entities, with the indicator column defined on the primary table.

Example 286. Basic entity soft-delete
@Entity(name = "SimpleEntity")
@SoftDelete(columnName = "removed", converter = YesNoConverter.class)
public class SimpleEntity {
	// ...
}

For entity hierarchies, the soft delete applies to all inheritance types.

Example 287. Inherited entity soft-delete
@Entity
@Inheritance(strategy = InheritanceType.JOINED)
@SoftDelete(columnName = "removed", converter = YesNoConverter.class)
public abstract class JoinedRoot {
	// ...
}
@Entity
@Table(name = "joined_sub")
@PrimaryKeyJoinColumn(name = "joined_fk")
public class JoinedSub extends JoinedRoot {
	// ...
}

3.12.5. Collection soft delete

Soft delete may be applied to collection mapped with a "collection table", aka @ElementCollection and @ManyToMany. The soft delete applies to the collection table row.

Annotating a @OneToMany association with @SoftDelete will throw an exception.

In the case of @OneToMany and @ManyToMany, the mapped entity may itself be soft deletable which is handled transparently.

Example 288. Soft delete for @ElementCollection
@ElementCollection
@CollectionTable(name = "elements", joinColumns = @JoinColumn(name = "owner_fk"))
@Column(name = "txt")
@SoftDelete(converter = YesNoConverter.class)
private Collection<String> elements;

Given this @ElementCollection mapping, rows in the elements table will be soft deleted using an indicator column named deleted.

Example 289. Soft delete for @ManyToMany
@ManyToMany
@JoinTable(
		name = "m2m",
		joinColumns = @JoinColumn(name = "owner_fk"),
		inverseJoinColumns = @JoinColumn(name = "owned_fk")
)
@SoftDelete(columnName = "gone", converter = NumericBooleanConverter.class)
private Collection<CollectionOwned> manyToMany;

Given this @ManyToMany mapping, rows in the m2m table will be soft deleted using an indicator column named gone.

3.12.6. Package-level soft delete

The @SoftDelete annotation may also be placed at the package level, in which case it applies to all entities and collections defined within the package.

3.13. Dynamic Model

Jakarta Persistence only acknowledges the POJO entity model mapping so, if you are concerned about Jakarta Persistence provider portability, it’s best to stick to the strict POJO model. On the other hand, Hibernate can work with both POJO entities and dynamic entity models.

3.13.1. Dynamic mapping models

Persistent entities do not necessarily have to be represented as POJO/JavaBean classes. Hibernate also supports dynamic models (using Map of Maps at runtime). With this approach, you do not write persistent classes, only mapping files.

A given entity has just one entity mode within a given SessionFactory. This is a change from previous versions which allowed to define multiple entity modes for an entity and to select which to load. Entity modes can now be mixed within a domain model; a dynamic entity might reference a POJO entity and vice versa.

Example 290. Dynamic domain model Hibernate mapping
<!DOCTYPE hibernate-mapping PUBLIC
    "-//Hibernate/Hibernate Mapping DTD 3.0//EN"
    "http://www.hibernate.org/dtd/hibernate-mapping-3.0.dtd">

<hibernate-mapping>
    <class entity-name="Book">
        <id name="isbn" column="isbn" length="32" type="string"/>

        <property name="title" not-null="true" length="50" type="string"/>

        <property name="author" not-null="true" length="50" type="string"/>

    </class>
</hibernate-mapping>

After you defined your entity mapping, you need to instruct Hibernate to use the dynamic mapping mode:

Example 291. Dynamic domain model Hibernate mapping
settings.put("hibernate.default_entity_mode", "dynamic-map");

When you are going to save the following Book dynamic entity, Hibernate is going to generate the following SQL statement:

Example 292. Persist dynamic entity
Map<String, String> book = new HashMap<>();
book.put("isbn", "978-9730228236");
book.put("title", "High-Performance Java Persistence");
book.put("author", "Vlad Mihalcea");

entityManager
	.unwrap(Session.class)
	.persist("Book", book);
insert
into
    Book
    (title, author, isbn)
values
    (?, ?, ?)

-- binding parameter [1] as [VARCHAR] - [High-Performance Java Persistence]
-- binding parameter [2] as [VARCHAR] - [Vlad Mihalcea]
-- binding parameter [3] as [VARCHAR] - [978-9730228236]

The main advantage of dynamic models is the quick turnaround time for prototyping without the need for entity class implementation. The main downfall is that you lose compile-time type checking and will likely deal with many exceptions at runtime. However, as a result of the Hibernate mapping, the database schema can easily be normalized and sound, allowing to add a proper domain model implementation on top later on.

It is also interesting to note that dynamic models are great for certain integration use cases as well. Envers, for example, makes extensive use of dynamic models to represent the historical data.

3.14. Inheritance

Although relational database systems don’t provide support for inheritance, Hibernate provides several strategies to leverage this object-oriented trait onto domain model entities:

MappedSuperclass

Inheritance is implemented in the domain model only without reflecting it in the database schema. See MappedSuperclass.

Single table

The domain model class hierarchy is materialized into a single table which contains entities belonging to different class types. See Single table.

Joined table

The base class and all the subclasses have their own database tables and fetching a subclass entity requires a join with the parent table as well. See Joined table.

Table per class

Each subclass has its own table containing both the subclass and the base class properties. See Table per class.

3.14.1. MappedSuperclass

In the following domain model class hierarchy, a DebitAccount and a CreditAccount share the same Account base class.

Inheritance class diagram

When using MappedSuperclass, the inheritance is visible in the domain model only, and each database table contains both the base class and the subclass properties.

Example 293. @MappedSuperclass inheritance
@MappedSuperclass
public static class Account {

	@Id
	private Long id;

	private String owner;

	private BigDecimal balance;

	private BigDecimal interestRate;

	//Getters and setters are omitted for brevity

}

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

	private BigDecimal overdraftFee;

	//Getters and setters are omitted for brevity

}

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

	private BigDecimal creditLimit;

	//Getters and setters are omitted for brevity

}
CREATE TABLE DebitAccount (
    id BIGINT NOT NULL ,
    balance NUMERIC(19, 2) ,
    interestRate NUMERIC(19, 2) ,
    owner VARCHAR(255) ,
    overdraftFee NUMERIC(19, 2) ,
    PRIMARY KEY ( id )
)

CREATE TABLE CreditAccount (
    id BIGINT NOT NULL ,
    balance NUMERIC(19, 2) ,
    interestRate NUMERIC(19, 2) ,
    owner VARCHAR(255) ,
    creditLimit NUMERIC(19, 2) ,
    PRIMARY KEY ( id )
)

Because the @MappedSuperclass inheritance model is not mirrored at the database level, it’s not possible to use polymorphic queries referencing the @MappedSuperclass when fetching persistent objects by their base class.

3.14.2. Single table

The single table inheritance strategy maps all subclasses to only one database table. Each subclass declares its own persistent properties. Version and id properties are assumed to be inherited from the root class.

When omitting an explicit inheritance strategy (e.g. @Inheritance), Jakarta Persistence will choose the SINGLE_TABLE strategy by default.

Example 294. Single Table inheritance
@Entity(name = "Account")
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
public static class Account {

	@Id
	private Long id;

	private String owner;

	private BigDecimal balance;

	private BigDecimal interestRate;

	//Getters and setters are omitted for brevity

}

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

	private BigDecimal overdraftFee;

	//Getters and setters are omitted for brevity

}

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

	private BigDecimal creditLimit;

	//Getters and setters are omitted for brevity

}
CREATE TABLE Account (
    DTYPE VARCHAR(31) NOT NULL ,
    id BIGINT NOT NULL ,
    balance NUMERIC(19, 2) ,
    interestRate NUMERIC(19, 2) ,
    owner VARCHAR(255) ,
    overdraftFee NUMERIC(19, 2) ,
    creditLimit NUMERIC(19, 2) ,
    PRIMARY KEY ( id )
)

Each subclass in a hierarchy must define a unique discriminator value, which is used to differentiate between rows belonging to separate subclass types. If this is not specified, the DTYPE column is used as a discriminator, storing the associated subclass name.

Example 295. Single Table inheritance discriminator column
DebitAccount debitAccount = new DebitAccount();
debitAccount.setId(1L);
debitAccount.setOwner("John Doe");
debitAccount.setBalance(BigDecimal.valueOf(100));
debitAccount.setInterestRate(BigDecimal.valueOf(1.5d));
debitAccount.setOverdraftFee(BigDecimal.valueOf(25));

CreditAccount creditAccount = new CreditAccount();
creditAccount.setId(2L);
creditAccount.setOwner("John Doe");
creditAccount.setBalance(BigDecimal.valueOf(1000));
creditAccount.setInterestRate(BigDecimal.valueOf(1.9d));
creditAccount.setCreditLimit(BigDecimal.valueOf(5000));

entityManager.persist(debitAccount);
entityManager.persist(creditAccount);
INSERT INTO Account (balance, interestRate, owner, overdraftFee, DTYPE, id)
VALUES (100, 1.5, 'John Doe', 25, 'DebitAccount', 1)

INSERT INTO Account (balance, interestRate, owner, creditLimit, DTYPE, id)
VALUES (1000, 1.9, 'John Doe', 5000, 'CreditAccount', 2)

When using polymorphic queries, only a single table is required to be scanned to fetch all associated subclass instances.

Example 296. Single Table polymorphic query
List<Account> accounts = entityManager
	.createQuery("select a from Account a")
	.getResultList();
SELECT  singletabl0_.id AS id2_0_ ,
        singletabl0_.balance AS balance3_0_ ,
        singletabl0_.interestRate AS interest4_0_ ,
        singletabl0_.owner AS owner5_0_ ,
        singletabl0_.overdraftFee AS overdraf6_0_ ,
        singletabl0_.creditLimit AS creditLi7_0_ ,
        singletabl0_.DTYPE AS DTYPE1_0_
FROM    Account singletabl0_

Among all other inheritance alternatives, the single table strategy performs the best since it requires access to one table only. Because all subclass columns are stored in a single table, it’s not possible to use NOT NULL constraints anymore, so integrity checks must be moved either into the data access layer or enforced through CHECK or TRIGGER constraints.

Discriminator

The discriminator column contains marker values that tell the persistence layer what subclass to instantiate for a particular row. Hibernate Core supports the following restricted set of types as discriminator column: String, char, int, byte, short, boolean(including yes_no, true_false).

Use the @DiscriminatorColumn to define the discriminator column as well as the discriminator type.

The enum DiscriminatorType used in jakarta.persistence.DiscriminatorColumn only contains the values STRING, CHAR and INTEGER which means that not all Hibernate supported types are available via the @DiscriminatorColumn annotation. You can also use @DiscriminatorFormula to express in SQL a virtual discriminator column. This is particularly useful when the discriminator value can be extracted from one or more columns of the table. Both @DiscriminatorColumn and @DiscriminatorFormula are to be set on the root entity (once per persisted hierarchy).

@org.hibernate.annotations.DiscriminatorOptions allows to optionally specify Hibernate-specific discriminator options which are not standardized in Jakarta Persistence. The available options are force and insert.

The force attribute is useful if the table contains rows with extra discriminator values that are not mapped to a persistent class. This could, for example, occur when working with a legacy database. If force is set to true, Hibernate will specify the allowed discriminator values in the SELECT query even when retrieving all instances of the root class.

The second option, insert, tells Hibernate whether or not to include the discriminator column in SQL INSERTs. Usually, the column should be part of the INSERT statement, but if your discriminator column is also part of a mapped composite identifier you have to set this option to false.

There used to be a @org.hibernate.annotations.ForceDiscriminator annotation which was deprecated in version 3.6 and later removed. Use @DiscriminatorOptions instead.

Discriminator formula

Assuming a legacy database schema where the discriminator is based on inspecting a certain column, we can take advantage of the Hibernate specific @DiscriminatorFormula annotation and map the inheritance model as follows:

Example 297. Single Table discriminator formula
@Entity(name = "Account")
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorFormula(
	"case when debitKey is not null " +
	"then 'Debit' " +
	"else (" +
	"   case when creditKey is not null " +
	"   then 'Credit' " +
	"   else 'Unknown' " +
	"   end) " +
	"end "
)
public static class Account {

	@Id
	private Long id;

	private String owner;

	private BigDecimal balance;

	private BigDecimal interestRate;

	//Getters and setters are omitted for brevity

}

@Entity(name = "DebitAccount")
@DiscriminatorValue(value = "Debit")
public static class DebitAccount extends Account {

	private String debitKey;

	private BigDecimal overdraftFee;

	//Getters and setters are omitted for brevity

}

@Entity(name = "CreditAccount")
@DiscriminatorValue(value = "Credit")
public static class CreditAccount extends Account {

	private String creditKey;

	private BigDecimal creditLimit;

	//Getters and setters are omitted for brevity

}
CREATE TABLE Account (
    id int8 NOT NULL ,
    balance NUMERIC(19, 2) ,
    interestRate NUMERIC(19, 2) ,
    owner VARCHAR(255) ,
    debitKey VARCHAR(255) ,
    overdraftFee NUMERIC(19, 2) ,
    creditKey VARCHAR(255) ,
    creditLimit NUMERIC(19, 2) ,
    PRIMARY KEY ( id )
)

The @DiscriminatorFormula defines a custom SQL clause that can be used to identify a certain subclass type. The @DiscriminatorValue defines the mapping between the result of the @DiscriminatorFormula and the inheritance subclass type.

Implicit discriminator values

Aside from the usual discriminator values assigned to each individual subclass type, the @DiscriminatorValue can take two additional values:

null

If the underlying discriminator column is null, the null discriminator mapping is going to be used.

not null

If the underlying discriminator column has a not-null value that is not explicitly mapped to any entity, the not-null discriminator mapping used.

To understand how these two values work, consider the following entity mapping:

Example 298. @DiscriminatorValue null and not-null entity mapping
@Entity(name = "Account")
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorValue("null")
public static class Account {

	@Id
	private Long id;

	private String owner;

	private BigDecimal balance;

	private BigDecimal interestRate;

	//Getters and setters are omitted for brevity

}

@Entity(name = "DebitAccount")
@DiscriminatorValue("Debit")
public static class DebitAccount extends Account {

	private BigDecimal overdraftFee;

	//Getters and setters are omitted for brevity

}

@Entity(name = "CreditAccount")
@DiscriminatorValue("Credit")
public static class CreditAccount extends Account {

	private BigDecimal creditLimit;

	//Getters and setters are omitted for brevity

}

@Entity(name = "OtherAccount")
@DiscriminatorValue("not null")
public static class OtherAccount extends Account {

	private boolean active;

	//Getters and setters are omitted for brevity

}

The Account class has a @DiscriminatorValue( "null" ) mapping, meaning that any account row which does not contain any discriminator value will be mapped to an Account base class entity. The DebitAccount and CreditAccount entities use explicit discriminator values. The OtherAccount entity is used as a generic account type because it maps any database row whose discriminator column is not explicitly assigned to any other entity in the current inheritance tree.

To visualize how it works, consider the following example:

Example 299. @DiscriminatorValue null and not-null entity persistence
DebitAccount debitAccount = new DebitAccount();
debitAccount.setId(1L);
debitAccount.setOwner("John Doe");
debitAccount.setBalance(BigDecimal.valueOf(100));
debitAccount.setInterestRate(BigDecimal.valueOf(1.5d));
debitAccount.setOverdraftFee(BigDecimal.valueOf(25));

CreditAccount creditAccount = new CreditAccount();
creditAccount.setId(2L);
creditAccount.setOwner("John Doe");
creditAccount.setBalance(BigDecimal.valueOf(1000));
creditAccount.setInterestRate(BigDecimal.valueOf(1.9d));
creditAccount.setCreditLimit(BigDecimal.valueOf(5000));

Account account = new Account();
account.setId(3L);
account.setOwner("John Doe");
account.setBalance(BigDecimal.valueOf(1000));
account.setInterestRate(BigDecimal.valueOf(1.9d));

entityManager.persist(debitAccount);
entityManager.persist(creditAccount);
entityManager.persist(account);

entityManager.unwrap(Session.class).doWork(connection -> {
	try(Statement statement = connection.createStatement()) {
		statement.executeUpdate(
			"insert into Account (DTYPE, active, balance, interestRate, owner, id) " +
			"values ('Other', true, 25, 0.5, 'Vlad', 4)"
		);
	}
});

Map<Long, Account> accounts = entityManager.createQuery(
	"select a from Account a", Account.class)
.getResultList()
.stream()
.collect(Collectors.toMap(Account::getId, Function.identity()));

assertEquals(4, accounts.size());
assertEquals(DebitAccount.class, accounts.get(1L).getClass());
assertEquals(CreditAccount.class, accounts.get(2L).getClass());
assertEquals(Account.class, accounts.get(3L).getClass());
assertEquals(OtherAccount.class, accounts.get(4L).getClass());
INSERT INTO Account (balance, interestRate, owner, overdraftFee, DTYPE, id)
VALUES (100, 1.5, 'John Doe', 25, 'Debit', 1)

INSERT INTO Account (balance, interestRate, owner, overdraftFee, DTYPE, id)
VALUES (1000, 1.9, 'John Doe', 5000, 'Credit', 2)

INSERT INTO Account (balance, interestRate, owner, id)
VALUES (1000, 1.9, 'John Doe', 3)

INSERT INTO Account (DTYPE, active, balance, interestRate, owner, id)
VALUES ('Other', true, 25, 0.5, 'Vlad', 4)

SELECT a.id as id2_0_,
       a.balance as balance3_0_,
       a.interestRate as interest4_0_,
       a.owner as owner5_0_,
       a.overdraftFee as overdraf6_0_,
       a.creditLimit as creditLi7_0_,
       a.active as active8_0_,
       a.DTYPE as DTYPE1_0_ 
FROM   Account a

As you can see, the Account entity row has a value of NULL in the DTYPE discriminator column, while the OtherAccount entity was saved with a DTYPE column value of other which has not explicit mapping.

3.14.3. Joined table

Each subclass can also be mapped to its own table. This is also called table-per-subclass mapping strategy. An inherited state is retrieved by joining with the table of the superclass.

A discriminator column is not required for this mapping strategy. Each subclass must, however, declare a table column holding the object identifier.

Example 300. Join Table
@Entity(name = "Account")
@Inheritance(strategy = InheritanceType.JOINED)
public static class Account {

	@Id
	private Long id;

	private String owner;

	private BigDecimal balance;

	private BigDecimal interestRate;

	//Getters and setters are omitted for brevity

}

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

	private BigDecimal overdraftFee;

	//Getters and setters are omitted for brevity

}

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

	private BigDecimal creditLimit;

	//Getters and setters are omitted for brevity

}
CREATE TABLE Account (
    id BIGINT NOT NULL ,
    balance NUMERIC(19, 2) ,
    interestRate NUMERIC(19, 2) ,
    owner VARCHAR(255) ,
    PRIMARY KEY ( id )
)

CREATE TABLE CreditAccount (
    creditLimit NUMERIC(19, 2) ,
    id BIGINT NOT NULL ,
    PRIMARY KEY ( id )
)

CREATE TABLE DebitAccount (
    overdraftFee NUMERIC(19, 2) ,
    id BIGINT NOT NULL ,
    PRIMARY KEY ( id )
)

ALTER TABLE CreditAccount
ADD CONSTRAINT FKihw8h3j1k0w31cnyu7jcl7n7n
FOREIGN KEY (id) REFERENCES Account

ALTER TABLE DebitAccount
ADD CONSTRAINT FKia914478noepymc468kiaivqm
FOREIGN KEY (id) REFERENCES Account

The primary keys of the CreditAccount and DebitAccount tables are also foreign keys to the superclass table primary key and described by the @PrimaryKeyJoinColumns.

The table name still defaults to the non-qualified class name. Also, if @PrimaryKeyJoinColumn is not set, the primary key / foreign key columns are assumed to have the same names as the primary key columns of the primary table of the superclass.

Example 301. Join Table with @PrimaryKeyJoinColumn
@Entity(name = "Account")
@Inheritance(strategy = InheritanceType.JOINED)
public static class Account {

	@Id
	private Long id;

	private String owner;

	private BigDecimal balance;

	private BigDecimal interestRate;

	//Getters and setters are omitted for brevity

}

@Entity(name = "DebitAccount")
@PrimaryKeyJoinColumn(name = "account_id")
public static class DebitAccount extends Account {

	private BigDecimal overdraftFee;

	//Getters and setters are omitted for brevity

}

@Entity(name = "CreditAccount")
@PrimaryKeyJoinColumn(name = "account_id")
public static class CreditAccount extends Account {

	private BigDecimal creditLimit;

	//Getters and setters are omitted for brevity

}
CREATE TABLE CreditAccount (
    creditLimit NUMERIC(19, 2) ,
    account_id BIGINT NOT NULL ,
    PRIMARY KEY ( account_id )
)

CREATE TABLE DebitAccount (
    overdraftFee NUMERIC(19, 2) ,
    account_id BIGINT NOT NULL ,
    PRIMARY KEY ( account_id )
)

ALTER TABLE CreditAccount
ADD CONSTRAINT FK8ulmk1wgs5x7igo370jt0q005
FOREIGN KEY (account_id) REFERENCES Account

ALTER TABLE DebitAccount
ADD CONSTRAINT FK7wjufa570onoidv4omkkru06j
FOREIGN KEY (account_id) REFERENCES Account

When using polymorphic queries, the base class table must be joined with all subclass tables to fetch every associated subclass instance.

Example 302. Join Table polymorphic query
List<Account> accounts = entityManager
	.createQuery("select a from Account a")
	.getResultList();
SELECT jointablet0_.id AS id1_0_ ,
       jointablet0_.balance AS balance2_0_ ,
       jointablet0_.interestRate AS interest3_0_ ,
       jointablet0_.owner AS owner4_0_ ,
       jointablet0_1_.overdraftFee AS overdraf1_2_ ,
       jointablet0_2_.creditLimit AS creditLi1_1_ ,
       CASE WHEN jointablet0_1_.id IS NOT NULL THEN 1
            WHEN jointablet0_2_.id IS NOT NULL THEN 2
            WHEN jointablet0_.id IS NOT NULL THEN 0
       END AS clazz_
FROM   Account jointablet0_
       LEFT OUTER JOIN DebitAccount jointablet0_1_ ON jointablet0_.id = jointablet0_1_.id
       LEFT OUTER JOIN CreditAccount jointablet0_2_ ON jointablet0_.id = jointablet0_2_.id

The joined table inheritance polymorphic queries can use several JOINS which might affect performance when fetching a large number of entities.

3.14.4. Table per class

A third option is to map only the concrete classes of an inheritance hierarchy to tables. This is called the table-per-concrete-class strategy. Each table defines all persistent states of the class, including the inherited state.

In Hibernate, it is not necessary to explicitly map such inheritance hierarchies. You can map each class as a separate entity root. However, if you wish to use polymorphic associations (e.g. an association to the superclass of your hierarchy), you need to use the union subclass mapping.

Example 303. Table per class
@Entity(name = "Account")
@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
public static class Account {

	@Id
	private Long id;

	private String owner;

	private BigDecimal balance;

	private BigDecimal interestRate;

	//Getters and setters are omitted for brevity

}

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

	private BigDecimal overdraftFee;

	//Getters and setters are omitted for brevity

}

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

	private BigDecimal creditLimit;

	//Getters and setters are omitted for brevity

}
CREATE TABLE Account (
    id BIGINT NOT NULL ,
    balance NUMERIC(19, 2) ,
    interestRate NUMERIC(19, 2) ,
    owner VARCHAR(255) ,
    PRIMARY KEY ( id )
)

CREATE TABLE CreditAccount (
    id BIGINT NOT NULL ,
    balance NUMERIC(19, 2) ,
    interestRate NUMERIC(19, 2) ,
    owner VARCHAR(255) ,
    creditLimit NUMERIC(19, 2) ,
    PRIMARY KEY ( id )
)

CREATE TABLE DebitAccount (
    id BIGINT NOT NULL ,
    balance NUMERIC(19, 2) ,
    interestRate NUMERIC(19, 2) ,
    owner VARCHAR(255) ,
    overdraftFee NUMERIC(19, 2) ,
    PRIMARY KEY ( id )
)

When using polymorphic queries, a UNION is required to fetch the base class table along with all subclass tables as well.

Example 304. Table per class polymorphic query
List<Account> accounts = entityManager
	.createQuery("select a from Account a")
	.getResultList();
SELECT tablepercl0_.id AS id1_0_ ,
       tablepercl0_.balance AS balance2_0_ ,
       tablepercl0_.interestRate AS interest3_0_ ,
       tablepercl0_.owner AS owner4_0_ ,
       tablepercl0_.overdraftFee AS overdraf1_2_ ,
       tablepercl0_.creditLimit AS creditLi1_1_ ,
       tablepercl0_.clazz_ AS clazz_
FROM (
    SELECT    id ,
             balance ,
             interestRate ,
             owner ,
             CAST(NULL AS INT) AS overdraftFee ,
             CAST(NULL AS INT) AS creditLimit ,
             0 AS clazz_
    FROM     Account
    UNION ALL
    SELECT   id ,
             balance ,
             interestRate ,
             owner ,
             overdraftFee ,
             CAST(NULL AS INT) AS creditLimit ,
             1 AS clazz_
    FROM     DebitAccount
    UNION ALL
    SELECT   id ,
             balance ,
             interestRate ,
             owner ,
             CAST(NULL AS INT) AS overdraftFee ,
             creditLimit ,
             2 AS clazz_
    FROM     CreditAccount
) tablepercl0_

Polymorphic queries require multiple UNION queries, so be aware of the performance implications of a large class hierarchy.

3.14.5. Implicit and explicit polymorphism

By default, when you query a base class entity, the polymorphic query will fetch all subclasses belonging to the base type.

However, you can even query interfaces or base classes that don’t belong to the Jakarta Persistence entity inheritance model.

For instance, considering the following DomainModelEntity interface:

Example 305. DomainModelEntity interface
public interface DomainModelEntity<ID> {

    ID getId();

    Integer getVersion();
}

If we have two entity mappings, a Book and a Blog, and the Blog entity is mapped with the @Polymorphism annotation and taking the PolymorphismType.EXPLICIT setting:

Example 306. @Polymorphism entity mapping
@Entity(name = "Event")
public static class Book implements DomainModelEntity<Long> {

	@Id
	private Long id;

	@Version
	private Integer version;

	private String title;

	private String author;

	//Getter and setters omitted for brevity
}

@Entity(name = "Blog")
@Polymorphism(type = PolymorphismType.EXPLICIT)
public static class Blog implements DomainModelEntity<Long> {

	@Id
	private Long id;

	@Version
	private Integer version;

	private String site;

	//Getter and setters omitted for brevity
}

If we have the following entity objects in our system:

Example 307. Domain Model entity objects
Book book = new Book();
book.setId(1L);
book.setAuthor("Vlad Mihalcea");
book.setTitle("High-Performance Java Persistence");
entityManager.persist(book);

Blog blog = new Blog();
blog.setId(1L);
blog.setSite("vladmihalcea.com");
entityManager.persist(blog);

We can now query against the DomainModelEntity interface, and Hibernate is going to fetch only the entities that are either mapped with @Polymorphism(type = PolymorphismType.IMPLICIT) or they are not annotated at all with the @Polymorphism annotation (implying the IMPLICIT behavior):

Example 308. Fetching Domain Model entities using non-mapped base class polymorphism
List<DomainModelEntity> accounts = entityManager
.createQuery(
	"select e " +
	"from org.hibernate.orm.test.inheritance.polymorphism.DomainModelEntity e")
.getResultList();

assertEquals(1, accounts.size());
assertTrue(accounts.get(0) instanceof Book);

Therefore, only the Book was fetched since the Blog entity was marked with the @Polymorphism(type = PolymorphismType.EXPLICIT) annotation, which instructs Hibernate to skip it when executing a polymorphic query against a non-mapped base class.

3.15. Mutability

Immutability can be specified for both entities and attributes.

Unfortunately mutability is an overloaded term. It can refer to either:

  • Whether the internal state of a value can be changed. In this sense, a java.lang.Date is considered mutable because its internal state can be changed by calling Date#setTime, whereas java.lang.String is considered immutable because its internal state cannot be changed. Hibernate uses this distinction for numerous internal optimizations related to dirty checking and making copies.

  • Whether the value is updateable in regard to the database. Hibernate can perform other optimizations based on this distinction.

3.15.1. @Immutable

The @Immutable annotation declares something immutable in the updateability sense. Mutable (updateable) is the implicit condition.

@Immutable is allowed on an entity, attribute, AttributeConverter and UserType. Unfortunately, it has slightly different impacts depending on where it is placed; see the linked sections for details.

3.15.2. Entity immutability

If a specific entity is immutable, it is good practice to mark it with the @Immutable annotation.

Example 309. Immutable entity
@Entity(name = "Event")
@Immutable
public static class Event {

	@Id
	private Long id;

	private Date createdOn;

	private String message;

	//Getters and setters are omitted for brevity

}

Internally, Hibernate is going to perform several optimizations, such as:

  • reducing memory footprint since there is no need to retain the loaded state for the dirty checking mechanism

  • speeding-up the Persistence Context flushing phase since immutable entities can skip the dirty checking process

Considering the following entity is persisted in the database:

Example 310. Persisting an immutable entity
Event event = new Event();
event.setId(1L);
event.setCreatedOn(new Date());
event.setMessage("Hibernate User Guide rocks!");

entityManager.persist(event);

When loading the entity and trying to change its state, Hibernate will skip any modification, therefore no SQL UPDATE statement is executed.

Example 311. The immutable entity ignores any update
Event event = entityManager.find(Event.class, 1L);
log.info("Change event message");
event.setMessage("Hibernate User Guide");
SELECT e.id AS id1_0_0_,
       e.createdOn AS createdO2_0_0_,
       e.message AS message3_0_0_
FROM   event e
WHERE  e.id = 1

-- Change event message

SELECT e.id AS id1_0_0_,
       e.createdOn AS createdO2_0_0_,
       e.message AS message3_0_0_
FROM   event e
WHERE  e.id = 1

@Mutability is not allowed on an entity.

3.15.3. Attribute mutability

The @Immutable annotation may also be used on attributes. The impact varies slightly depending on the exact kind of attribute.

@Mutability on an attribute applies the specified MutabilityPlan to the attribute for handling internal state changes in the values for the attribute.

Attribute immutability - basic

When applied to a basic attribute, @Immutable implies immutability in both the updateable and internal-state sense. E.g.

Example 312. Immutable basic attribute
@Immutable
private Date theDate;

Changes to the theDate attribute are ignored.

Example 313. Immutable basic attribute change
final TheEntity theEntity = session.find( TheEntity.class, 1 );
// this change will be ignored
theEntity.theDate.setTime( Instant.EPOCH.toEpochMilli() );
Attribute immutability - embeddable

To be continued..

Attribute immutability - plural

Plural attributes (@ElementCollection, @OneToMany`, @ManyToMany and @ManyToAny) may also be annotated with @Immutable.

TIP

While most immutable changes are simply discarded, modifying an immutable collection will cause an exception.

Example 314. Persisting an immutable collection
Batch batch = new Batch();
batch.setId(1L);
batch.setName("Change request");

Event event1 = new Event();
event1.setId(1L);
event1.setCreatedOn(new Date());
event1.setMessage("Update Hibernate User Guide");

Event event2 = new Event();
event2.setId(2L);
event2.setCreatedOn(new Date());
event2.setMessage("Update Hibernate Getting Started Guide");

batch.getEvents().add(event1);
batch.getEvents().add(event2);

entityManager.persist(batch);

The Batch entity is mutable. Only the events collection is immutable.

For instance, we can still modify the entity name:

Example 315. Changing the mutable entity
Batch batch = entityManager.find(Batch.class, 1L);
log.info("Change batch name");
batch.setName("Proposed change request");
SELECT b.id AS id1_0_0_,
       b.name AS name2_0_0_
FROM   Batch b
WHERE  b.id = 1

-- Change batch name

UPDATE batch
SET    name = 'Proposed change request'
WHERE  id = 1

However, when trying to modify the events collection:

Example 316. Immutable collections cannot be modified
try {
		Batch batch = entityManager.find( Batch.class, 1L );
		batch.getEvents().clear();
}
catch (Exception e) {
	log.error("Immutable collections cannot be modified");
}
jakarta.persistence.RollbackException: Error while committing the transaction

Caused by: jakarta.persistence.PersistenceException: org.hibernate.HibernateException:

Caused by: org.hibernate.HibernateException: changed an immutable collection instance: [
    org.hibernate.orm.test.mapping.mutability.attribute.PluralAttributeMutabilityTest$Batch.events#1
]
Attribute immutability - entity

To be continued..

3.15.4. AttributeConverter mutability

Declaring @Mutability on an AttributeConverter applies the specified MutabilityPlan to all value mappings (attribute, collection element, etc.) to which the converter is applied.

Declaring @Immutable on an AttributeConverter is shorthand for declaring @Mutability with an immutable MutabilityPlan.

3.15.5. UserType mutability

Similar to AttributeConverter both @Mutability and @Immutable may be declared on a UserType.

@Mutability applies the specified MutabilityPlan to all value mappings (attribute, collection element, etc.) to which the UserType is applied.

@Immutable applies an immutable MutabilityPlan to all value mappings (attribute, collection element, etc.) to which the UserType is applied.

3.15.6. @Mutability

MutabilityPlan is the contract used by Hibernate to abstract mutability concerns, in the sense of internal state changes.

A Java type has an inherent MutabilityPlan based on its JavaType#getMutabilityPlan.

The @Mutability annotation allows a specific MutabilityPlan to be used and is allowed on an attribute, AttributeConverter and UserType. When used on a AttributeConverter or UserType, the specified MutabilityPlan is effective for all basic values to which the AttributeConverter or UserType is applied.

To understand the impact of internal-state mutability, consider the following entity:

Example 317. Basic mutability model
@Entity
public class MutabilityBaselineEntity {
	@Id
	private Integer id;
	@Basic
	private String name;
	@Basic
	private Date activeTimestamp;
}

When dealing with an inherently immutable value, such as a String, there is only one way to update the value:

Example 318. Changing immutable value
Session session = getSession();
MutabilityBaselineEntity entity = session.find( MutabilityBaselineEntity.class, 1 );
entity.setName( "new name" );

During flush, this change will make the entity "dirty" and the changes will be written (UPDATE) to the database.

When dealing with mutable values, however, Hibernate must be aware of both ways to change the value. First, like with the immutable value, we can set the new value:

Example 319. Changing mutable value - setting
Session session = getSession();
MutabilityBaselineEntity entity = session.find( MutabilityBaselineEntity.class, 1 );
entity.setActiveTimestamp( now() );

We can also mutate the existing value:

Example 320. Changing mutable value - mutating
Session session = getSession();
MutabilityBaselineEntity entity = session.find( MutabilityBaselineEntity.class, 1 );
entity.getActiveTimestamp().setTime( now().getTime() );

This mutating example has the same effect as the setting example - they each will make the entity dirty.

3.16. Customizing the domain model

For cases where Hibernate does not provide a built-in way to configure the domain model mapping based on requirements, it provides a very broad and flexible way to adjust the mapping model through its "boot-time model" (defined in the org.hibernate.mapping package) using its @AttributeBinderType meta annotation and corresponding AttributeBinder contract.

An example:

Example 321. AttributeBinder example
/**
 * Custom annotation applying 'Y'/'N' storage semantics to a boolean.
 *
 * The important piece here is `@AttributeBinderType`
 */
@Target({METHOD,FIELD})
@Retention(RUNTIME)
@AttributeBinderType( binder = YesNoBinder.class )
public @interface YesNo {
}

/**
 * The actual binder responsible for configuring the model objects
 */
public class YesNoBinder implements AttributeBinder<YesNo> {
	@Override
	public void bind(
			YesNo annotation,
			MetadataBuildingContext buildingContext,
			PersistentClass persistentClass,
			Property property) {
		( (SimpleValue) property.getValue() ).setJpaAttributeConverterDescriptor(
				new InstanceBasedConverterDescriptor(
						YesNoConverter.INSTANCE,
						buildingContext.getBootstrapContext().getClassmateContext()
				)
		);
	}
}

The important thing to take away here is that both @YesNo and YesNoBinder are custom, user-written code. Hibernate has no inherent understanding of what a @YesNo does or is. It only understands that it has the @AttributeBinderType meta-annotation and knows how to apply that through the corresponding YesNoBinder.

Notice also that @AttributeBinderType provides a type-safe way to perform configuration because the AttributeBinder (YesNoBinder) is handed the custom annotation (@YesNo) to grab its configured attributes. @YesNo does not provide any attributes, but it easily could. Whatever YesNoBinder supports.

4. Bootstrap

The term bootstrapping refers to initializing and starting a software component. In Hibernate, we are specifically talking about the process of building a fully functional SessionFactory instance or EntityManagerFactory instance, for Jakarta Persistence. The process is very different for each.

During the bootstrap process, you might want to customize Hibernate behavior so make sure you check the Configuration Settings section as well.

4.1. Native Bootstrapping

This section discusses the process of bootstrapping a Hibernate SessionFactory. Specifically, it addresses the bootstrapping APIs. For a discussion of the legacy bootstrapping API, see Legacy Bootstrapping.

4.1.1. Building the ServiceRegistry

The first step in native bootstrapping is the building of a ServiceRegistry holding the services Hibernate will need during bootstrapping and at run time.

Actually, we are concerned with building 2 different ServiceRegistries. First is the org.hibernate.boot.registry.BootstrapServiceRegistry. The BootstrapServiceRegistry is intended to hold services that Hibernate needs at both bootstrap and run time. This boils down to 3 services:

org.hibernate.boot.registry.classloading.spi.ClassLoaderService

which controls how Hibernate interacts with ClassLoaders.

org.hibernate.integrator.spi.IntegratorService

which controls the management and discovery of org.hibernate.integrator.spi.Integrator instances.

org.hibernate.boot.registry.selector.spi.StrategySelector

which controls how Hibernate resolves implementations of various strategy contracts. This is a very powerful service, but a full discussion of it is beyond the scope of this guide.

If you are ok with the default behavior of Hibernate in regards to these BootstrapServiceRegistry services (which is quite often the case, especially in stand-alone environments), then you don’t need to explicitly build the BootstrapServiceRegistry.

If you wish to alter how the BootstrapServiceRegistry is built, that is controlled through the org.hibernate.boot.registry.BootstrapServiceRegistryBuilder:

Example 322. Controlling BootstrapServiceRegistry building
BootstrapServiceRegistryBuilder bootstrapRegistryBuilder =
    new BootstrapServiceRegistryBuilder();
// add a custom ClassLoader
bootstrapRegistryBuilder.applyClassLoader(customClassLoader);
// manually add an Integrator
bootstrapRegistryBuilder.applyIntegrator(customIntegrator);

BootstrapServiceRegistry bootstrapRegistry = bootstrapRegistryBuilder.build();

The services of the BootstrapServiceRegistry cannot be extended (added to) nor overridden (replaced).

The second ServiceRegistry is the org.hibernate.boot.registry.StandardServiceRegistry. You will almost always need to configure the StandardServiceRegistry, which is done through org.hibernate.boot.registry.StandardServiceRegistryBuilder:

Example 323. Building a BootstrapServiceRegistryBuilder
// An example using an implicitly built BootstrapServiceRegistry
StandardServiceRegistryBuilder standardRegistryBuilder =
    new StandardServiceRegistryBuilder();

// An example using an explicitly built BootstrapServiceRegistry
BootstrapServiceRegistry bootstrapRegistry =
    new BootstrapServiceRegistryBuilder().build();

StandardServiceRegistryBuilder standardRegistryBuilder =
    new StandardServiceRegistryBuilder(bootstrapRegistry);

A StandardServiceRegistry is also highly configurable via the StandardServiceRegistryBuilder API. See the StandardServiceRegistryBuilder Javadocs for more details.

Some specific methods of interest:

Example 324. Configuring a MetadataSources
ServiceRegistry standardRegistry =
        new StandardServiceRegistryBuilder().build();

MetadataSources sources = new MetadataSources(standardRegistry);

// alternatively, we can build the MetadataSources without passing
// a service registry, in which case it will build a default
// BootstrapServiceRegistry to use.  But the approach shown
// above is preferred
// MetadataSources sources = new MetadataSources();

// add a class using JPA/Hibernate annotations for mapping
sources.addAnnotatedClass(MyEntity.class);

// add the name of a class using JPA/Hibernate annotations for mapping.
// differs from above in that accessing the Class is deferred which is
// important if using runtime bytecode-enhancement
sources.addAnnotatedClassName("org.hibernate.example.Customer");

// Read package-level metadata.
sources.addPackage("hibernate.example");

// Read package-level metadata.
sources.addPackage(MyEntity.class.getPackage());

// Adds the named hbm.xml resource as a source: which performs the
// classpath lookup and parses the XML
sources.addResource("org/hibernate/example/Order.hbm.xml");

// Adds the named JPA orm.xml resource as a source: which performs the
// classpath lookup and parses the XML
sources.addResource("org/hibernate/example/Product.orm.xml");

// Read all mapping documents from a directory tree.
// Assumes that any file named *.hbm.xml is a mapping document.
sources.addDirectory(new File("."));

// Read mappings from a particular XML file
sources.addFile(new File("./mapping.xml"));

// Read all mappings from a jar file.
// Assumes that any file named *.hbm.xml is a mapping document.
sources.addJar(new File("./entities.jar"));

4.1.2. Event Listener registration

The main use cases for an org.hibernate.integrator.spi.Integrator right now are registering event listeners.

Example 325. Configuring an event listener
public class MyIntegrator implements Integrator {

    @Override
    public void integrate(
            Metadata metadata,
            BootstrapContext bootstrapContext,
            SessionFactoryImplementor sessionFactory) {

        // As you might expect, an EventListenerRegistry is the thing with which event
        // listeners are registered
        // It is a service so we look it up using the service registry
        final EventListenerRegistry eventListenerRegistry =
            bootstrapContext.getServiceRegistry().getService(EventListenerRegistry.class);

        // If you wish to have custom determination and handling of "duplicate" listeners,
        // you would have to add an implementation of the
        // org.hibernate.event.service.spi.DuplicationStrategy contract like this
        eventListenerRegistry.addDuplicationStrategy(new CustomDuplicationStrategy());

        // EventListenerRegistry defines 3 ways to register listeners:

        // 1) This form overrides any existing registrations with
        eventListenerRegistry.setListeners(EventType.AUTO_FLUSH,
                                            DefaultAutoFlushEventListener.class);

        // 2) This form adds the specified listener(s) to the beginning of the listener chain
        eventListenerRegistry.prependListeners(EventType.PERSIST,
                                                DefaultPersistEventListener.class);

        // 3) This form adds the specified listener(s) to the end of the listener chain
        eventListenerRegistry.appendListeners(EventType.MERGE,
                                               DefaultMergeEventListener.class);
    }

    @Override
    public void disintegrate(
            SessionFactoryImplementor sessionFactory,
            SessionFactoryServiceRegistry serviceRegistry) {

    }
}

4.1.3. Building the Metadata

The second step in native bootstrapping is the building of an org.hibernate.boot.Metadata object containing the parsed representations of an application domain model and its mapping to a database. The first thing we obviously need to build a parsed representation is the source information to be parsed (annotated classes, hbm.xml files, orm.xml files). This is the purpose of org.hibernate.boot.MetadataSources.

MetadataSources has many other methods as well. Explore its API and Javadocs for more information. Also, all methods on MetadataSources offer fluent-style call chaining::

Example 326. Configuring a MetadataSources with method chaining
ServiceRegistry standardRegistry =
        new StandardServiceRegistryBuilder().build();

MetadataSources sources = new MetadataSources(standardRegistry)
    .addAnnotatedClass(MyEntity.class)
    .addAnnotatedClassName("org.hibernate.example.Customer")
    .addResource("org/hibernate/example/Order.hbm.xml")
    .addResource("org/hibernate/ex