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.
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 mailing lists, forums, IRC, or other ways listed in the Community section.
-
Help improve or translate this documentation. Contact us on the developer mailing list if you have interest.
-
Spread the word. Let the rest of your organization know about the benefits of Hibernate.
System Requirements
Hibernate 6.0 and later versions require at least Java 11 and JDBC 4.2.
Getting Started Guide
New users may want to first look through the Hibernate Getting Started Guide for basic information as well as tutorials. There is also a series of topical guides providing deep dives into various topics.
While having a strong background in SQL is not required to use Hibernate, it certainly helps a lot because it all boils down to SQL statements. Probably even more important is an understanding of data modeling principles. You might want to consider these resources as a good starting point: Understanding the basics of transactions and design patterns such as Unit of Work (PoEAA) or Application Transaction are important as well. These topics will be discussed in the documentation, but a prior understanding will certainly help. |
1. Architecture
1.1. Overview
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:
- 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. TheEntityManagerFactory
is the Jakarta Persistence equivalent of aSessionFactory
and basically, those two converge into the sameSessionFactory
implementation.A
SessionFactory
is very expensive to create, so, for any given database, the application should have only one associatedSessionFactory
. TheSessionFactory
maintains services that Hibernate uses across allSession(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 anEntityManager
.Behind the scenes, the Hibernate
Session
wraps a JDBCjava.sql.Connection
and acts as a factory fororg.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).
2. 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. |
2.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.
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:
2.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
2.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.
2.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:
If provider portability is a concern, you should stick to just these basic types. Java Persistence 2.1 introduced the |
2.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.
@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;
}
@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 supportEAGER
, while support forLAZY
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.
2.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.
@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.
2.2.3. @Formula
@Formula
allows mapping any database computed value as a virtual read-only column.
|
@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
:
@Formula
mappingdoInJPA(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 |
2.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 likejava.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:
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.
2.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:
PhoneType
enumerationpublic 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
@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:
@Enumerated(ORDINAL)
mappingPhone 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
@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:
@Enumerated(STRING)
mappingINSERT 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.
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.
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 So, when using the |
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.
@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.
2.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
.
// 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, and0
for false.
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
2.2.7. Byte
By default, Hibernate maps values of Byte
/ byte
to the TINYINT
JDBC type.
// these will both be mapped using TINYINT
Byte wrapper;
byte primitive;
See Byte array for mapping arrays of bytes.
2.2.8. Short
By default, Hibernate maps values of Short
/ short
to the SMALLINT
JDBC type.
// these will both be mapped using SMALLINT
Short wrapper;
short primitive;
2.2.9. Integer
By default, Hibernate maps values of Integer
/ int
to the INTEGER
JDBC type.
// these will both be mapped using INTEGER
Integer wrapper;
int primitive;
2.2.10. Long
By default, Hibernate maps values of Long
/ long
to the BIGINT
JDBC type.
// these will both be mapped using BIGINT
Long wrapper;
long primitive;
2.2.11. BigInteger
By default, Hibernate maps values of BigInteger
to the NUMERIC
JDBC type.
// will be mapped using NUMERIC
BigInteger wrapper;
2.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
// 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
2.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.
// 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
2.2.14. BigDecimal
By default, Hibernate maps values of BigDecimal
to the NUMERIC
JDBC type.
// will be mapped using NUMERIC
BigDecimal wrapper;
2.2.15. Character
By default, Hibernate maps Character
to the CHAR
JDBC type.
// these will be mapped using CHAR
Character wrapper;
char primitive;
2.2.16. String
By default, Hibernate maps String
to the VARCHAR
JDBC type.
// 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 |
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.
// 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.
2.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 )
.
// 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.
// 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.
2.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:
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:
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:
java.sql.Clob
entityString 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
:
java.sql.Clob
entityProduct 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[]
.
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.
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:
NCLOB
- SQLCREATE TABLE Product (
id INTEGER NOT NULL ,
name VARCHAR(255) ,
warranty nclob ,
PRIMARY KEY ( id )
)
Hibernate can map the NCLOB
to a java.sql.NClob
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:
java.sql.NClob
entityString 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
:
java.sql.NClob
entityProduct 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[]
.
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.
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
}
2.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 )
.
// 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 |
See Handling LOB data for details on mapping to a database BLOB.
2.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:
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.
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:
java.sql.Blob
entitybyte[] 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
:
java.sql.Blob
entityProduct 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[]
).
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
}
2.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 @JdbcType(INTERVAL_SECOND) or by setting hibernate.type.preferred_duration_jdbc_type=INTERVAL_SECOND
|
private Duration duration;
2.2.22. Instant
Instant
is mapped to the TIMESTAMP_UTC
SQL type.
// mapped as TIMESTAMP
private Instant instant;
See Handling temporal data for basics of temporal mapping
2.2.23. LocalDate
LocalDate
is mapped to the DATE
JDBC type.
// mapped as DATE
private LocalDate localDate;
See Handling temporal data for basics of temporal mapping
2.2.24. LocalDateTime
LocalDateTime
is mapped to the TIMESTAMP
JDBC type.
// mapped as TIMESTAMP
private LocalDateTime localDateTime;
See Handling temporal data for basics of temporal mapping
2.2.25. LocalTime
LocalTime
is mapped to the TIME
JDBC type.
// mapped as TIME
private LocalTime localTime;
See Handling temporal data for basics of temporal mapping
2.2.26. OffsetDateTime
OffsetDateTime
is mapped to the TIMESTAMP
or TIMESTAMP_WITH_TIMEZONE
JDBC type
depending on the database.
// 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
2.2.27. OffsetTime
OffsetTime
is mapped to the TIME
or TIME_WITH_TIMEZONE
JDBC type
depending on the database.
// 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
2.2.28. TimeZone
TimeZone
is mapped to VARCHAR
JDBC type.
// mapped as VARCHAR
private TimeZone timeZone;
2.2.29. ZonedDateTime
ZonedDateTime
is mapped to the TIMESTAMP
or TIMESTAMP_WITH_TIMEZONE
JDBC type
depending on the database.
// 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
2.2.30. ZoneOffset
ZoneOffset
is mapped to VARCHAR
JDBC type.
// mapped as VARCHAR
private ZoneOffset zoneOffset;
2.2.31. Calendar
See Handling temporal data for basics of temporal mapping See Using a specific time zone for basics of time-zone handling
2.2.32. Date
See Handling temporal data for basics of temporal mapping See Using a specific time zone for basics of time-zone handling
2.2.33. Time
See Handling temporal data for basics of temporal mapping See Using a specific time zone for basics of time-zone handling
2.2.34. Timestamp
See Handling temporal data for basics of temporal mapping See Using a specific time zone for basics of time-zone handling
2.2.35. Class
Hibernate maps Class
references to VARCHAR
JDBC type
// mapped as VARCHAR
private Class<?> clazz;
2.2.36. Currency
Hibernate maps Currency
references to VARCHAR
JDBC type
// mapped as VARCHAR
private Currency currency;
2.2.37. Locale
Hibernate maps Locale
references to VARCHAR
JDBC type
// mapped as VARCHAR
private Locale locale;
2.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 |
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.
2.2.39. InetAddress
By default, Hibernate will map InetAddress
to the INET
SQL type and fallback to BINARY
if necessary.
private InetAddress address;
2.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.
@JdbcTypeCode( SqlTypes.JSON )
private Map<String, String> stringMap;
2.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.
@JdbcTypeCode( SqlTypes.SQLXML )
private Map<String, StringNode> stringMap;
2.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
.
Short[] wrapper;
short[] primitive;
2.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
.
List<Short> list;
SortedSet<Short> sortedSet;
2.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 |
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:
-
Local to the mapping
-
On the associated
AttributeConverter
implementation class (if one) -
On the value’s Java type
In most cases, the fallback defined by JavaType#getMutabilityPlan
is the proper strategy.
Hibernate uses MutabilityPlan
to:
-
Check whether a value is considered dirty
-
Make deep copies
-
Marshal values to and from the second-level cache
Generally speaking, immutable values perform better in all of these cases
-
To check for dirtiness, Hibernate just needs to check object identity (
==
) as opposed to equality (Object#equals
). -
The same value instance can be used as the deep copy of itself.
-
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 |
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
-
If
@JavaType
is also used, that specificJavaType
is used for the converter’s "domain type". Otherwise, the Java type defined by the converter as its "domain type" is resolved against theJavaTypeRegistry
-
If
@JdbcType
or@JdbcTypeCode
is used, the indicatedJdbcType
is used and the converted "relational Java type" is determined byJdbcType#getJdbcRecommendedJavaTypeMapping
. Otherwise, the Java type defined by the converter as its relational type is used and theJdbcType
is determined byJdbcType#getRecommendedJdbcType
-
The
MutabilityPlan
can be specified using@Mutability
or@Immutable
on theAttributeConverter
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.
-
For enum values, we check for an explicit
@Enumerated
and create an enumeration mapping. Note that this resolution still uses any explicitJdbcType
indicators -
For temporal values, we check for
@Temporal
and create an enumeration mapping. Note that this resolution still uses any explicitJdbcType
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
.
2.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
2.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.
|
For databases with no See also Handling LOB data regarding similar limitation for databases which do not support
explicit |
Considering we have the following database table:
NVARCHAR
- SQLCREATE 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.
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
}
2.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 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 |
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[]
, orbyte[]
.
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 |
2.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
andjava.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 |
When using java.util.Date
or java.util.Calendar
, Hibernate assumes TIMESTAMP
. To alter that,
use @Temporal
.
// 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.
2.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 |
@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;
2.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.
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.
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:
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:
Money
typepublic 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:
Account
entity using the Money
typepublic 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:
MoneyConverter
implementing the Jakarta Persistence AttributeConverter
interfacepublic 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.
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:
-
the basic value
-
the
AttributeConverter
class -
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 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:
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:
Caption
Java objectpublic 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:
Caption
Java object AttributeConverterpublic 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.
Caption
property using the DB data representationPhoto 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
.
Caption
property using the Java Object representationSessionFactoryImplementor 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.
2.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.
2.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.
2.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.
@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.
@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 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.
See also AttributeConverter
Mutability Plan.
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.
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
@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
@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.
@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.
@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
@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
}
2.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.
@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
}
@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:
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:
@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.
2.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 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:
-
Using the dedicated generators provided by Hibernate
-
@CurrentTimestamp
-@CurrentTimestamp
-
@CreationTimestamp
-@CreationTimestamp
-
@UpdateTimestamp
-@UpdateTimestamp
annotation -
@Generated
-@Generated
annotation -
@GeneratorType
- is deprecated and not covered here -
Using a custom generation strategy - Custom generation strategy
@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
@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)
@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)
@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.
@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
@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.
@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
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 UUID.randomUUID();
}
}
See @ValueGenerationType
and AnnotationValueGeneration
for details of each contract
2.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.
@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.
@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.
@ColumnTransformer
and a composite typedoInJPA(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
2.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 Throughout this chapter and thereafter, for brevity sake, embeddable types may also be referred to as embeddable. |
@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.
2.3.1. Component / Embedded
Most often, embeddable types are used to group multiple basic type mappings and reuse them across several entities.
@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:
|
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.
@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.
2.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:
@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:
@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
2.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.
2.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.
2.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. |
2.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.
@Target
mapping usagepublic 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 tye, which is GPS
in this case,
hence the @Target
annotation is used to provide this information.
Assuming we have persisted the following City
entity:
@Target
persist exampledoInJPA(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:
@Target
fetching exampledoInJPA(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.
2.3.7. @Parent
mapping
The Hibernate-specific @Parent
annotation allows you to reference the owner entity from within an embeddable.
@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:
@Parent
persist exampledoInJPA(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:
@Parent
fetching exampledoInJPA(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.
2.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:
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.
EmbeddableInstantiator
- Implementationpublic 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:
@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:
@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.
@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;
}
2.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:
CompositeUserType
- Domain typepublic 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.
CompositeUserType
- Implementationpublic 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:
@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.
@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;
}
2.3.10. Embeddables and ImplicitNamingStrategy
The |
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.
@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.
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.
2.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
:
@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:
@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.
2.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 Throughout this chapter and thereafter, entity types will be simply referred to as entity. |
2.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.
2.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. |
2.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.
2.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.
2.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. |
The placement of the @Id
annotation marks the persistence state access strategy.
@Id
private Long id;
Hibernate offers multiple identifier generation strategies, see the Identifier Generators chapter for more about this topic.
2.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.
@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.
@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.
@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.
book
table located in the public
catalogcreate 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.
@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.
book
table located in the library
schemacreate 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.
@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 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
|
2.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:
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:
@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
}
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:
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:
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:
@Entity(name = "Library")
public static class Library {
@Id
private Long id;
private String name;
@OneToMany(cascade = CascadeType.ALL)
@JoinColumn(name = "book_id")
private Set<Book> books = new HashSet<>();
//Getters and setters are omitted for brevity
}
@Entity(name = "Book")
public static class Book {
@Id
@GeneratedValue
private Long id;
private String title;
private String author;
//Getters and setters are omitted for brevity
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
Book book = (Book) o;
return Objects.equals(id, book.id);
}
@Override
public int hashCode() {
return Objects.hash(id);
}
}
It turns out that this still breaks when adding transient instance of Book
to a set as we saw in the last example:
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
:
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.
@Entity(name = "Library")
public static class Library {
@Id
private Long id;
private String name;
@OneToMany(cascade = CascadeType.ALL)
@JoinColumn(name = "book_id")
private Set<Book> books = new HashSet<>();
//Getters and setters are omitted for brevity
}
@Entity(name = "Book")
public static class Book {
@Id
@GeneratedValue
private Long id;
private String title;
private String author;
@NaturalId
private String isbn;
//Getters and setters are omitted for brevity
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
Book book = (Book) o;
return Objects.equals(isbn, book.isbn);
}
@Override
public int hashCode() {
return Objects.hash(isbn);
}
}
This time, when adding a Book
to the Library
Set
, you can retrieve the Book
even after it’s being persisted:
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 It’s possible to use the entity identifier for equality check, but it needs a workaround:
|
For details on mapping the identifier, see the Identifiers chapter.
2.4.8. Mapping the entity to a SQL query
You can map an entity to a SQL query using the @Subselect
annotation.
@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
.
@Subselect
entityscope.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:
@Subselect
entityscope.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 With the |
2.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.
Identifiable
interfacepublic 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:
Identifiable
interfacedoInHibernate(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.
2.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.
|
@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.
2.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 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, |
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. |
2.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.
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 forjpa
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 followsImplicitNamingStrategyJpaCompliantImpl
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.
2.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 But the point here is the separation of concerns. The |
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
/*
* 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.
2.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.
2.6.1. 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 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. |
2.6.2. 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.
2.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.
@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;
}
}
2.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:
@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:
@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:
@Entity(name = "Book")
public static class Book {
@Id
private Long id;
private String title;
@ElementCollection
@CollectionTable(
name = "book_author",
joinColumns = @JoinColumn(name = "book_id")
)
private List<Author> authors = new ArrayList<>();
//Getters and setters are omitted for brevity
}
2.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.
2.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.
@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
@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. |
2.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 thejakarta.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. Hibernate does allow composite identifiers to be defined without a "primary key class" via multiple |
The attributes making up the composition can be either basic, composite or @ManyToOne
. Note especially that collection and one-to-one
are never appropriate.
2.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.
@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:
@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 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. |
2.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.
@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.
@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.
@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. |
2.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.
@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.
Book book = entityManager.find(Book.class, new Book(
author,
publisher,
"High-Performance Java Persistence"
));
assertEquals("Vlad Mihalcea", book.getAuthor().getName());
2.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.
@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:
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.
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.
2.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.
2.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.
2.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).
@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 |
To specify the sequence name explicitly, the simplest form is to specify @GeneratedValue#generator
.
@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.
@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.
@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").
2.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 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 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. |
2.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.
@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.
@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:
for (long i = 1; i <= 3; i++) {
Product product = new Product();
product.setName(String.format("Product %d", i));
entityManager.persist(product);
}
select
tbl.product_id
from
table_identifier tbl
where
tbl.table_name = ?
for update
-- binding parameter [1] - [Product]
insert
into
table_identifier
(table_name, product_id)
values
(?, ?)
-- binding parameter [1] - [Product]
-- binding parameter [2] - [1]
update
table_identifier
set
product_id= ?
where
product_id= ?
and table_name= ?
-- binding parameter [1] - [6]
-- binding parameter [2] - [1]
select
tbl.product_id
from
table_identifier tbl
where
tbl.table_name= ? for update
update
table_identifier
set
product_id= ?
where
product_id= ?
and table_name= ?
-- binding parameter [1] - [11]
-- binding parameter [2] - [6]
insert
into
Product
(product_name, id)
values
(?, ?)
-- binding parameter [1] as [VARCHAR] - [Product 1]
-- binding parameter [2] as [BIGINT] - [1]
insert
into
Product
(product_name, id)
values
(?, ?)
-- binding parameter [1] as [VARCHAR] - [Product 2]
-- binding parameter [2] as [BIGINT] - [2]
insert
into
Product
(product_name, id)
values
(?, ?)
-- binding parameter [1] as [VARCHAR] - [Product 3]
-- binding parameter [2] as [BIGINT] - [3]
2.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).
@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
.
@Entity(name = "Book")
public static class Book {
@Id
@GeneratedValue(generator = "custom-uuid")
@GenericGenerator(
name = "custom-uuid",
strategy = "org.hibernate.id.UUIDGenerator",
parameters = {
@Parameter(
name = "uuid_gen_strategy_class",
value = "org.hibernate.id.uuid.CustomVersionOneStrategy"
)
}
)
private UUID id;
private String title;
private String author;
//Getters and setters are omitted for brevity
}
2.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 |
2.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.
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:
-
The configuration annotation - here,
@Sequence
. This is the type-safety aspect, rather than relying on untyped configuration properties in a Map, etc. -
The
Member
to which annotation was applied. This allows access to the Java type of the identifier attribute, etc. -
CustomIdGeneratorCreationContext
is a "parameter object" providing access to things often useful for identifier generators.
2.7.15. Using @GenericGenerator
|
@GenericGenerator
allows integration of any Hibernate org.hibernate.id.IdentifierGenerator
implementation, including any of the specific ones discussed here and any custom ones.
@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:
@GenericGenerator
mappingfor (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 roundtrips, therefore reducing the overall transaction response time.
2.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.
@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.
@MapsId
persist exampledoInJPA(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
.
@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 |
2.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.
@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.
@RowId
exampleProduct product = entityManager.find(Product.class, 1L);
product.setName("Smart phone");
SELECT
p.id as id1_0_0_,
p."name" as name2_0_0_,
p."number" as number3_0_0_,
p.ROWID as rowid_0_
FROM
Product p
WHERE
p.id = ?
-- binding parameter [1] as [BIGINT] - [1]
-- extracted value ([name2_0_0_] : [VARCHAR]) - [Mobile phone]
-- extracted value ([number3_0_0_] : [VARCHAR]) - [123-456-7890]
-- extracted ROWID value: AAAwkBAAEAAACP3AAA
UPDATE
Product
SET
"name" = ?,
"number" = ?
WHERE
ROWID = ?
-- binding parameter [1] as [VARCHAR] - [Smart phone]
-- binding parameter [2] as [VARCHAR] - [123-456-7890]
-- binding parameter [3] as ROWID - [AAAwkBAAEAAACP3AAA]
2.8. Associations
Associations describe how two or more entities form a relationship based on a database joining semantics.
2.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.
@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.
@ManyToOne
association lifecyclePerson person = new Person();
entityManager.persist(person);
Phone phone = new Phone("123-456-7890");
phone.setPerson(person);
entityManager.persist(phone);
entityManager.flush();
phone.setPerson(null);
INSERT INTO Person ( id )
VALUES ( 1 )
INSERT INTO Phone ( number, person_id, id )
VALUES ( '123-456-7890', 1, 2 )
UPDATE Phone
SET number = '123-456-7890',
person_id = NULL
WHERE id = 2
2.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.
@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
associationPerson 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. On the other hand, a bidirectional |
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.
@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 |
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.
@OneToMany
with an owner @ManyToOne
side lifecyclePerson 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.
2.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
@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
@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.
@OneToOne
lifecyclePhone 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.
@OneToOne
unique constraintPhoneDetails 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.
@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.
2.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
@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.
@ManyToMany
lifecyclePerson 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 For example, if
|
By simply removing the parent-side, Hibernate can safely remove the associated link records as you can see in the following example:
@ManyToMany
entity removalPerson 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.
@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:
@ManyToMany
lifecyclePerson 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.
Bidirectional many-to-many with a link entity
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.
@Entity(name = "Person")
public static class Person implements Serializable {
@Id
@GeneratedValue
private Long id;
@NaturalId
private String registrationNumber;
@OneToMany(
mappedBy = "person",
cascade = CascadeType.ALL,
orphanRemoval = true
)
private List<PersonAddress> addresses = new ArrayList<>();
//Getters and setters are omitted for brevity
public void addAddress(Address address) {
PersonAddress personAddress = new PersonAddress(this, address);
addresses.add(personAddress);
address.getOwners().add(personAddress);
}
public void removeAddress(Address address) {
PersonAddress personAddress = new PersonAddress(this, address);
address.getOwners().remove(personAddress);
addresses.remove(personAddress);
personAddress.setPerson(null);
personAddress.setAddress(null);
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
Person person = (Person) o;
return Objects.equals(registrationNumber, person.registrationNumber);
}
@Override
public int hashCode() {
return Objects.hash(registrationNumber);
}
}
@Entity(name = "PersonAddress")
public static class PersonAddress implements Serializable {
@Id
@ManyToOne
private Person person;
@Id
@ManyToOne
private Address address;
//Getters and setters are omitted for brevity
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
PersonAddress that = (PersonAddress) o;
return Objects.equals(person, that.person) &&
Objects.equals(address, that.address);
}
@Override
public int hashCode() {
return Objects.hash(person, address);
}
}
@Entity(name = "Address")
public static class Address implements Serializable {
@Id
@GeneratedValue
private Long id;
private String street;
@Column(name = "`number`")
private String number;
private String postalCode;
@OneToMany(
mappedBy = "address",
cascade = CascadeType.ALL,
orphanRemoval = true
)
private List<PersonAddress> owners = new ArrayList<>();
//Getters and setters are omitted for brevity
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
Address address = (Address) o;
return Objects.equals(street, address.street) &&
Objects.equals(number, address.number) &&
Objects.equals(postalCode, address.postalCode);
}
@Override
public int hashCode() {
return Objects.hash(street, number, postalCode);
}
}
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 PersonAddress (
person_id BIGINT NOT NULL ,
address_id BIGINT NOT NULL ,
PRIMARY KEY ( person_id, address_id )
)
ALTER TABLE Person
ADD CONSTRAINT UK_23enodonj49jm8uwec4i7y37f
UNIQUE (registrationNumber)
ALTER TABLE PersonAddress
ADD CONSTRAINT FK8b3lru5fyej1aarjflamwghqq
FOREIGN KEY (person_id) REFERENCES Person
ALTER TABLE PersonAddress
ADD CONSTRAINT FK7p69mgialumhegyl4byrh65jk
FOREIGN KEY (address_id) REFERENCES Address
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 For more details, see the composite identifiers with associations section. |
The entity state transitions are better managed than in the previous bidirectional @ManyToMany
case.
Person person1 = new Person("ABC-123");
Person person2 = new Person("DEF-456");
Address address1 = new Address("12th Avenue", "12A", "4005A");
Address address2 = new Address("18th Avenue", "18B", "4007B");
entityManager.persist(person1);
entityManager.persist(person2);
entityManager.persist(address1);
entityManager.persist(address2);
person1.addAddress(address1);
person1.addAddress(address2);
person2.addAddress(address1);
entityManager.flush();
log.info("Removing address");
person1.removeAddress(address1);
INSERT INTO Person ( registrationNumber, id )
VALUES ( 'ABC-123', 1 )
INSERT INTO Person ( registrationNumber, id )
VALUES ( 'DEF-456', 2 )
INSERT INTO Address ( number, postalCode, street, id )
VALUES ( '12A', '4005A', '12th Avenue', 3 )
INSERT INTO Address ( number, postalCode, street, id )
VALUES ( '18B', '4007B', '18th Avenue', 4 )
INSERT INTO PersonAddress ( person_id, address_id )
VALUES ( 1, 3 )
INSERT INTO PersonAddress ( person_id, address_id )
VALUES ( 1, 4 )
INSERT INTO PersonAddress ( person_id, address_id )
VALUES ( 2, 3 )
DELETE FROM PersonAddress
WHERE person_id = 1 AND address_id = 3
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.
2.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, |
Considering the following City
and Person
entity mappings:
@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:
@NotFound
persist exampleCity 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:
@NotFound
- find existing entity examplePerson person = entityManager.find( Person.class, 1 );
assertEquals( "New York", person.getCity().getName() );
However, if we 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:
@NotFound
- find non-existing City examplePerson 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.
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.
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" );
2.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:
-
The column and mapping for the discriminator
-
The column and mapping for the key
-
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:
-
@AnyDiscriminator
allows re-using theDiscriminatorType
simplified mappings from Jakarta Persistence for the common cases -
@JavaType
-
@JdbcType
-
@JdbcTypeCode
2.8.7. Example using @Any mapping
For this example, consider the following Property
class hierarchy:
Property
class hierarchypublic 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
:
@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:
@Any
mapping persist exampleIntegerProperty 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:
@Any
mapping query examplePropertyHolder 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.
@Any
mapping usage@Entity
@Table(name = "property_holder")
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:
-
The use of
@ManyToAny
instead of@Any
-
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.
@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:
@ManyToAny
mapping persist exampleIntegerProperty 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:
@ManyToAny
mapping query examplePropertyRepository 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
2.8.8. @JoinFormula
mapping
The @JoinFormula
annotation is used to customize the join between a child Foreign Key and a parent row Primary Key.
@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:
@JoinFormula
mapping usageCountry 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:
@JoinFormula
mapping usagedoInJPA(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.
2.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
.
@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:
@JoinColumnOrFormula
persist exampleCountry 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:
@JoinColumnOrFormula
fetching exampledoInJPA(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.
2.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 Embeddables which are used as a collection element, Map value or Map key may not themselves define collections |
2.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. See Wrappers
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:
-
if an array → ARRAY
-
if a
List
→ LIST -
if a
SortedSet
→ SORTED_SET -
if a
Set
→ SET -
if a
SortedMap
→ SORTED_MAP -
if a
Map
→ MAP -
else
Collection
→ BAG
2.9.2. Mapping Lists
java.util.List
defines a collection of ordered, non-unique elements.
@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
.
@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.
@Entity
public class EntityWithIndexBasedList {
// ...
@ElementCollection
@OrderColumn(name = "name_index")
@ListIndexBase(1)
private List<Name> names;
}
2.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
.
@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
ororg.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.
@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
:
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.
2.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 |
@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
@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
2.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.
2.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
-
@Embeddable
public class Phone {
private String type;
@Column(name = "`number`")
private String number;
//Getters and setters are omitted for brevity
}
First, a 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
}
// 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.
2.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:
@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.
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.
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.
2.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
:
@Entity
@CollectionTypeRegistration( type = UniqueListType.class, classification = CollectionClassification.LIST )
public class TheEntityWithUniqueListRegistration {
@ElementCollection
private List<String> strings;
// ...
}
This example behaves exactly as in @CollectionType.
2.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
2.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:
@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.
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.
@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
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
@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.
2.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:
@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:
@OrderBy
list select statementSELECT
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 If no property is specified (e.g. |
Another ordering option is to use the @OrderColumn
annotation:
@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:
@OrderColumn
list select statementselect
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
.
@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:
@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.
@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.
@ListIndexBase
persist examplePerson 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.
@OrderBy
mapping example@Entity(name = "Person")
public static class Person {
@Id
private Long id;
private String name;
@OneToMany(
mappedBy = "person",
cascade = CascadeType.ALL
)
@org.hibernate.annotations.OrderBy(
clause = "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:
@OrderBy
fetching examplePerson 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
2.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:
@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.
@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);
}
}
2.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.
@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:
@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:
@OneToMany(mappedBy = "person", cascade = CascadeType.ALL)
@SortNatural
private SortedSet<Phone> phones = new TreeSet<>();
//Getters and setters are omitted for brevity
Before v6, |
2.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 aCalendar
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.
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:
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 |
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:
@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:
PhoneNumber
interface and the MobilePhone
class typepublic 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.
@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:
@MapKeyClass
persist examplePerson 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:
@MapKeyClass
fetch examplePerson 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 |
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.
@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
2.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. |
2.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)
.
@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 )
)
2.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.
@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.
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;
// ...
}
2.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 its identifier (PK).
2.10.1. Natural Id Mapping
Natural ids are defined in terms of one or more persistent attributes.
@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
}
@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);
}
}
@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);
}
}
2.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. |
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:
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. |
2.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.
@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 |
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.
@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
}
2.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.
2.11.1. Partition Key Mapping
Partition keys are defined in terms of one or more persistent attributes.
@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=?
2.12. 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. |
2.12.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 Map
s 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.
<!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:
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:
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. |
2.13. 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.
2.13.1. MappedSuperclass
In the following domain model class hierarchy, a DebitAccount
and a CreditAccount
share the same Account
base class.
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.
@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 |
2.13.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. |
@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.
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.
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 |
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
The The second option, |
There used to be a |
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:
@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:
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:
null
and not-null
entity persistenceDebitAccount 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.
2.13.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.
@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 The table name still defaults to the non-qualified class name.
Also, if |
@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.
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. |
2.13.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.
@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.
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. |
2.13.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:
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:
@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:
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):
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.
2.14. 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 callingDate#setTime
, whereasjava.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.
2.14.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.
2.14.2. Entity immutability
If a specific entity is immutable, it is good practice to mark it with the @Immutable
annotation.
@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:
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.
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.
2.14.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.
@Immutable
private Date theDate;
Changes to the theDate
attribute are ignored.
final TheEntity theEntity = session.find( TheEntity.class, 1 );
// this change will be ignored
theEntity.theDate.setTime( Instant.EPOCH.toEpochMilli() );
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.
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:
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:
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
]
2.14.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
.
2.14.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.
2.14.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:
@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:
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:
Session session = getSession();
MutabilityBaselineEntity entity = session.find( MutabilityBaselineEntity.class, 1 );
entity.setActiveTimestamp( now() );
We can also mutate the existing value:
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.
2.15. 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:
/**
* 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.
3. 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 Configurations section as well. |
3.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.
3.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
ClassLoader
s. 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 |
If you wish to alter how the BootstrapServiceRegistry
is built, that is controlled through the org.hibernate.boot.registry.BootstrapServiceRegistryBuilder
:
BootstrapServiceRegistry
buildingBootstrapServiceRegistryBuilder 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 |
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
:
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:
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"));
3.1.2. Event Listener registration
The main use cases for an org.hibernate.integrator.spi.Integrator
right now are registering event listeners.
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) {
}
}
3.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::
MetadataSources
with method chainingServiceRegistry 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/example/Product.orm.xml");
Once we have the sources of mapping information defined, we need to build the Metadata
object.
If you are ok with the default behavior in building the Metadata then you can simply call the buildMetadata
method of the MetadataSources
.
Notice that a |
However, if you wish to adjust the process of building Metadata
from MetadataSources
,
you will need to use the MetadataBuilder
as obtained via MetadataSources#getMetadataBuilder
.
MetadataBuilder
allows a lot of control over the Metadata
building process.
See its Javadocs for full details.
MetadataBuilder
ServiceRegistry standardRegistry =
new StandardServiceRegistryBuilder().build();
MetadataSources sources = new MetadataSources(standardRegistry);
MetadataBuilder metadataBuilder = sources.getMetadataBuilder();
// Use the JPA-compliant implicit naming strategy
metadataBuilder.applyImplicitNamingStrategy(
ImplicitNamingStrategyJpaCompliantImpl.INSTANCE);
// specify the schema name to use for tables, etc when none is explicitly specified
metadataBuilder.applyImplicitSchemaName("my_default_schema");
// specify a custom Attribute Converter
metadataBuilder.applyAttributeConverter(myAttributeConverter);
Metadata metadata = metadataBuilder.build();
3.1.4. Building the SessionFactory
The final step in native bootstrapping is to build the SessionFactory
itself.
Much like discussed above, if you are ok with the default behavior of building a SessionFactory
from a Metadata
reference, you can simply call the buildSessionFactory
method on the Metadata
object.
However, if you would like to adjust that building process, you will need to use SessionFactoryBuilder
as obtained via Metadata#getSessionFactoryBuilder
. Again, see its Javadocs for more details.
StandardServiceRegistry standardRegistry = new StandardServiceRegistryBuilder()
.configure("org/hibernate/example/hibernate.cfg.xml")
.build();
Metadata metadata = new MetadataSources(standardRegistry)
.addAnnotatedClass(MyEntity.class)
.addAnnotatedClassName("org.hibernate.example.Customer")
.addResource("org/hibernate/example/Order.hbm.xml")
.addResource("org/hibernate/example/Product.orm.xml")
.getMetadataBuilder()
.applyImplicitNamingStrategy(ImplicitNamingStrategyJpaCompliantImpl.INSTANCE)
.build();
SessionFactory sessionFactory = metadata.getSessionFactoryBuilder()
.applyBeanManager(getBeanManager())
.build();
The bootstrapping API is quite flexible, but in most cases it makes the most sense to think of it as a 3 step process:
-
Build the
StandardServiceRegistry
-
Build the
Metadata
-
Use those 2 to build the
SessionFactory
SessionFactory
via SessionFactoryBuilder
StandardServiceRegistry standardRegistry = new StandardServiceRegistryBuilder()
.configure("org/hibernate/example/hibernate.cfg.xml")
.build();
Metadata metadata = new MetadataSources(standardRegistry)
.addAnnotatedClass(MyEntity.class)
.addAnnotatedClassName("org.hibernate.example.Customer")
.addResource("org/hibernate/example/Order.hbm.xml")
.addResource("org/hibernate/example/Product.orm.xml")
.getMetadataBuilder()
.applyImplicitNamingStrategy(ImplicitNamingStrategyJpaCompliantImpl.INSTANCE)
.build();
SessionFactoryBuilder sessionFactoryBuilder = metadata.getSessionFactoryBuilder();
// Supply a SessionFactory-level Interceptor
sessionFactoryBuilder.applyInterceptor(new CustomSessionFactoryInterceptor());
// Add a custom observer
sessionFactoryBuilder.addSessionFactoryObservers(new CustomSessionFactoryObserver());
// Apply a CDI BeanManager (for JPA event listeners)
sessionFactoryBuilder.applyBeanManager(getBeanManager());
SessionFactory sessionFactory = sessionFactoryBuilder.build();
3.2. Jakarta Persistence Bootstrapping
Bootstrapping Hibernate as a Jakarta Persistence provider can be done in a Jakarta Persistence-spec compliant manner or using a proprietary bootstrapping approach. The standardized approach has some limitations in certain environments, but aside from those, it is highly recommended that you use Jakarta Persistence-standardized bootstrapping.
3.2.1. Jakarta Persistence-compliant bootstrapping
In Jakarta Persistence, we are ultimately interested in bootstrapping a jakarta.persistence.EntityManagerFactory
instance.
The Jakarta Persistence specification defines two primary standardized bootstrap approaches depending on how the application intends to access the jakarta.persistence.EntityManager
instances from an EntityManagerFactory
.
It uses the terms EE and SE for these two approaches, but those terms are very misleading in this context. What the Jakarta Persistence spec calls EE bootstrapping implies the existence of a container (EE, OSGi, etc), who’ll manage and inject the persistence context on behalf of the application. What it calls SE bootstrapping is everything else. We will use the terms container-bootstrapping and application-bootstrapping in this guide.
For compliant container-bootstrapping, the container will build an EntityManagerFactory
for each persistent-unit defined in the META-INF/persistence.xml
configuration file
and make that available to the application for injection via the jakarta.persistence.PersistenceUnit
annotation or via JNDI lookup.
EntityManagerFactory
@PersistenceUnit
private EntityManagerFactory emf;
Or, in case you have multiple Persistence Units (e.g. multiple persistence.xml
configuration files),
you can inject a specific EntityManagerFactory
by Unit name:
EntityManagerFactory
@PersistenceUnit(
unitName = "CRM"
)
private EntityManagerFactory entityManagerFactory;
The META-INF/persistence.xml
file looks as follows:
<persistence xmlns="http://xmlns.jcp.org/xml/ns/persistence"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/persistence
http://xmlns.jcp.org/xml/ns/persistence/persistence_2_1.xsd"
version="2.1">
<persistence-unit name="CRM">
<description>
Persistence unit for Hibernate User Guide
</description>
<provider>org.hibernate.jpa.HibernatePersistenceProvider</provider>
<class>org.hibernate.documentation.userguide.Document</class>
<properties>
<property name="jakarta.persistence.jdbc.driver"
value="org.h2.Driver" />
<property name="jakarta.persistence.jdbc.url"
value="jdbc:h2:mem:db1;DB_CLOSE_DELAY=-1" />
<property name="jakarta.persistence.jdbc.user"
value="sa" />
<property name="jakarta.persistence.jdbc.password"
value="" />
<property name="hibernate.show_sql"
value="true" />
<property name="hibernate.hbm2ddl.auto"
value="update" />
</properties>
</persistence-unit>
</persistence>
For compliant application-bootstrapping, rather than the container building the EntityManagerFactory
for the application, the application builds the EntityManagerFactory
itself using the jakarta.persistence.Persistence
bootstrap class.
The application creates an EntityManagerFactory
by calling the createEntityManagerFactory
method:
EntityManagerFactory
// Create an EMF for our CRM persistence-unit.
EntityManagerFactory emf = Persistence.createEntityManagerFactory("CRM");
If you don’t want to provide a |
To inject the default Persistence Context, you can use the @PersistenceContext
annotation.
EntityManager
@PersistenceContext
private EntityManager em;
To inject a specific Persistence Context,
you can use the @PersistenceContext
annotation,
and you can even pass EntityManager
-specific properties using the
@PersistenceProperty
annotation.
EntityManager
@PersistenceContext(
unitName = "CRM",
properties = {
@PersistenceProperty(
name="org.hibernate.flushMode",
value= "MANUAL"
)
}
)
private EntityManager entityManager;
If you would like additional details on accessing and using |
3.2.2. Externalizing XML mapping files
Jakarta Persistence offers two mapping options:
-
annotations
-
XML mappings
Although annotations are much more common, there are projects where XML mappings are preferred. You can even mix annotations and XML mappings so that you can override annotation mappings with XML configurations that can be easily changed without recompiling the project source code. This is possible because if there are two conflicting mappings, the XML mappings take precedence over its annotation counterpart.
The Jakarta Persistence specification requires the XML mappings to be located on the classpath:
An object/relational mapping XML file named
orm.xml
may be specified in theMETA-INF
directory in the root of the persistence unit or in theMETA-INF
directory of any jar file referenced by thepersistence.xml
.Alternatively, or in addition, one or more mapping files may be referenced by the mapping-file elements of the persistence-unit element. These mapping files may be present anywhere on the classpath.
Therefore, the mapping files can reside in the application jar artifacts, or they can be stored in an external folder location with the cogitation that that location be included in the classpath.
Hibernate is more lenient in this regard so you can use any external location even outside of the application configured classpath.
<persistence xmlns="http://xmlns.jcp.org/xml/ns/persistence"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/persistence
http://xmlns.jcp.org/xml/ns/persistence/persistence_2_1.xsd"
version="2.1">
<persistence-unit name="CRM">
<description>
Persistence unit for Hibernate User Guide
</description>
<provider>org.hibernate.jpa.HibernatePersistenceProvider</provider>
<mapping-file>file:///etc/opt/app/mappings/orm.xml</mapping-file>
<properties>
<property name="jakarta.persistence.jdbc.driver"
value="org.h2.Driver" />
<property name="jakarta.persistence.jdbc.url"
value="jdbc:h2:mem:db1;DB_CLOSE_DELAY=-1" />
<property name="jakarta.persistence.jdbc.user"
value="sa" />
<property name="jakarta.persistence.jdbc.password"
value="" />
<property name="hibernate.show_sql"
value="true" />
<property name="hibernate.hbm2ddl.auto"
value="update" />
</properties>
</persistence-unit>
</persistence>
In the persistence.xml
configuration file above, the orm.xml
XML file containing all Jakarta Persistence entity mappings is located in the /etc/opt/app/mappings/
folder.
3.2.3. Configuring the SessionFactory
Metadata
via the Jakarta Persistence bootstrap
As previously seen, the Hibernate native bootstrap mechanism allows you to customize a great variety of configurations which are passed via the Metadata
object.
When using Hibernate as a Jakarta Persistence provider, the EntityManagerFactory
is backed by a SessionFactory
. For this reason, you might still want to use the Metadata
object to pass various settings which cannot be supplied via the standard Hibernate configuration settings.
For this reason, you can use the
MetadataBuilderContributor
class as you can see in the following examples.
MetadataBuilderContributor
public class SqlFunctionMetadataBuilderContributor
implements MetadataBuilderContributor {
@Override
public void contribute(MetadataBuilder metadataBuilder) {
metadataBuilder.applySqlFunction(
"instr", new StandardSQLFunction( "instr", StandardBasicTypes.INTEGER )
);
}
}
org.hibernate.orm.test.bootstrap.spi.metadatabuildercontributor
The above MetadataBuilderContributor
is used to register a SqlFuction
which is not defined by the currently running Hibernate Dialect
, but which we need to reference in our JPQL queries.
By having access to the
MetadataBuilder
class that’s used by the underlying SessionFactory
, the Jakarta Persistence bootstrap becomes just as flexible as the Hibernate native bootstrap mechanism.
You can then pass the custom MetadataBuilderContributor
via the hibernate.metadata_builder_contributor
configuration property as explained in the Configuration chapter.
4. Schema generation
Hibernate allows you to generate the database from the entity mappings.
Although the automatic schema generation is very useful for testing and prototyping purposes, in a production environment, it’s much more flexible to manage the schema using incremental migration scripts. |
Traditionally, the process of generating schema from entity mapping has been called HBM2DDL
.
To get a list of Hibernate-native and Jakarta Persistence-specific configuration properties consider reading the Configurations section.
Considering the following Domain Model:
@Entity(name = "Customer")
public class Customer {
@Id
private Integer id;
private String name;
@Basic(fetch = FetchType.LAZY)
private UUID accountsPayableXrefId;
@Lob
@Basic(fetch = FetchType.LAZY)
@LazyGroup("lobs")
private Blob image;
//Getters and setters are omitted for brevity
}
@Entity(name = "Person")
public static class Person {
@Id
private Long id;
private String name;
@OneToMany(mappedBy = "author")
private List<Book> books = new ArrayList<>();
//Getters and setters are omitted for brevity
}
@Entity(name = "Book")
public static class Book {
@Id
private Long id;
private String title;
@NaturalId
private String isbn;
@ManyToOne
private Person author;
//Getters and setters are omitted for brevity
}
If the hibernate.hbm2ddl.auto
configuration is set to create
, Hibernate is going to generate the following database schema:
create table Customer (
id integer not null,
accountsPayableXrefId binary,
image blob,
name varchar(255),
primary key (id)
)
create table Book (
id bigint not null,
isbn varchar(255),
title varchar(255),
author_id bigint,
primary key (id)
)
create table Person (
id bigint not null,
name varchar(255),
primary key (id)
)
alter table Book
add constraint UK_u31e1frmjp9mxf8k8tmp990i unique (isbn)
alter table Book
add constraint FKrxrgiajod1le3gii8whx2doie
foreign key (author_id)
references Person
4.1. Importing script files
To customize the schema generation process, the hibernate.hbm2ddl.import_files
configuration property must be used to provide other scripts files that Hibernate can use when the SessionFactory
is started.
For instance, considering the following schema-generation.sql
import file:
create sequence book_sequence start with 1 increment by 1
If we configure Hibernate to import the script above:
<property
name="hibernate.hbm2ddl.import_files"
value="schema-generation.sql" />
Hibernate is going to execute the script file after the schema is automatically generated.
4.2. Database objects
Hibernate allows you to customize the schema generation process via the HBM database-object
element.
Considering the following HBM mapping:
<?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>
<database-object>
<create>
CREATE OR REPLACE FUNCTION sp_count_books(
IN authorId bigint,
OUT bookCount bigint)
RETURNS bigint AS
$BODY$
BEGIN
SELECT COUNT(*) INTO bookCount
FROM book
WHERE author_id = authorId;
END;
$BODY$
LANGUAGE plpgsql;
</create>
<drop></drop>
<dialect-scope name="org.hibernate.dialect.PostgreSQLDialect" />
</database-object>
</hibernate-mapping>
When the SessionFactory
is bootstrapped, Hibernate is going to execute the database-object
, therefore creating the sp_count_books
function.
4.3. Database-level checks
Hibernate offers the @Check
annotation so that you can specify an arbitrary SQL CHECK constraint which can be defined as follows:
@Entity(name = "Book")
@Check(name = "ValidIsbn", constraints = "CASE WHEN isbn IS NOT NULL THEN LENGTH(isbn) = 13 ELSE true END")
@SecondaryTable(name = "BookEdition")
@Check(name = "PositiveEdition", constraints = "edition > 0")
public static class Book {
@Id
private Long id;
private String title;
@NaturalId
private String isbn;
private Double price;
@Column(table = "BookEdition")
private int edition = 1;
@Formula("edition + 1")
private int nextEdition = 2;
@Column(table = "BookEdition")
private LocalDate editionDate;
//Getters and setters omitted for brevity
}
Now, if you try to add a Book
entity with an isbn
attribute whose length is not 13 characters,
a ConstraintViolationException
is going to be thrown.
Book book = new Book();
book.setId(1L);
book.setPrice(49.99d);
book.setTitle("High-Performance Java Persistence");
book.setIsbn("11-11-2016");
entityManager.persist(book);
INSERT INTO Book (isbn, price, title, id)
VALUES ('11-11-2016', 49.99, 'High-Performance Java Persistence', 1)
-- WARN SqlExceptionHelper:129 - SQL Error: 0, SQLState: 23514
-- ERROR SqlExceptionHelper:131 - ERROR: new row for relation "book" violates check constraint "book_isbn_check"
4.4. Default value for a database column
With Hibernate, you can specify a default value for a given database column using the @ColumnDefault
annotation.
@ColumnDefault
mapping example@Entity(name = "Person")
@DynamicInsert
public static class Person {
@Id
private Long id;
@ColumnDefault("'N/A'")
private String name;
@ColumnDefault("-1")
private Long clientId;
//Getter and setters omitted for brevity
}
CREATE TABLE Person (
id BIGINT NOT NULL,
clientId BIGINT DEFAULT -1,
name VARCHAR(255) DEFAULT 'N/A',
PRIMARY KEY (id)
)
In the mapping above, both the name
and clientId
table columns have a DEFAULT
value.
The |
This way, when the name
and or clientId
attribute is null, the database will set them according to their declared default values.
@ColumnDefault
mapping exampledoInJPA(this::entityManagerFactory, entityManager -> {
Person person = new Person();
person.setId(1L);
entityManager.persist(person);
});
doInJPA(this::entityManagerFactory, entityManager -> {
Person person = entityManager.find(Person.class, 1L);
assertEquals("N/A", person.getName());
assertEquals(Long.valueOf(-1L), person.getClientId());
});
INSERT INTO Person (id) VALUES (?)
If the column value should be generated not only when a row is inserted, but also when it’s updated, the |
4.5. Columns unique constraint
The @UniqueConstraint
annotation is used to specify a unique constraint to be included by the automated schema generator for the primary or secondary table associated with the current annotated entity.
Considering the following entity mapping, Hibernate generates the unique constraint DDL when creating the database schema:
@UniqueConstraint
mapping example @Entity
@Table(
name = "book",
uniqueConstraints = @UniqueConstraint(
name = "uk_book_title_author",
columnNames = {
"title",
"author_id"
}
)
)
public static class Book {
@Id
@GeneratedValue
private Long id;
private String title;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(
name = "author_id",
foreignKey = @ForeignKey(name = "fk_book_author_id")
)
private Author author;
//Getter and setters omitted for brevity
}
@Entity
@Table(name = "author")
public static class Author {
@Id
@GeneratedValue
private Long id;
@Column(name = "first_name")
private String firstName;
@Column(name = "last_name")
private String lastName;
//Getter and setters omitted for brevity
}
create table author (
id bigint not null,
first_name varchar(255),
last_name varchar(255),
primary key (id)
)
create table book (
id bigint not null,
title varchar(255),
author_id bigint,
primary key (id)
)
alter table book
add constraint uk_book_title_author
unique (title, author_id)
alter table book
add constraint fk_book_author_id
foreign key (author_id)
references author
With the uk_book_title_author
unique constraint in place,
it’s no longer possible to add two books with the same title and for the same author.
@UniqueConstraintTest
persist example Author _author = doInJPA(this::entityManagerFactory, entityManager -> {
Author author = new Author();
author.setFirstName("Vlad");
author.setLastName("Mihalcea");
entityManager.persist(author);
Book book = new Book();
book.setTitle("High-Performance Java Persistence");
book.setAuthor(author);
entityManager.persist(book);
return author;
});
try {
doInJPA(this::entityManagerFactory, entityManager -> {
Book book = new Book();
book.setTitle("High-Performance Java Persistence");
book.setAuthor(_author);
entityManager.persist(book);
});
}
catch (Exception expected) {
assertNotNull(ExceptionUtil.findCause(expected, ConstraintViolationException.class));
}
insert
into
author
(first_name, last_name, id)
values
(?, ?, ?)
-- binding parameter [1] as [VARCHAR] - [Vlad]
-- binding parameter [2] as [VARCHAR] - [Mihalcea]
-- binding parameter [3] as [BIGINT] - [1]
insert
into
book
(author_id, title, id)
values
(?, ?, ?)
-- binding parameter [1] as [BIGINT] - [1]
-- binding parameter [2] as [VARCHAR] - [High-Performance Java Persistence]
-- binding parameter [3] as [BIGINT] - [2]
insert
into
book
(author_id, title, id)
values
(?, ?, ?)
-- binding parameter [1] as [BIGINT] - [1]
-- binding parameter [2] as [VARCHAR] - [High-Performance Java Persistence]
-- binding parameter [3] as [BIGINT] - [3]
-- SQL Error: 23505, SQLState: 23505
-- Unique index or primary key violation: "UK_BOOK_TITLE_AUTHOR_INDEX_1 ON PUBLIC.BOOK(TITLE, AUTHOR_ID) VALUES ( /* key:1 */ 3, 'High-Performance Java Persistence', 1)";
The second INSERT statement fails because of the unique constraint violation.
4.6. Columns index
The @Index
annotation is used by the automated schema generation tool to create a database index.
Considering the following entity mapping. Hibernate generates the index when creating the database schema:
@Index
mapping example @Entity
@Table(
name = "author",
indexes = @Index(
name = "idx_author_first_last_name",
columnList = "first_name, last_name",
unique = false
)
)
public static class Author {
@Id
@GeneratedValue
private Long id;
@Column(name = "first_name")
private String firstName;
@Column(name = "last_name")
private String lastName;
//Getter and setters omitted for brevity
}
create table author (
id bigint not null,
first_name varchar(255),
last_name varchar(255),
primary key (id)
)
create index idx_author_first_last_name
on author (first_name, last_name)
5. Persistence Context
Both the org.hibernate.Session
API and jakarta.persistence.EntityManager
API represent a context for dealing with persistent data.
This concept is called a persistence context
.
Persistent data has a state in relation to both a persistence context and the underlying database.
transient
-
the entity has just been instantiated and is not associated with a persistence context. It has no persistent representation in the database and typically no identifier value has been assigned (unless the assigned generator was used).
managed
orpersistent
-
the entity has an associated identifier and is associated with a persistence context. It may or may not physically exist in the database yet.
detached
-
the entity has an associated identifier but is no longer associated with a persistence context (usually because the persistence context was closed or the instance was evicted from the context)
removed
-
the entity has an associated identifier and is associated with a persistence context, however, it is scheduled for removal from the database.
Much of the org.hibernate.Session
and jakarta.persistence.EntityManager
methods deal with moving entities among these states.
5.1. Accessing Hibernate APIs from Jakarta Persistence
Jakarta Persistence defines an incredibly useful method to allow applications access to the APIs of the underlying provider.
Session session = entityManager.unwrap(Session.class);
SessionImplementor sessionImplementor = entityManager.unwrap(SessionImplementor.class);
SessionFactory sessionFactory = entityManager.getEntityManagerFactory().unwrap(SessionFactory.class);
5.2. Bytecode Enhancement
Hibernate "grew up" not supporting bytecode enhancement at all. At that time, Hibernate only supported proxy-based alternative for lazy loading and always used diff-based dirty calculation. Hibernate 3.x saw the first attempts at bytecode enhancement support in Hibernate. We consider those initial attempts (up until 5.0) completely as an incubation. The support for bytecode enhancement in 5.0 onward is what we are discussing here.
See Bytecode Enhancement for discussion of performing enhancement.
5.2.1. Lazy attribute loading
Think of this as partial loading support. Essentially, you can tell Hibernate that only part(s) of an entity should be loaded upon fetching from the database and when the other part(s) should be loaded as well. Note that this is very much different from the proxy-based idea of lazy loading which is entity-centric where the entity’s state is loaded at once as needed. With bytecode enhancement, individual attributes or groups of attributes are loaded as needed.
Lazy attributes can be designated to be loaded together, and this is called a "lazy group".
By default, all singular attributes are part of a single group, meaning that when one lazy singular attribute is accessed all lazy singular attributes are loaded.
Lazy plural attributes, by default, are each a lazy group by themselves.
This behavior is explicitly controllable through the @org.hibernate.annotations.LazyGroup
annotation.
@LazyGroup
example@Entity
public class Customer {
@Id
private Integer id;
private String name;
@Basic(fetch = FetchType.LAZY)
private UUID accountsPayableXrefId;
@Lob
@Basic(fetch = FetchType.LAZY)
@LazyGroup("lobs")
private Blob image;
//Getters and setters are omitted for brevity
}
In the above example, we have 2 lazy attributes: accountsPayableXrefId
and image
.
Each is part of a different fetch group (accountsPayableXrefId is part of the default fetch group),
which means that accessing accountsPayableXrefId
will not force the loading of the image
attribute, and vice-versa.
As a hopefully temporary legacy hold-over, it is currently required that all lazy singular associations (many-to-one and one-to-one) also include |
5.2.2. In-line dirty tracking
Historically Hibernate only supported diff-based dirty calculation for determining which entities in a persistence context have changed.
This essentially means that Hibernate would keep track of the last known state of an entity in regards to the database (typically the last read or write).
Then, as part of flushing the persistence context, Hibernate would walk every entity associated with the persistence context and check its current state against that "last known database state".
This is by far the most thorough approach to dirty checking because it accounts for data-types that can change their internal state (java.util.Date
is the prime example of this).
However, in a persistence context with a large number of associated entities, it can also be a performance-inhibiting approach.
If your application does not need to care about "internal state changing data-type" use cases, bytecode-enhanced dirty tracking might be a worthwhile alternative to consider, especially in terms of performance. In this approach Hibernate will manipulate the bytecode of your classes to add "dirty tracking" directly to the entity, allowing the entity itself to keep track of which of its attributes have changed. During the flush time, Hibernate asks your entity what has changed rather than having to perform the state-diff calculations.
5.2.3. Bidirectional association management
Hibernate strives to keep your application as close to "normal Java usage" (idiomatic Java) as possible.
Consider a domain model with a normal Person
/Book
bidirectional association:
@Entity(name = "Person")
public static class Person {
@Id
private Long id;
private String name;
@OneToMany(mappedBy = "author")
private List<Book> books = new ArrayList<>();
//Getters and setters are omitted for brevity
}
@Entity(name = "Book")
public static class Book {
@Id
private Long id;
private String title;
@NaturalId
private String isbn;
@ManyToOne
private Person author;
//Getters and setters are omitted for brevity
}
Person person = new Person();
person.setName("John Doe");
Book book = new Book();
person.getBooks().add(book);
try {
book.getAuthor().getName();
}
catch (NullPointerException expected) {
// This blows up (NPE) in normal Java usage
}
This blows up in normal Java usage. The correct normal Java usage is:
Person person = new Person();
person.setName("John Doe");
Book book = new Book();
person.getBooks().add(book);
book.setAuthor(person);
book.getAuthor().getName();
Bytecode-enhanced bi-directional association management makes that first example work by managing the "other side" of a bi-directional association whenever one side is manipulated.
5.3. Making entities persistent
Once you’ve created a new entity instance (using the standard new
operator) it is in new
state.
You can make it persistent by associating it to either an org.hibernate.Session
or a jakarta.persistence.EntityManager
.
Person person = new Person();
person.setId(1L);
person.setName("John Doe");
entityManager.persist(person);
Person person = new Person();
person.setId(1L);
person.setName("John Doe");
session.persist(person);
org.hibernate.Session
also has a method named persist which follows the exact semantics defined in the Jakarta Persistence specification for the persist method.
It is this org.hibernate.Session
method to which the Hibernate jakarta.persistence.EntityManager
implementation delegates.
If the DomesticCat
entity type has a generated identifier, the value is associated with the instance when the save or persist is called.
If the identifier is not automatically generated, the manually assigned (usually natural) key value has to be set on the instance before the save or persist methods are called.
5.4. Deleting (removing) entities
Entities can also be deleted.
entityManager.remove(person);
session.remove(person);
Hibernate itself can handle deleting entities in detached state. Jakarta Persistence, however, disallows this behavior. The implication here is that the entity instance passed to the |
5.5. Obtain an entity reference without initializing its data
Sometimes referred to as lazy loading, the ability to obtain a reference to an entity without having to load its data is hugely important. The most common case being the need to create an association between an entity and another existing entity.
Book book = new Book();
book.setAuthor(entityManager.getReference(Person.class, personId));
Book book = new Book();
book.setId(1L);
book.setIsbn("123-456-7890");
entityManager.persist(book);
book.setAuthor(session.getReference(Person.class, personId));
The above works on the assumption that the entity is defined to allow lazy loading, generally through use of runtime proxies. In both cases an exception will be thrown later if the given entity does not refer to actual database state when the application attempts to use the returned proxy in any way that requires access to its data.
Unless the entity class is declared |
5.6. Obtain an entity with its data initialized
It is also quite common to want to obtain an entity along with its data (e.g. like when we need to display it in the UI).
Person person = entityManager.find(Person.class, personId);
Person person = session.get(Person.class, personId);
byId()
Hibernate APIPerson person = session.byId(Person.class).load(personId);
In both cases null is returned if no matching database row was found.
It’s possible to return a Java 8 Optional
as well:
byId()
Hibernate APIOptional<Person> optionalPerson = session.byId(Person.class).loadOptional(personId);
5.7. Obtain multiple entities by their identifiers
If you want to load multiple entities by providing their identifiers, calling the EntityManager#find
method multiple times is not only inconvenient,
but also inefficient.
While the Jakarta Persistence standard does not support retrieving multiple entities at once, other than running a JPQL or Criteria API query,
Hibernate offers this functionality via the
byMultipleIds
method of the Hibernate Session
.
The byMultipleIds
method returns a
MultiIdentifierLoadAccess
which you can use to customize the multi-load request.
The MultiIdentifierLoadAccess
interface provides several methods which you can use to
change the behavior of the multi-load call:
enableOrderedReturn(boolean enabled)
-
This setting controls whether the returned
List
is ordered and positional in relation to the incoming ids. If enabled (the default), the returnList
is ordered and positional relative to the incoming ids. In other words, a request tomultiLoad([2,1,3])
will return[Entity#2, Entity#1, Entity#3]
.An important distinction is made here in regards to the handling of unknown entities depending on this "ordered return" setting. If enabled, a null is inserted into the
List
at the proper position(s). If disabled, the nulls are not put into the return List.In other words, consumers of the returned ordered List would need to be able to handle null elements.
enableSessionCheck(boolean enabled)
-
This setting, which is disabled by default, tells Hibernate to check the first-level cache (a.k.a
Session
or Persistence Context) first and, if the entity is found and already managed by the HibernateSession
, the cached entity will be added to the returnedList
, therefore skipping it from being fetched via the multi-load query. enableReturnOfDeletedEntities(boolean enabled)
-
This setting instructs Hibernate if the multi-load operation is allowed to return entities that were deleted by the current Persistence Context. A deleted entity is one which has been passed to this
Session.delete
orSession.remove
method, but theSession
was not flushed yet, meaning that the associated row was not deleted in the database table.The default behavior is to handle them as null in the return (see
enableOrderedReturn
). When enabled, the result set will contain deleted entities. When disabled (which is the default behavior), deleted entities are not included in the returningList
. with(LockOptions lockOptions)
-
This setting allows you to pass a given
LockOptions
mode to the multi-load query. with(CacheMode cacheMode)
-
This setting allows you to pass a given
CacheMode
strategy so that we can load entities from the second-level cache, therefore skipping the cached entities from being fetched via the multi-load query. withBatchSize(int batchSize)
-
This setting allows you to specify a batch size for loading the entities (e.g. how many at a time).
The default is to use a batch sizing strategy defined by the
Dialect.getDefaultBatchLoadSizingStrategy()
method.Any greater-than-one value here will override that default behavior.
with(RootGraph<T> graph)
-
The
RootGraph
is a Hibernate extension to the Jakarta PersistenceEntityGraph
contract, and this method allows you to pass a specificRootGraph
to the multi-load query so that it can fetch additional relationships of the current loading entity.
Now, assuming we have 3 Person
entities in the database, we can load all of them with a single call
as illustrated by the following example:
byMultipleIds()
Hibernate APISession session = entityManager.unwrap( Session.class );
List<Person> persons = session
.byMultipleIds( Person.class )
.multiLoad( 1L, 2L, 3L );
assertEquals( 3, persons.size() );
List<Person> samePersons = session
.byMultipleIds( Person.class )
.enableSessionCheck( true )
.multiLoad( 1L, 2L, 3L );
assertEquals( persons, samePersons );
SELECT p.id AS id1_0_0_,
p.name AS name2_0_0_
FROM Person p
WHERE p.id IN ( 1, 2, 3 )
Notice that only one SQL SELECT statement was executed since the second call uses the
enableSessionCheck
method of the MultiIdentifierLoadAccess
to instruct Hibernate to skip entities that are already loaded in the current Persistence Context.
If the entities are not available in the current Persistence Context but they could be loaded from the second-level cache, you can use the
with(CacheMode)
method of the MultiIdentifierLoadAccess
object.
SessionFactory sessionFactory = scope.getEntityManagerFactory().unwrap(SessionFactory.class);
Statistics statistics = sessionFactory.getStatistics();
sessionFactory.getCache().evictAll();
statistics.clear();
final SQLStatementInspector sqlStatementInspector = scope.getStatementInspector( SQLStatementInspector.class );
sqlStatementInspector.clear();
assertEquals(0, statistics.getQueryExecutionCount());
scope.inTransaction(
entityManager -> {
Session session = entityManager.unwrap(Session.class);
List<Person> persons = session
.byMultipleIds(Person.class)
.multiLoad(1L, 2L, 3L);
assertEquals(3, persons.size());
}
);
assertEquals(0, statistics.getSecondLevelCacheHitCount());
assertEquals(3, statistics.getSecondLevelCachePutCount());
assertEquals(1, sqlStatementInspector.getSqlQueries().size());
scope.inTransaction(
entityManager -> {
Session session = entityManager.unwrap(Session.class);
sqlStatementInspector.clear();
List<Person> persons = session.byMultipleIds(Person.class)
.with(CacheMode.NORMAL)
.multiLoad(1L, 2L, 3L);
assertEquals(3, persons.size());
}
);
assertEquals(3, statistics.getSecondLevelCacheHitCount());
assertEquals(0, sqlStatementInspector.getSqlQueries().size());
In the example above, we first make sure that we clear the second-level cache to demonstrate that the multi-load query will put the returning entities into the second-level cache.
After executing the first byMultipleIds
call, Hibernate is going to fetch the requested entities,
and as illustrated by the getSecondLevelCachePutCount
method call, 3 entities were indeed added to the
shared cache.
Afterward, when executing the second byMultipleIds
call for the same entities in a new Hibernate Session
,
we set the
CacheMode.NORMAL
second-level cache mode so that entities are going to be returned from the second-level cache.
The getSecondLevelCacheHitCount
statistics method returns 3 this time, since the 3 entities were loaded from the second-level cache, and, as illustrated by sqlStatementInterceptor.getSqlQueries()
, no multi-load SELECT statement was executed this time.
5.8. Obtain an entity by natural-id
In addition to allowing to load the entity by its identifier, Hibernate allows applications to load entities by the declared natural identifier.
@Entity(name = "Book")
public static class Book {
@Id
private Long id;
private String title;
@NaturalId
private String isbn;
@ManyToOne
private Person author;
//Getters and setters are omitted for brevity
}
We can also opt to fetch the entity or just retrieve a reference to it when using the natural identifier loading methods.
Book book = session.bySimpleNaturalId(Book.class).getReference(isbn);
Book book = session
.byNaturalId(Book.class)
.using("isbn", isbn)
.load();
We can also use a Java 8 Optional
to load an entity by its natural id:
Optional<Book> optionalBook = session
.byNaturalId(Book.class)
.using("isbn", isbn)
.loadOptional();
Hibernate offers a consistent API for accessing persistent data by identifier or by the natural-id. Each of these defines the same two data access methods:
- getReference
-
Should be used in cases where the identifier is assumed to exist, where non-existence would be an actual error. Should never be used to test existence. That is because this method will prefer to create and return a proxy if the data is not already associated with the Session rather than hit the database. The quintessential use-case for using this method is to create foreign key based associations.
- load
-
Will return the persistent data associated with the given identifier value or null if that identifier does not exist.
Each of these two methods defines an overloading variant accepting a org.hibernate.LockOptions
argument.
Locking is discussed in a separate chapter.
5.9. Filtering entities and associations
Hibernate offers two options if you want to filter entities or entity associations:
- static (e.g.
@Where
and@WhereJoinTable
) -
which are defined at mapping time and cannot change at runtime.
- dynamic (e.g.
@Filter
and@FilterJoinTable
) -
which are applied and configured at runtime.
5.9.1. @Where
Sometimes, you want to filter out entities or collections using custom SQL criteria.
This can be achieved using the @Where
annotation, which can be applied to entities and collections.
@Where
mapping usagepublic enum AccountType {
DEBIT,
CREDIT
}
@Entity(name = "Client")
public static class Client {
@Id
private Long id;
private String name;
@Where(clause = "account_type = 'DEBIT'")
@OneToMany(mappedBy = "client")
private List<Account> debitAccounts = new ArrayList<>();
@Where(clause = "account_type = 'CREDIT'")
@OneToMany(mappedBy = "client")
private List<Account> creditAccounts = new ArrayList<>();
//Getters and setters omitted for brevity
}
@Entity(name = "Account")
@Where(clause = "active = true")
public static class Account {
@Id
private Long id;
@ManyToOne
private Client client;
@Column(name = "account_type")
@Enumerated(EnumType.STRING)
private AccountType type;
private Double amount;
private Double rate;
private boolean active;
//Getters and setters omitted for brevity
}
If the database contains the following entities:
@Where
mappingdoInJPA(this::entityManagerFactory, entityManager -> {
Client client = new Client();
client.setId(1L);
client.setName("John Doe");
entityManager.persist(client);
Account account1 = new Account();
account1.setId(1L);
account1.setType(AccountType.CREDIT);
account1.setAmount(5000d);
account1.setRate(1.25 / 100);
account1.setActive(true);
account1.setClient(client);
client.getCreditAccounts().add(account1);
entityManager.persist(account1);
Account account2 = new Account();
account2.setId(2L);
account2.setType(AccountType.DEBIT);
account2.setAmount(0d);
account2.setRate(1.05 / 100);
account2.setActive(false);
account2.setClient(client);
client.getDebitAccounts().add(account2);
entityManager.persist(account2);
Account account3 = new Account();
account3.setType(AccountType.DEBIT);
account3.setId(3L);
account3.setAmount(250d);
account3.setRate(1.05 / 100);
account3.setActive(true);
account3.setClient(client);
client.getDebitAccounts().add(account3);
entityManager.persist(account3);
});
INSERT INTO Client (name, id)
VALUES ('John Doe', 1)
INSERT INTO Account (active, amount, client_id, rate, account_type, id)
VALUES (true, 5000.0, 1, 0.0125, 'CREDIT', 1)
INSERT INTO Account (active, amount, client_id, rate, account_type, id)
VALUES (false, 0.0, 1, 0.0105, 'DEBIT', 2)
INSERT INTO Account (active, amount, client_id, rate, account_type, id)
VALUES (true, 250.0, 1, 0.0105, 'DEBIT', 3)
When executing an Account
entity query, Hibernate is going to filter out all records that are not active.
@Where
doInJPA(this::entityManagerFactory, entityManager -> {
List<Account> accounts = entityManager.createQuery(
"select a from Account a", Account.class)
.getResultList();
assertEquals(2, accounts.size());
});
SELECT
a.id as id1_0_,
a.active as active2_0_,
a.amount as amount3_0_,
a.client_id as client_i6_0_,
a.rate as rate4_0_,
a.account_type as account_5_0_
FROM
Account a
WHERE ( a.active = true )
When fetching the debitAccounts
or the creditAccounts
collections, Hibernate is going to apply the @Where
clause filtering criteria to the associated child entities.
@Where
doInJPA(this::entityManagerFactory, entityManager -> {
Client client = entityManager.find(Client.class, 1L);
assertEquals(1, client.getCreditAccounts().size());
assertEquals(1, client.getDebitAccounts().size());
});
SELECT
c.client_id as client_i6_0_0_,
c.id as id1_0_0_,
c.id as id1_0_1_,
c.active as active2_0_1_,
c.amount as amount3_0_1_,
c.client_id as client_i6_0_1_,
c.rate as rate4_0_1_,
c.account_type as account_5_0_1_
FROM
Account c
WHERE ( c.active = true and c.account_type = 'CREDIT' ) AND c.client_id = 1
SELECT
d.client_id as client_i6_0_0_,
d.id as id1_0_0_,
d.id as id1_0_1_,
d.active as active2_0_1_,
d.amount as amount3_0_1_,
d.client_id as client_i6_0_1_,
d.rate as rate4_0_1_,
d.account_type as account_5_0_1_
FROM
Account d
WHERE ( d.active = true and d.account_type = 'DEBIT' ) AND d.client_id = 1
5.9.2. @WhereJoinTable
Just like @Where
annotation, @WhereJoinTable
is used to filter out collections using a joined table (e.g. @ManyToMany association).
@WhereJoinTable
mapping example@Entity(name = "Book")
public static class Book {
@Id
private Long id;
private String title;
private String author;
@ManyToMany
@JoinTable(
name = "Book_Reader",
joinColumns = @JoinColumn(name = "book_id"),
inverseJoinColumns = @JoinColumn(name = "reader_id")
)
@WhereJoinTable(clause = "created_on > DATEADD('DAY', -7, CURRENT_TIMESTAMP())")
private List<Reader> currentWeekReaders = new ArrayList<>();
//Getters and setters omitted for brevity
}
@Entity(name = "Reader")
public static class Reader {
@Id
private Long id;
private String name;
//Getters and setters omitted for brevity
}
create table Book (
id bigint not null,
author varchar(255),
title varchar(255),
primary key (id)
)
create table Book_Reader (
book_id bigint not null,
reader_id bigint not null
)
create table Reader (
id bigint not null,
name varchar(255),
primary key (id)
)
alter table Book_Reader
add constraint FKsscixgaa5f8lphs9bjdtpf9g
foreign key (reader_id)
references Reader
alter table Book_Reader
add constraint FKoyrwu9tnwlukd1616qhck21ra
foreign key (book_id)
references Book
alter table Book_Reader
add created_on timestamp
default current_timestamp
In the example above, the current week Reader
entities are included in the currentWeekReaders
collection
which uses the @WhereJoinTable
annotation to filter the joined table rows according to the provided SQL clause.
Considering that the following two Book_Reader
entries are added into our system:
@WhereJoinTable
test dataBook book = new Book();
book.setId(1L);
book.setTitle("High-Performance Java Persistence");
book.setAuthor("Vad Mihalcea");
entityManager.persist(book);
Reader reader1 = new Reader();
reader1.setId(1L);
reader1.setName("John Doe");
entityManager.persist(reader1);
Reader reader2 = new Reader();
reader2.setId(2L);
reader2.setName("John Doe Jr.");
entityManager.persist(reader2);
statement.executeUpdate(
"INSERT INTO Book_Reader " +
" (book_id, reader_id) " +
"VALUES " +
" (1, 1) "
);
statement.executeUpdate(
"INSERT INTO Book_Reader " +
" (book_id, reader_id, created_on) " +
"VALUES " +
" (1, 2, DATEADD('DAY', -10, CURRENT_TIMESTAMP())) "
);
When fetching the currentWeekReaders
collection, Hibernate is going to find only one entry:
@WhereJoinTable
fetch exampleBook book = entityManager.find(Book.class, 1L);
assertEquals(1, book.getCurrentWeekReaders().size());
5.9.3. @Filter
The @Filter
annotation is another way to filter out entities or collections using custom SQL criteria.
Unlike the @Where
annotation, @Filter
allows you to parameterize the filter clause at runtime.
Now, considering we have the following Account
entity:
@Filter
mapping entity-level usage @Entity(name = "Account")
@Table(name = "account")
@FilterDef(
name="activeAccount",
parameters = @ParamDef(
name="active",
type=Boolean.class
)
)
@Filter(
name="activeAccount",
condition="active_status = :active"
)
public static class Account {
@Id
private Long id;
@ManyToOne(fetch = FetchType.LAZY)
private Client client;
@Column(name = "account_type")
@Enumerated(EnumType.STRING)
private AccountType type;
private Double amount;
private Double rate;
@Column(name = "active_status")
private boolean active;
//Getters and setters omitted for brevity
}
Notice that the This mapping was done to show you that the |
As already explained, we can also apply the @Filter
annotation for collections as illustrated by the Client
entity:
@Filter
mapping collection-level usage@Entity(name = "Client")
@Table(name = "client")
public static class Client {
@Id
private Long id;
private String name;
private AccountType type;
@OneToMany(
mappedBy = "client",
cascade = CascadeType.ALL
)
@Filter(
name="activeAccount",
condition="active_status = :active"
)
private List<Account> accounts = new ArrayList<>();
//Getters and setters omitted for brevity
public void addAccount(Account account) {
account.setClient(this);
this.accounts.add(account);
}
}
If we persist a Client
with three associated Account
entities,
Hibernate will execute the following SQL statements:
@Filter
mapping Client client = new Client()
.setId(1L)
.setName("John Doe")
.setType(AccountType.DEBIT);
client.addAccount(
new Account()
.setId(1L)
.setType(AccountType.CREDIT)
.setAmount(5000d)
.setRate(1.25 / 100)
.setActive(true)
);
client.addAccount(
new Account()
.setId(2L)
.setType(AccountType.DEBIT)
.setAmount(0d)
.setRate(1.05 / 100)
.setActive(false)
);
client.addAccount(
new Account()
.setType(AccountType.DEBIT)
.setId(3L)
.setAmount(250d)
.setRate(1.05 / 100)
.setActive(true)
);
entityManager.persist(client);
INSERT INTO Client (name, id)
VALUES ('John Doe', 1)
INSERT INTO Account (active_status, amount, client_id, rate, account_type, id)
VALUES (true, 5000.0, 1, 0.0125, 'CREDIT', 1)
INSERT INTO Account (active_status, amount, client_id, rate, account_type, id)
VALUES (false, 0.0, 1, 0.0105, 'DEBIT', 2)
INSERT INTO Account (active_status, amount, client_id, rate, account_type, id)
VALUES (true, 250.0, 1, 0.0105, 'DEBIT', 3)
By default, without explicitly enabling the filter, Hibernate is going to fetch all Account
entities.
@Filter
List<Account> accounts = entityManager.createQuery(
"select a from Account a", Account.class)
.getResultList();
assertEquals(3, accounts.size());
SELECT
a.id as id1_0_,
a.active_status as active2_0_,
a.amount as amount3_0_,
a.client_id as client_i6_0_,
a.rate as rate4_0_,
a.account_type as account_5_0_
FROM
Account a
If the filter is enabled and the filter parameter value is provided,
then Hibernate is going to apply the filtering criteria to the associated Account
entities.
@Filter
entityManager
.unwrap(Session.class)
.enableFilter("activeAccount")
.setParameter("active", true);
List<Account> accounts = entityManager.createQuery(
"select a from Account a", Account.class)
.getResultList();
assertEquals(2, accounts.size());
SELECT
a.id as id1_0_,
a.active_status as active2_0_,
a.amount as amount3_0_,
a.client_id as client_i6_0_,
a.rate as rate4_0_,
a.account_type as account_5_0_
FROM
Account a
WHERE
a.active_status = true
Filters apply to entity queries, but not to direct fetching. Therefore, in the following example, the filter is not taken into consideration when fetching an entity from the Persistence Context. Fetching entities mapped with
@Filter
As you can see from the example above, just like an entity query, the filter prevents the entity from being loaded. |
Just like with entity queries, collections can be filtered as well, but only if the filter is explicitly enabled on the currently running Hibernate Session
.
@Filter
Client client = entityManager.find(Client.class, 1L);
assertEquals(3, client.getAccounts().size());
SELECT
c.id as id1_1_0_,
c.name as name2_1_0_
FROM
Client c
WHERE
c.id = 1
SELECT
a.id as id1_0_,
a.active_status as active2_0_,
a.amount as amount3_0_,
a.client_id as client_i6_0_,
a.rate as rate4_0_,
a.account_type as account_5_0_
FROM
Account a
WHERE
a.client_id = 1
When activating the @Filter
and fetching the accounts
collections, Hibernate is going to apply the filter condition to the associated collection entries.
@Filter
entityManager
.unwrap(Session.class)
.enableFilter("activeAccount")
.setParameter("active", true);
Client client = entityManager.find(Client.class, 1L);
assertEquals(2, client.getAccounts().size());
SELECT
c.id as id1_1_0_,
c.name as name2_1_0_
FROM
Client c
WHERE
c.id = 1
SELECT
a.id as id1_0_,
a.active_status as active2_0_,
a.amount as amount3_0_,
a.client_id as client_i6_0_,
a.rate as rate4_0_,
a.account_type as account_5_0_
FROM
Account a
WHERE
accounts0_.active_status = true
and a.client_id = 1
The main advantage of |
It’s not possible to combine the If caching were allowed for a currently filtered collection, then the second-level cache would store only a subset of the whole collection. Afterward, every other Session will get the filtered collection from the cache, even if the Session-level filters have not been explicitly activated. For this reason, the second-level collection cache is limited to storing whole collections, and not subsets. |
5.9.4. @Filter
with @SqlFragmentAlias
When using the @Filter
annotation and working with entities that are mapped onto multiple database tables,
you will need to use the
@SqlFragmentAlias
annotation
if the @Filter
defines a condition that uses predicates across multiple tables.
@SqlFragmentAlias
mapping usage@Entity(name = "Account")
@Table(name = "account")
@Comment(on="account", value = "The account table")
@SecondaryTable(
name = "account_details"
)
@Comment(on="account_details", value = "The account details secondary table")
@SQLDelete(
sql = "UPDATE account_details SET deleted = true WHERE id = ? "
)
@FilterDef(
name="activeAccount",
parameters = @ParamDef(
name="active",
type=Boolean.class
)
)
@Filter(
name="activeAccount",
condition="{a}.active = :active and {ad}.deleted = false",
aliases = {
@SqlFragmentAlias(alias = "a", table= "account"),
@SqlFragmentAlias(alias = "ad", table= "account_details"),
}
)
public static class Account {
@Id
private Long id;
private Double amount;
private Double rate;
private boolean active;
@Column(table = "account_details")
private boolean deleted;
//Getters and setters omitted for brevity
}
Now, when fetching the Account
entities and activating the filter,
Hibernate is going to apply the right table aliases to the filter predicates:
@SqlFragmentAlias
entityManager
.unwrap(Session.class)
.enableFilter("activeAccount")
.setParameter("active", true);
List<Account> accounts = entityManager.createQuery(
"select a from Account a", Account.class)
.getResultList();
select
filtersqlf0_.id as id1_0_,
filtersqlf0_.active as active2_0_,
filtersqlf0_.amount as amount3_0_,
filtersqlf0_.rate as rate4_0_,
filtersqlf0_1_.deleted as deleted1_1_
from
account filtersqlf0_
left outer join
account_details filtersqlf0_1_
on filtersqlf0_.id=filtersqlf0_1_.id
where
filtersqlf0_.active = ?
and filtersqlf0_1_.deleted = false
-- binding parameter [1] as [BOOLEAN] - [true]
5.9.5. @FilterJoinTable
When using the @Filter
annotation with collections, the filtering is done against the child entries (entities or embeddables).
However, if you have a link table between the parent entity and the child table, then you need to use the @FilterJoinTable
to filter child entries according to some column contained in the join table.
The @FilterJoinTable
annotation can be, therefore, applied to a unidirectional @OneToMany
collection as illustrated in the following mapping:
@FilterJoinTable
mapping usage @Entity(name = "Client")
@FilterDef(
name="firstAccounts",
parameters=@ParamDef(
name="maxOrderId",
type=int.class
)
)
public static class Client {
@Id
private Long id;
private String name;
@OneToMany(cascade = CascadeType.ALL)
@OrderColumn(name = "order_id")
@FilterJoinTable(
name="firstAccounts",
condition="order_id <= :maxOrderId"
)
private List<Account> accounts = new ArrayList<>();
//Getters and setters omitted for brevity
public void addAccount(Account account) {
this.accounts.add(account);
}
}
@Entity(name = "Account")
public static class Account {
@Id
private Long id;
@Column(name = "account_type")
@Enumerated(EnumType.STRING)
private AccountType type;
private Double amount;
private Double rate;
//Getters and setters omitted for brevity
}
The firstAccounts
filter will allow us to get only the Account
entities that have the order_id
(which tells the position of every entry inside the accounts
collection)
less than a given number (e.g. maxOrderId
).
Let’s assume our database contains the following entities:
@FilterJoinTable
mapping Client client = new Client()
.setId(1L)
.setName("John Doe");
client.addAccount(
new Account()
.setId(1L)
.setType(AccountType.CREDIT)
.setAmount(5000d)
.setRate(1.25 / 100)
);
client.addAccount(
new Account()
.setId(2L)
.setType(AccountType.DEBIT)
.setAmount(0d)
.setRate(1.05 / 100)
);
client.addAccount(
new Account()
.setType(AccountType.DEBIT)
.setId(3L)
.setAmount(250d)
.setRate(1.05 / 100)
);
entityManager.persist(client);
INSERT INTO Client (name, id)
VALUES ('John Doe', 1)
INSERT INTO Account (amount, client_id, rate, account_type, id)
VALUES (5000.0, 1, 0.0125, 'CREDIT', 1)
INSERT INTO Account (amount, client_id, rate, account_type, id)
VALUES (0.0, 1, 0.0105, 'DEBIT', 2)
INSERT INTO Account (amount, client_id, rate, account_type, id)
VALUES (250.0, 1, 0.0105, 'DEBIT', 3)
INSERT INTO Client_Account (Client_id, order_id, accounts_id)
VALUES (1, 0, 1)
INSERT INTO Client_Account (Client_id, order_id, accounts_id)
VALUES (1, 0, 1)
INSERT INTO Client_Account (Client_id, order_id, accounts_id)
VALUES (1, 1, 2)
INSERT INTO Client_Account (Client_id, order_id, accounts_id)
VALUES (1, 2, 3)
The collections can be filtered only if the associated filter is enabled on the currently running Hibernate Session
.
@FilterJoinTable
without enabling the filterClient client = entityManager.find(Client.class, 1L);
assertEquals(3, client.getAccounts().size());
SELECT
ca.Client_id as Client_i1_2_0_,
ca.accounts_id as accounts2_2_0_,
ca.order_id as order_id3_0_,
a.id as id1_0_1_,
a.amount as amount3_0_1_,
a.rate as rate4_0_1_,
a.account_type as account_5_0_1_
FROM
Client_Account ca
INNER JOIN
Account a
ON ca.accounts_id=a.id
WHERE
ca.Client_id = ?
-- binding parameter [1] as [BIGINT] - [1]
If we enable the filter and set the maxOrderId
to 1
when fetching the accounts
collections, Hibernate is going to apply the @FilterJoinTable
clause filtering criteria, and we will get just
2
Account
entities, with the order_id
values of 0
and 1
.
@FilterJoinTable
Client client = entityManager.find(Client.class, 1L);
entityManager
.unwrap(Session.class)
.enableFilter("firstAccounts")
.setParameter("maxOrderId", 1);
assertEquals(2, client.getAccounts().size());
SELECT
ca.Client_id as Client_i1_2_0_,
ca.accounts_id as accounts2_2_0_,
ca.order_id as order_id3_0_,
a.id as id1_0_1_,
a.amount as amount3_0_1_,
a.rate as rate4_0_1_,
a.account_type as account_5_0_1_
FROM
Client_Account ca
INNER JOIN
Account a
ON ca.accounts_id=a.id
WHERE
ca.order_id <= ?
AND ca.Client_id = ?
-- binding parameter [1] as [INTEGER] - [1]
-- binding parameter [2] as [BIGINT] - [1]
5.10. Modifying managed/persistent state
Entities in managed/persistent state may be manipulated by the application, and any changes will be automatically detected and persisted when the persistence context is flushed. There is no need to call a particular method to make your modifications persistent.
Person person = entityManager.find(Person.class, personId);
person.setName("John Doe");
entityManager.flush();
Person person = session.byId(Person.class).load(personId);
person.setName("John Doe");
session.flush();
By default, when you modify an entity, all columns but the identifier are being set during update.
Therefore, considering you have the following Product
entity mapping:
Product
entity mapping@Entity(name = "Product")
public static class Product {
@Id
private Long id;
@Column
private String name;
@Column
private String description;
@Column(name = "price_cents")
private Integer priceCents;
@Column
private Integer quantity;
//Getters and setters are omitted for brevity
}
If you persist the following Product
entity:
Product
entityProduct book = new Product();
book.setId(1L);
book.setName("High-Performance Java Persistence");
book.setDescription("Get the most out of your persistence layer");
book.setPriceCents(29_99);
book.setQuantity(10_000);
entityManager.persist(book);
When you modify the Product
entity, Hibernate generates the following SQL UPDATE statement:
Product
entitydoInJPA(this::entityManagerFactory, entityManager -> {
Product book = entityManager.find(Product.class, 1L);
book.setPriceCents(24_99);
});
UPDATE
Product
SET
description = ?,
name = ?,
price_cents = ?,
quantity = ?
WHERE
id = ?
-- binding parameter [1] as [VARCHAR] - [Get the most out of your persistence layer]
-- binding parameter [2] as [VARCHAR] - [High-Performance Java Persistence]
-- binding parameter [3] as [INTEGER] - [2499]
-- binding parameter [4] as [INTEGER] - [10000]
-- binding parameter [5] as [BIGINT] - [1]
The default UPDATE statement containing all columns has two advantages:
-
it allows you to better benefit from JDBC Statement caching.
-
it allows you to enable batch updates even if multiple entities modify different properties.
However, there is also one downside to including all columns in the SQL UPDATE statement. If you have multiple indexes, the database might update those redundantly even if you don’t actually modify all column values.
To fix this issue, you can use dynamic updates.
5.10.1. Dynamic updates
To enable dynamic updates, you need to annotate the entity with the @DynamicUpdate
annotation:
Product
entity mapping@Entity(name = "Product")
@DynamicUpdate
public static class Product {
@Id
private Long id;
@Column
private String name;
@Column
private String description;
@Column(name = "price_cents")
private Integer priceCents;
@Column
private Integer quantity;
//Getters and setters are omitted for brevity
}
This time, when rerunning the previous test case, Hibernate generates the following SQL UPDATE statement:
Product
entity with a dynamic updateUPDATE
Product
SET
price_cents = ?
WHERE
id = ?
-- binding parameter [1] as [INTEGER] - [2499]
-- binding parameter [2] as [BIGINT] - [1]
The dynamic update allows you to set just the columns that were modified in the associated entity.
5.11. Refresh entity state
You can reload an entity instance and its collections at any time.
Person person = entityManager.find(Person.class, personId);
entityManager.createQuery("update Person set name = UPPER(name)").executeUpdate();
entityManager.refresh(person);
assertEquals("JOHN DOE", person.getName());
Person person = session.byId(Person.class).load(personId);
session.doWork(connection -> {
try(Statement statement = connection.createStatement()) {
statement.executeUpdate("UPDATE Person SET name = UPPER(name)");
}
});
session.refresh(person);
assertEquals("JOHN DOE", person.getName());
One case where this is useful is when it is known that the database state has changed since the data was read. Refreshing allows the current database state to be pulled into the entity instance and the persistence context.
Another case where this might be useful is when database triggers are used to initialize some of the properties of the entity.
Only the entity instance and its value type collections are refreshed unless you specify |
Traditionally, Hibernate allowed detached entities to be refreshed.
Unfortunately, Jakarta Persistence prohibits this practice and specifies that an For this reason, when bootstrapping the Hibernate However, this default behavior can be overwritten through the For more about the |
5.11.1. Refresh gotchas
The refresh
entity state transition is meant to overwrite the entity attributes according to the info currently contained in the associated database record.
However, you have to be very careful when cascading the refresh action to any transient entity.
For instance, consider the following example:
try {
Person person = entityManager.find(Person.class, personId);
Book book = new Book();
book.setId(100L);
book.setTitle("Hibernate User Guide");
book.setAuthor(person);
person.getBooks().add(book);
entityManager.refresh(person);
}
catch (EntityNotFoundException expected) {
log.info("Beware when cascading the refresh associations to transient entities!");
}
In the aforementioned example, an EntityNotFoundException
is thrown because the Book
entity is still in a transient state.
When the refresh action is cascaded from the Person
entity, Hibernate will not be able to locate the Book
entity in the database.
For this reason, you should be very careful when mixing the refresh action with transient child entity objects.
5.12. Working with detached data
Detachment is the process of working with data outside the scope of any persistence context. Data becomes detached in a number of ways. Once the persistence context is closed, all data that was associated with it becomes detached. Clearing the persistence context has the same effect. Evicting a particular entity from the persistence context makes it detached. And finally, serialization will make the deserialized form be detached (the original instance is still managed).
Detached data can still be manipulated, however, the persistence context will no longer automatically know about these modifications, and the application will need to intervene to make the changes persistent again.
5.12.1. Reattaching detached data
Reattachment is the process of taking an incoming entity instance that is in the detached state and re-associating it with the current persistence context.
Jakarta Persistence does not support reattaching detached data. This is only available through Hibernate |
lock
Person person = session.byId(Person.class).load(personId);
//Clear the Session so the person entity becomes detached
session.clear();
person.setName("Mr. John Doe");
session.lock(person, LockMode.NONE);
saveOrUpdate
Person person = session.byId(Person.class).load(personId);
//Clear the Session so the person entity becomes detached
session.clear();
person.setName("Mr. John Doe");
session.merge(person);
The method name |
Provided the entity is detached, update
and saveOrUpdate
operate exactly the same.
5.12.2. Merging detached data
Merging is the process of taking an incoming entity instance that is in the detached state and copying its data over onto a new managed instance.
Although not exactly per se, the following example is a good visualization of the merge
operation internals.
public Person merge(Person detached) {
Person newReference = session.byId(Person.class).load(detached.getId());
newReference.setName(detached.getName());
return newReference;
}
Person person = entityManager.find(Person.class, personId);
//Clear the EntityManager so the person entity becomes detached
entityManager.clear();
person.setName("Mr. John Doe");
person = entityManager.merge(person);
Person person = session.byId(Person.class).load(personId);
//Clear the Session so the person entity becomes detached
session.clear();
person.setName("Mr. John Doe");
person = (Person) session.merge(person);
Merging gotchas
For example, Hibernate throws IllegalStateException
when merging a parent entity which has references to 2 detached child entities child1
and child2
(obtained from different sessions), and child1
and child2
represent the same persistent entity, Child
.
A new configuration property, hibernate.event.merge.entity_copy_observer
, controls how Hibernate will respond when multiple representations of the same persistent entity ("entity copy") is detected while merging.
The possible values are:
- disallow (the default)
-
throws
IllegalStateException
if an entity copy is detected - allow
-
performs the merge operation on each entity copy that is detected
- log
-
(provided for testing only) performs the merge operation on each entity copy that is detected and logs information about the entity copies. This setting requires DEBUG logging be enabled for
org.hibernate.event.internal.EntityCopyAllowedLoggedObserver
In addition, the application may customize the behavior by providing an implementation of org.hibernate.event.spi.EntityCopyObserver
and setting hibernate.event.merge.entity_copy_observer
to the class name.
When this property is set to allow
or log
, Hibernate will merge each entity copy detected while cascading the merge operation.
In the process of merging each entity copy, Hibernate will cascade the merge operation from each entity copy to its associations with cascade=CascadeType.MERGE
or CascadeType.ALL
.
The entity state resulting from merging an entity copy will be overwritten when another entity copy is merged.
Because cascade order is undefined, the order in which the entity copies are merged is undefined. As a result, if property values in the entity copies are not consistent, the resulting entity state will be indeterminate, and data will be lost from all entity copies except for the last one merged. Therefore, the last writer wins. If an entity copy cascades the merge operation to an association that is (or contains) a new entity, that new entity will be merged (i.e., persisted and the merge operation will be cascaded to its associations according to its mapping), even if that same association is ultimately overwritten when Hibernate merges a different representation having a different value for its association. If the association is mapped with There are known issues when representations of the same persistent entity have different values for a collection. See HHH-9239 and HHH-9240 for more details. These issues can cause data loss or corruption. By setting The only way to exclude particular entity classes or associations that contain critical data is to provide a custom implementation of |
Hibernate provides limited DEBUG logging capabilities that can help determine the entity classes for which entity copies were found.
By setting
The log should be reviewed to determine if multiple representations of entities containing critical data are detected.
If so, the application should be modified so there is only one representation, and a custom implementation of Using optimistic locking is recommended to detect if different representations are from different versions of the same persistent entity.
If they are not from the same version, Hibernate will throw either the Jakarta Persistence |
5.13. Checking persistent state
An application can verify the state of entities and collections in relation to the persistence context.
boolean contained = entityManager.contains(person);
boolean contained = session.contains(person);
PersistenceUnitUtil persistenceUnitUtil = entityManager.getEntityManagerFactory().getPersistenceUnitUtil();
boolean personInitialized = persistenceUnitUtil.isLoaded(person);
boolean personBooksInitialized = persistenceUnitUtil.isLoaded(person.getBooks());
boolean personNameInitialized = persistenceUnitUtil.isLoaded(person, "name");
boolean personInitialized = Hibernate.isInitialized(person);
boolean personBooksInitialized = Hibernate.isInitialized(person.getBooks());
boolean personNameInitialized = Hibernate.isPropertyInitialized(person, "name");
In Jakarta Persistence there is an alternative means to check laziness using the following jakarta.persistence.PersistenceUtil
pattern (which is recommended wherever possible).
PersistenceUtil persistenceUnitUtil = Persistence.getPersistenceUtil();
boolean personInitialized = persistenceUnitUtil.isLoaded(person);
boolean personBooksInitialized = persistenceUnitUtil.isLoaded(person.getBooks());
boolean personNameInitialized = persistenceUnitUtil.isLoaded(person, "name");
5.14. Evicting entities
When the flush()
method is called, the state of the entity is synchronized with the database.
If you do not want this synchronization to occur, or if you are processing a huge number of objects and need to manage memory efficiently,
the evict()
method can be used to remove the object and its collections from the first-level cache.
EntityManager
for(Person person : entityManager.createQuery("select p from Person p", Person.class)
.getResultList()) {
dtos.add(toDTO(person));
entityManager.detach(person);
}
Session
Session session = entityManager.unwrap(Session.class);
for(Person person : (List<Person>) session.createSelectionQuery("select p from Person p").list()) {
dtos.add(toDTO(person));
session.evict(person);
}
To detach all entities from the current persistence context, both the EntityManager
and the Hibernate Session
define a clear()
method.
entityManager.clear();
session.clear();
To verify if an entity instance is currently attached to the running persistence context, both the EntityManager
and the Hibernate Session
define a contains(Object entity)
method.
entityManager.contains(person);
session.contains(person);
5.15. Cascading entity state transitions
Jakarta Persistence allows you to propagate the state transition from a parent entity to a child.
For this purpose, the Jakarta Persistence jakarta.persistence.CascadeType
defines various cascade types:
ALL
-
cascades all entity state transitions.
PERSIST
-
cascades the entity persist operation.
MERGE
-
cascades the entity merge operation.
REMOVE
-
cascades the entity remove operation.
REFRESH
-
cascades the entity refresh operation.
DETACH
-
cascades the entity detach operation.
Additionally, the CascadeType.ALL
will propagate any Hibernate-specific operation, which is defined by the org.hibernate.annotations.CascadeType
enum:
SAVE_UPDATE
-
cascades the entity saveOrUpdate operation.
REPLICATE
-
cascades the entity replicate operation.
LOCK
-
cascades the entity lock operation.
The following examples will explain some of the aforementioned cascade operations using the following entities:
@Entity
public class Person {
@Id
private Long id;
private String name;
@OneToMany(mappedBy = "owner", cascade = CascadeType.ALL)
private List<Phone> phones = new ArrayList<>();
//Getters and setters are omitted for brevity
public void addPhone(Phone phone) {
this.phones.add(phone);
phone.setOwner(this);
}
}
@Entity
public class Phone {
@Id
private Long id;
@Column(name = "`number`")
private String number;
@ManyToOne(fetch = FetchType.LAZY)
private Person owner;
//Getters and setters are omitted for brevity
}
5.15.1. CascadeType.PERSIST
The CascadeType.PERSIST
allows us to persist a child entity along with the parent one.
CascadeType.PERSIST
examplePerson person = new Person();
person.setId(1L);
person.setName("John Doe");
Phone phone = new Phone();
phone.setId(1L);
phone.setNumber("123-456-7890");
person.addPhone(phone);
entityManager.persist(person);
INSERT INTO Person ( name, id )
VALUES ( 'John Doe', 1 )
INSERT INTO Phone ( `number`, person_id, id )
VALUE ( '123-456-7890', 1, 1 )
Even if just the Person
parent entity was persisted, Hibernate has managed to cascade the persist operation to the associated Phone
child entity as well.
5.15.2. CascadeType.MERGE
The CascadeType.MERGE
allows us to merge a child entity along with the parent one.
CascadeType.MERGE
examplePhone phone = entityManager.find(Phone.class, 1L);
Person person = phone.getOwner();
person.setName("John Doe Jr.");
phone.setNumber("987-654-3210");
entityManager.clear();
entityManager.merge(person);
SELECT
p.id as id1_0_1_,
p.name as name2_0_1_,
ph.owner_id as owner_id3_1_3_,
ph.id as id1_1_3_,
ph.id as id1_1_0_,
ph."number" as number2_1_0_,
ph.owner_id as owner_id3_1_0_
FROM
Person p
LEFT OUTER JOIN
Phone ph
on p.id=ph.owner_id
WHERE
p.id = 1
During merge, the current state of the entity is copied onto the entity version that was just fetched from the database.
That’s the reason why Hibernate executed the SELECT statement which fetched both the Person
entity along with its children.
5.15.3. CascadeType.REMOVE
The CascadeType.REMOVE
allows us to remove a child entity along with the parent one.
Traditionally, Hibernate called this operation delete, that’s why the org.hibernate.annotations.CascadeType
provides a DELETE
cascade option.
However, CascadeType.REMOVE
and org.hibernate.annotations.CascadeType.DELETE
are identical.
CascadeType.REMOVE
examplePerson person = entityManager.find(Person.class, 1L);
entityManager.remove(person);
DELETE FROM Phone WHERE id = 1
DELETE FROM Person WHERE id = 1
5.15.4. CascadeType.DETACH
CascadeType.DETACH
is used to propagate the detach operation from a parent entity to a child.
CascadeType.DETACH
examplePerson person = entityManager.find(Person.class, 1L);
assertEquals(1, person.getPhones().size());
Phone phone = person.getPhones().get(0);
assertTrue(entityManager.contains(person));
assertTrue(entityManager.contains(phone));
entityManager.detach(person);
assertFalse(entityManager.contains(person));
assertFalse(entityManager.contains(phone));
5.15.5. CascadeType.LOCK
Although unintuitively, CascadeType.LOCK
does not propagate a lock request from a parent entity to its children.
Such a use case requires the use of the PessimisticLockScope.EXTENDED
value of the jakarta.persistence.lock.scope
property.
However, CascadeType.LOCK
allows us to reattach a parent entity along with its children to the currently running Persistence Context.
CascadeType.LOCK
examplePerson person = entityManager.find(Person.class, 1L);
assertEquals(1, person.getPhones().size());
Phone phone = person.getPhones().get(0);
assertTrue(entityManager.contains(person));
assertTrue(entityManager.contains(phone));
entityManager.detach(person);
assertFalse(entityManager.contains(person));
assertFalse(entityManager.contains(phone));
entityManager.unwrap(Session.class)
.lock(person, new LockOptions(LockMode.NONE));
assertTrue(entityManager.contains(person));
assertTrue(entityManager.contains(phone));
5.15.6. CascadeType.REFRESH
The CascadeType.REFRESH
is used to propagate the refresh operation from a parent entity to a child.
The refresh operation will discard the current entity state, and it will override it using the one loaded from the database.
CascadeType.REFRESH
examplePerson person = entityManager.find(Person.class, 1L);
Phone phone = person.getPhones().get(0);
person.setName("John Doe Jr.");
phone.setNumber("987-654-3210");
entityManager.refresh(person);
assertEquals("John Doe", person.getName());
assertEquals("123-456-7890", phone.getNumber());
SELECT
p.id as id1_0_1_,
p.name as name2_0_1_,
ph.owner_id as owner_id3_1_3_,
ph.id as id1_1_3_,
ph.id as id1_1_0_,
ph."number" as number2_1_0_,
ph.owner_id as owner_id3_1_0_
FROM
Person p
LEFT OUTER JOIN
Phone ph
ON p.id=ph.owner_id
WHERE
p.id = 1
In the aforementioned example, you can see that both the Person
and Phone
entities are refreshed even if we only called this operation on the parent entity only.
5.15.7. CascadeType.REPLICATE
The CascadeType.REPLICATE
is to replicate both the parent and the child entities.
The replicate operation allows you to synchronize entities coming from different sources of data.
CascadeType.REPLICATE
examplePerson person = new Person();
person.setId(1L);
person.setName("John Doe Sr.");
Phone phone = new Phone();
phone.setId(1L);
phone.setNumber("(01) 123-456-7890");
person.addPhone(phone);
entityManager.unwrap(Session.class).replicate(person, ReplicationMode.OVERWRITE);
SELECT
id
FROM
Person
WHERE
id = 1
SELECT
id
FROM
Phone
WHERE
id = 1
UPDATE
Person
SET
name = 'John Doe Sr.'
WHERE
id = 1
UPDATE
Phone
SET
"number" = '(01) 123-456-7890',
owner_id = 1
WHERE
id = 1
As illustrated by the SQL statements being generated, both the Person
and Phone
entities are replicated to the underlying database rows.
5.15.8. @OnDelete
cascade
While the previous cascade types propagate entity state transitions, the @OnDelete
cascade is a DDL-level FK feature which allows you
to remove a child record whenever the parent row is deleted.
So, when annotating the @ManyToOne
association with @OnDelete( action = OnDeleteAction.CASCADE )
,
the automatic schema generator will apply the ON DELETE CASCADE SQL directive to the Foreign Key declaration,
as illustrated by the following example.
@OnDelete
@ManyToOne
mapping@Entity(name = "Person")
public static class Person {
@Id
private Long id;
private String name;
//Getters and setters are omitted for brevity
}
@Entity(name = "Phone")
public static class Phone {
@Id
private Long id;
@Column(name = "`number`")
private String number;
@ManyToOne(fetch = LAZY)
@OnDelete(action = CASCADE)
private Person owner;
//Getters and setters are omitted for brevity
}
create table Person (
id bigint not null,
name varchar(255),
primary key (id)
)
create table Phone (
id bigint not null,
"number" varchar(255),
owner_id bigint,
primary key (id)
)
alter table Phone
add constraint FK82m836qc1ss2niru7eogfndhl
foreign key (owner_id)
references Person
on delete cascade
Now, you can just remove the Person
entity, and the associated Phone
entities are going to be deleted automatically via the Foreign Key cascade.
@OnDelete
@ManyToOne
delete examplePerson person = entityManager.find(Person.class, 1L);
entityManager.remove(person);
delete from Person where id = ?
-- binding parameter [1] as [BIGINT] - [1]
The @OnDelete
annotation can also be placed on a collection, as
illustrated in the following example.
@OnDelete
@OneToMany
mapping@Entity(name = "Person")
public static class Person {
@Id
private Long id;
private String name;
@OneToMany(mappedBy = "owner", cascade = CascadeType.ALL)
@OnDelete(action = OnDeleteAction.CASCADE)
private List<Phone> phones = new ArrayList<>();
//Getters and setters are omitted for brevity
}
@Entity(name = "Phone")
public static class Phone {
@Id
private Long id;
@Column(name = "`number`")
private String number;
@ManyToOne(fetch = FetchType.LAZY)
private Person owner;
//Getters and setters are omitted for brevity
}
Now, when removing the Person
entity, all the associated Phone
child entities are deleted via the Foreign Key cascade even if the @OneToMany
collection was using the CascadeType.ALL
attribute.
@OnDelete
@ManyToOne
delete examplePerson person = entityManager.find(Person.class, 1L);
entityManager.remove(person);
delete from Person where id = ?
-- binding parameter [1] as [BIGINT] - [1]
Without the This way, only the parent entity gets deleted, and all the associated child records are removed by the database engine, instead of being deleted explicitly via |
5.16. Exception handling
If the Jakarta Persistence EntityManager
or the Hibernate-specific Session
throws an exception, including any JDBC SQLException
, you have to immediately rollback the database transaction and close the current EntityManager
or Session
.
Certain methods of the Jakarta Persistence EntityManager
or the Hibernate Session
will not leave the Persistence Context in a consistent state. As a rule of thumb, no exception thrown by Hibernate can be treated as recoverable. Ensure that the Session will be closed by calling the close()
method in a finally block.
Rolling back the database transaction does not put your business objects back into the state they were at the start of the transaction. This means that the database state and the business objects will be out of sync. Usually, this is not a problem because exceptions are not recoverable and you will have to start over after rollback anyway.
The Jakarta Persistence PersistenceException
or the
HibernateException
wraps most of the errors that can occur in a Hibernate persistence layer.
Both the PersistenceException
and the HibernateException
are runtime exceptions because, in our opinion, we should not force the application developer to catch an unrecoverable exception at a low layer. In most systems, unchecked and fatal exceptions are handled in one of the first frames of the method call stack (i.e., in higher layers) and either an error message is presented to the application user or some other appropriate action is taken. Note that Hibernate might also throw other unchecked exceptions that are not a HibernateException
. These are not recoverable either, and appropriate action should be taken.
Hibernate wraps the JDBC SQLException
, thrown while interacting with the database, in a
JDBCException
.
In fact, Hibernate will attempt to convert the exception into a more meaningful subclass of JDBCException
. The underlying SQLException
is always available via JDBCException.getSQLException()
. Hibernate converts the SQLException
into an appropriate JDBCException subclass using the
SQLExceptionConverter
attached to the current SessionFactory
.
By default, the SQLExceptionConverter
is defined by the configured Hibernate Dialect
via the
buildSQLExceptionConversionDelegate
method
which is overridden by several database-specific Dialect
s.
The standard JDBCException
subtypes are:
- ConstraintViolationException
-
indicates some form of integrity constraint violation.
- DataException
-
indicates that evaluation of the valid SQL statement against the given data resulted in some illegal operation, mismatched types, truncation or incorrect cardinality.
- GenericJDBCException
-
a generic exception which did not fall into any of the other categories.
- JDBCConnectionException
-
indicates an error with the underlying JDBC communication.
- LockAcquisitionException
-
indicates an error acquiring a lock level necessary to perform the requested operation.
- LockTimeoutException
-
indicates that the lock acquisition request has timed out.
- PessimisticLockException
-
indicates that a lock acquisition request has failed.
- QueryTimeoutException
-
indicates that the current executing query has timed out.
- SQLGrammarException
-
indicates a grammar or syntax problem with the issued SQL.
Starting with Hibernate 5.2, the Hibernate If your |
6. Flushing
Flushing is the process of synchronizing the state of the persistence context with the underlying database.
The EntityManager
and the Hibernate Session
expose a set of methods, through which the application developer can change the persistent state of an entity.
The persistence context acts as a transactional write-behind cache, queuing any entity state change.
Like any write-behind cache, changes are first applied in-memory and synchronized with the database during the flush time.
The flush operation takes every entity state change and translates it to an INSERT
, UPDATE
or DELETE
statement.
Because DML statements are grouped together, Hibernate can apply batching transparently. See the Batching chapter for more information. |
The flushing strategy is given by the flushMode
of the current running Hibernate Session
.
Although Jakarta Persistence defines only two flushing strategies (AUTO
and COMMIT
),
Hibernate has a much broader spectrum of flush types:
- ALWAYS
-
Flushes the
Session
before every query. - AUTO
-
This is the default mode, and it flushes the
Session
only if necessary. - COMMIT
-
The
Session
tries to delay the flush until the currentTransaction
is committed, although it might flush prematurely too. - MANUAL
-
The
Session
flushing is delegated to the application, which must callSession.flush()
explicitly in order to apply the persistence context changes.
6.1. AUTO
flush
By default, Hibernate uses the AUTO
flush mode which triggers a flush in the following circumstances:
-
prior to committing a
Transaction
-
prior to executing a JPQL/HQL query that overlaps with the queued entity actions
-
before executing any native SQL query that has no registered synchronization
6.1.1. AUTO
flush on commit
In the following example, an entity is persisted, and then the transaction is committed.
entityManager = entityManagerFactory().createEntityManager();
txn = entityManager.getTransaction();
txn.begin();
Person person = new Person("John Doe");
entityManager.persist(person);
log.info("Entity is in persisted state");
txn.commit();
--INFO: Entity is in persisted state
INSERT INTO Person (name, id) VALUES ('John Doe', 1)
Hibernate logs the message prior to inserting the entity because the flush only occurred during transaction commit.
This is valid for the |
6.1.2. AUTO
flush on JPQL/HQL query
A flush may also be triggered when executing an entity query.
Person person = new Person("John Doe");
entityManager.persist(person);
entityManager.createQuery("select p from Advertisement p").getResultList();
entityManager.createQuery("select p from Person p").getResultList();
SELECT a.id AS id1_0_ ,
a.title AS title2_0_
FROM Advertisement a
INSERT INTO Person (name, id) VALUES ('John Doe', 1)
SELECT p.id AS id1_1_ ,
p.name AS name2_1_
FROM Person p
The reason why the Advertisement
entity query didn’t trigger a flush is that there’s no overlapping between the Advertisement
and the Person
tables:
@Entity(name = "Person")
public static class Person {
@Id
@GeneratedValue
private Long id;
private String name;
//Getters and setters are omitted for brevity
}
@Entity(name = "Advertisement")
public static class Advertisement {
@Id
@GeneratedValue
private Long id;
private String title;
//Getters and setters are omitted for brevity
}
When querying for a Person
entity, the flush is triggered prior to executing the entity query.
Person person = new Person("John Doe");
entityManager.persist(person);
entityManager.createQuery("select p from Person p").getResultList();
INSERT INTO Person (name, id) VALUES ('John Doe', 1)
SELECT p.id AS id1_1_ ,
p.name AS name2_1_
FROM Person p
This time, the flush was triggered by a JPQL query because the pending entity persisting action overlaps with the query being executed.
6.1.3. AUTO
flush on native SQL query
When executing a native SQL query, a flush is always triggered when using the EntityManager
API.
EntityManager
assertTrue(((Number) entityManager
.createNativeQuery("select count(*) from Person")
.getSingleResult()).intValue() == 0);
Person person = new Person("John Doe");
entityManager.persist(person);
assertTrue(((Number) entityManager
.createNativeQuery("select count(*) from Person")
.getSingleResult()).intValue() == 1);
If you bootstrap Hibernate natively, and not through Jakarta Persistence, by default,
the Session
API will trigger a flush automatically when executing a native query.
Session
assertTrue(((Number) session
.createNativeQuery("select count(*) from Person", Integer.class)
.getSingleResult()).intValue() == 0);
Person person = new Person("John Doe");
session.persist(person);
assertTrue(((Number) session
.createNativeQuery("select count(*) from Person", Integer.class)
.uniqueResult()).intValue() == 0);
To flush the Session
, the query must use a synchronization:
Session
synchronizationassertTrue(((Number) entityManager
.createNativeQuery("select count(*) from Person")
.getSingleResult()).intValue() == 0);
Person person = new Person("John Doe");
entityManager.persist(person);
Session session = entityManager.unwrap(Session.class);
assertTrue(((Number) session
.createNativeQuery("select count(*) from Person", Integer.class)
.addSynchronizedEntityClass(Person.class)
.uniqueResult()).intValue() == 1);
6.2. COMMIT
flush
Jakarta Persistence also defines a COMMIT flush mode, which is described as follows:
If
FlushModeType.COMMIT
is set, the effect of updates made to entities in the persistence context upon queries is unspecified.
When executing a JPQL query, the persistence context is only flushed when the current running transaction is committed.
COMMIT
flushing on JPQLPerson person = new Person("John Doe");
entityManager.persist(person);
entityManager.createQuery("select p from Advertisement p")
.setFlushMode(FlushModeType.COMMIT)
.getResultList();
entityManager.createQuery("select p from Person p")
.setFlushMode(FlushModeType.COMMIT)
.getResultList();
SELECT a.id AS id1_0_ ,
a.title AS title2_0_
FROM Advertisement a
SELECT p.id AS id1_1_ ,
p.name AS name2_1_
FROM Person p
INSERT INTO Person (name, id) VALUES ('John Doe', 1)
Because the Jakarta Persistence doesn’t impose a strict rule on delaying flushing, when executing a native SQL query, the persistence context is going to be flushed.
COMMIT
flushing on native SQLPerson person = new Person("John Doe");
entityManager.persist(person);
assertTrue(((Number) entityManager
.createNativeQuery("select count(*) from Person")
.getSingleResult()).intValue() == 1);
INSERT INTO Person (name, id) VALUES ('John Doe', 1)
SELECT COUNT(*) FROM Person
6.3. ALWAYS
flush
The |
The ALWAYS
flush mode triggers a persistence context flush even when executing a native SQL query against the Session
API.
COMMIT
flushing on native SQLPerson person = new Person("John Doe");
entityManager.persist(person);
Session session = entityManager.unwrap(Session.class);
assertTrue(((Number) session
.createNativeQuery("select count(*) from Person", Integer.class)
.setHibernateFlushMode(FlushMode.ALWAYS)
.uniqueResult()).intValue() == 1);
INSERT INTO Person (name, id) VALUES ('John Doe', 1)
SELECT COUNT(*) FROM Person
6.4. MANUAL
flush
Both the EntityManager
and the Hibernate Session
define a flush()
method that, when called, triggers a manual flush.
Hibernate also provides a MANUAL
flush mode so the persistence context can only be flushed manually.
MANUAL
flushingPerson person = new Person("John Doe");
entityManager.persist(person);
Session session = entityManager.unwrap(Session.class);
session.setHibernateFlushMode(FlushMode.MANUAL);
assertTrue(((Number) entityManager
.createQuery("select count(id) from Person")
.getSingleResult()).intValue() == 0);
assertTrue(((Number) session
.createNativeQuery("select count(*) from Person", Integer.class)
.uniqueResult()).intValue() == 0);
SELECT COUNT(p.id) AS col_0_0_
FROM Person p
SELECT COUNT(*)
FROM Person
The INSERT
statement was not executed because there was no manual flush()
call.
The |
6.5. Flush operation order
From a database perspective, a row state can be altered using either an INSERT
, an UPDATE
or a DELETE
statement.
Because entity state changes are automatically converted to SQL statements, it’s important to know which entity actions are associated with a given SQL statement.
INSERT
-
The
INSERT
statement is generated either by theEntityInsertAction
orEntityIdentityInsertAction
. These actions are scheduled by thepersist
operation, either explicitly or through cascading thePersistEvent
from a parent to a child entity. DELETE
-
The
DELETE
statement is generated by theEntityDeleteAction
orOrphanRemovalAction
. UPDATE
-
The
UPDATE
statement is generated byEntityUpdateAction
during flushing if the managed entity has been marked modified. The dirty checking mechanism is responsible for determining if a managed entity has been modified since it was first loaded.
Hibernate does not execute the SQL statements in the order of their associated entity state operations.
To visualize how this works, consider the following example:
Person person = entityManager.find(Person.class, 1L);
entityManager.remove(person);
Person newPerson = new Person();
newPerson.setId(2L);
newPerson.setName("John Doe");
entityManager.persist(newPerson);
INSERT INTO Person (name, id)
VALUES ('John Doe', 2L)
DELETE FROM Person WHERE id = 1
Even if we removed the first entity and then persist a new one, Hibernate is going to execute the DELETE
statement after the INSERT
.
The order in which SQL statements are executed is given by the |
The ActionQueue
executes all operations in the following order:
-
OrphanRemovalAction
-
EntityInsertAction
orEntityIdentityInsertAction
-
EntityUpdateAction
-
QueuedOperationCollectionAction
-
CollectionRemoveAction
-
CollectionUpdateAction
-
CollectionRecreateAction
-
EntityDeleteAction
7. Database access
7.1. ConnectionProvider
As an ORM tool, probably the single most important thing you need to tell Hibernate is how to connect to your database so that it may connect on behalf of your application.
This is ultimately the function of the org.hibernate.engine.jdbc.connections.spi.ConnectionProvider
interface.
Hibernate provides some out of the box implementations of this interface.
ConnectionProvider
is also an extension point so you can also use custom implementations from third parties or written yourself.
The ConnectionProvider
to use is defined by the hibernate.connection.provider_class
setting. See the org.hibernate.cfg.AvailableSettings#CONNECTION_PROVIDER
Generally speaking, applications should not have to configure a ConnectionProvider
explicitly if using one of the Hibernate-provided implementations.
Hibernate will internally determine which ConnectionProvider
to use based on the following algorithm:
-
If
hibernate.connection.provider_class
is set, it takes precedence -
else if
hibernate.connection.datasource
is set → Using DataSources -
else if any setting prefixed by
hibernate.c3p0.
is set → Using c3p0 -
else if any setting prefixed by
hibernate.proxool.
is set → Using Proxool -
else if any setting prefixed by
hibernate.hikari.
is set → Using HikariCP -
else if any setting prefixed by
hibernate.vibur.
is set → Using Vibur DBCP -
else if any setting prefixed by
hibernate.agroal.
is set → Using Agroal -
else if
hibernate.connection.url
is set → Using Hibernate’s built-in (and unsupported) pooling -
else → User-provided Connections
7.2. Using DataSources
Hibernate can integrate with a javax.sql.DataSource
for obtaining JDBC Connections.
Applications would tell Hibernate about the DataSource
via the (required) hibernate.connection.datasource
setting which can either specify a JNDI name or would reference the actual DataSource
instance.
For cases where a JNDI name is given, be sure to read JNDI.
For Jakarta Persistence applications, note that |
The DataSource
ConnectionProvider
also (optionally) accepts the hibernate.connection.username
and hibernate.connection.password
.
If specified, the DataSource#getConnection(String username, String password)
will be used.
Otherwise, the no-arg form is used.
7.3. Driver Configuration
hibernate.connection.driver_class
-
The name of the JDBC Driver class to use
hibernate.connection.url
-
The JDBC connection url
hibernate.connection.*
-
All such setting names (except the predefined ones) will have the
hibernate.connection.
prefix stripped. The remaining name and the original value will be passed to the driver as a JDBC connection property
Not all properties apply to all situations. For example, if you are providing a data source, hibernate.connection.driver_class setting will not be used.
|
7.4. Using c3p0
To use the c3p0 integration, the application must include the |
Hibernate also provides support for applications to use c3p0 connection pooling. When c3p0 support is enabled, a number of c3p0-specific configuration settings are recognized in addition to the general ones described in Driver Configuration.
Transaction isolation of the Connections is managed by the ConnectionProvider
itself. See ConnectionProvider support for transaction isolation setting.
hibernate.c3p0.min_size
orc3p0.minPoolSize
-
The minimum size of the c3p0 pool. See c3p0 minPoolSize
hibernate.c3p0.max_size
orc3p0.maxPoolSize
-
The maximum size of the c3p0 pool. See c3p0 maxPoolSize
hibernate.c3p0.timeout
orc3p0.maxIdleTime
-
The Connection idle time. See c3p0 maxIdleTime
hibernate.c3p0.max_statements
orc3p0.maxStatements
-
Controls the c3p0 PreparedStatement cache size (if using). See c3p0 maxStatements
hibernate.c3p0.acquire_increment
orc3p0.acquireIncrement
-
Number of connections c3p0 should acquire at a time when the pool is exhausted. See c3p0 acquireIncrement
hibernate.c3p0.idle_test_period
orc3p0.idleConnectionTestPeriod
-
Idle time before a c3p0 pooled connection is validated. See c3p0 idleConnectionTestPeriod
hibernate.c3p0.initialPoolSize
-
The initial c3p0 pool size. If not specified, default is to use the min pool size. See c3p0 initialPoolSize
- Any other settings prefixed with
hibernate.c3p0.
-
Will have the
hibernate.
portion stripped and be passed to c3p0. - Any other settings prefixed with
c3p0.
-
Get passed to c3p0 as is. See c3p0 configuration
7.5. Using Proxool
To use the Proxool integration, the application must include the |
Hibernate also provides support for applications to use Proxool connection pooling.
Transaction isolation of the Connections is managed by the ConnectionProvider
itself. See ConnectionProvider support for transaction isolation setting.
7.5.1. Using existing Proxool pools
Controlled by the hibernate.proxool.existing_pool
setting.
If set to true, this ConnectionProvider will use an already existing Proxool pool by alias as indicated by the hibernate.proxool.pool_alias
setting.
7.5.2. Configuring Proxool via XML
The hibernate.proxool.xml
setting names a Proxool configuration XML file to be loaded as a classpath resource and loaded by Proxool’s JAXPConfigurator.
See proxool configuration.
hibernate.proxool.pool_alias
must be set to indicate which pool to use.
7.5.3. Configuring Proxool via Properties
The hibernate.proxool.properties
setting names a Proxool configuration properties file to be loaded as a classpath resource and loaded by Proxool’s PropertyConfigurator
.
See proxool configuration.
hibernate.proxool.pool_alias
must be set to indicate which pool to use.
7.6. Using HikariCP
To use the HikariCP this integration, the application must include the |
Hibernate also provides support for applications to use HikariCP connection pool.
Set all of your Hikari settings in Hibernate prefixed by hibernate.hikari.
and this ConnectionProvider
will pick them up and pass them along to Hikari.
Additionally, this ConnectionProvider
will pick up the following Hibernate-specific properties and map them to the corresponding Hikari ones (any hibernate.hikari.
prefixed ones have precedence):
hibernate.connection.driver_class
-
Mapped to Hikari’s
driverClassName
setting hibernate.connection.url
-
Mapped to Hikari’s
jdbcUrl
setting hibernate.connection.username
-
Mapped to Hikari’s
username
setting hibernate.connection.password
-
Mapped to Hikari’s
password
setting hibernate.connection.isolation
-
Mapped to Hikari’s
transactionIsolation
setting. See ConnectionProvider support for transaction isolation setting. Note that Hikari only supports JDBC standard isolation levels (apparently). hibernate.connection.autocommit
-
Mapped to Hikari’s
autoCommit
setting
7.7. Using Vibur DBCP
To use the Vibur DBCP integration, the application must include the |
Hibernate also provides support for applications to use Vibur DBCP connection pool.
Set all of your Vibur settings in Hibernate prefixed by hibernate.vibur.
and this ConnectionProvider
will pick them up and pass them along to Vibur DBCP.
Additionally, this ConnectionProvider
will pick up the following Hibernate-specific properties and map them to the corresponding Vibur ones (any hibernate.vibur.
prefixed ones have precedence):
hibernate.connection.driver_class
-
Mapped to Vibur’s
driverClassName
setting hibernate.connection.url
-
Mapped to Vibur’s
jdbcUrl
setting hibernate.connection.username
-
Mapped to Vibur’s
username
setting hibernate.connection.password
-
Mapped to Vibur’s
password
setting hibernate.connection.isolation
-
Mapped to Vibur’s
defaultTransactionIsolationValue
setting. See ConnectionProvider support for transaction isolation setting. hibernate.connection.autocommit
-
Mapped to Vibur’s
defaultAutoCommit
setting
7.8. Using Agroal
To use the Agroal integration, the application must include the |
Hibernate also provides support for applications to use Agroal connection pool.
Set all of your Agroal settings in Hibernate prefixed by hibernate.agroal.
and this ConnectionProvider
will pick them up and pass them along to Agroal connection pool.
Additionally, this ConnectionProvider
will pick up the following Hibernate-specific properties and map them to the corresponding Agroal ones (any hibernate.agroal.
prefixed ones have precedence):
hibernate.connection.driver_class
-
Mapped to Agroal’s
driverClassName
setting hibernate.connection.url
-
Mapped to Agroal’s
jdbcUrl
setting hibernate.connection.username
-
Mapped to Agroal’s
principal
setting hibernate.connection.password
-
Mapped to Agroal’s
credential
setting hibernate.connection.isolation
-
Mapped to Agroal’s
jdbcTransactionIsolation
setting. See ConnectionProvider support for transaction isolation setting. hibernate.connection.autocommit
-
Mapped to Agroal’s
autoCommit
setting
7.9. Using Hibernate’s built-in (and unsupported) pooling
The built-in connection pool is not supported for use in a production system. |
This section is here just for completeness.
7.10. User-provided Connections
It is possible to use Hibernate by simply passing a Connection to use to the Session when the Session is opened. This usage is discouraged and not discussed here.
7.11. ConnectionProvider support for transaction isolation setting
All of the provided ConnectionProvider implementations, other than DataSourceConnectionProvider
, support consistent setting of transaction isolation for all Connections
obtained from the underlying pool.
The value for hibernate.connection.isolation
can be specified in one of 3 formats:
-
the integer value accepted at the JDBC level.
-
the name of the
java.sql.Connection
constant field representing the isolation you would like to use. For example,TRANSACTION_REPEATABLE_READ
forjava.sql.Connection#TRANSACTION_REPEATABLE_READ
. Not that this is only supported for JDBC standard isolation levels, not for isolation levels specific to a particular JDBC driver. -
a short-name version of the java.sql.Connection constant field without the
TRANSACTION_
prefix. For example,REPEATABLE_READ
forjava.sql.Connection#TRANSACTION_REPEATABLE_READ
. Again, this is only supported for JDBC standard isolation levels, not for isolation levels specific to a particular JDBC driver.
7.12. Connection handling
The connection handling mode is defined by the
PhysicalConnectionHandlingMode
enumeration which provides the following strategies:
IMMEDIATE_ACQUISITION_AND_HOLD
-
The
Connection
will be acquired as soon as theSession
is opened and held until theSession
is closed. DELAYED_ACQUISITION_AND_HOLD
-
The
Connection
will be acquired as soon as it is needed and then held until theSession
is closed. DELAYED_ACQUISITION_AND_RELEASE_AFTER_STATEMENT
-
The
Connection
will be acquired as soon as it is needed and will be released after each statement is executed. DELAYED_ACQUISITION_AND_RELEASE_AFTER_TRANSACTION
-
The
Connection
will be acquired as soon as it is needed and will be released after each transaction is completed.
If you don’t want to use the default connection handling mode, you can specify a connection handling mode via the hibernate.connection.handling_mode
configuration property. For more details, check out the
Database connection properties section.
7.12.1. Transaction type and connection handling
By default, the connection handling mode is given by the underlying transaction coordinator. There are two types of transactions: RESOURCE_LOCAL
(which involves a single database Connection
and the transaction is controlled via the commit
and rollback
Connection
methods) and JTA
(which may involve multiple resources including database connections, JMS queues, etc).
RESOURCE_LOCAL transaction connection handling
For RESOURCE_LOCAL
transactions, the connection handling mode is DELAYED_ACQUISITION_AND_RELEASE_AFTER_TRANSACTION
meaning that the database connection is acquired when needed and released after the current running transaction is either committed or rolled back.
However, because Hibernate needs to make sure that the default autocommit mode is disabled on the JDBC Connection
when starting a new transaction, the Connection
is acquired and the autocommit mode is set to false
.
If you are using a connection pool |
JTA transaction connection handling
For JTA
transactions, the connection handling mode is DELAYED_ACQUISITION_AND_RELEASE_AFTER_STATEMENT
meaning that the database connection is acquired when needed and released after each statement execution.
The reason for releasing the database connection after statement execution is because some Java EE application servers
report a connection leak when a method call goes from one EJB to another. However, even if the JDBC Connection
is released to the pool, the Connection
is still allocated to the current executing Thread
, hence when executing a subsequent statement in the current running transaction, the same Connection
object reference will be obtained from the pool.
If the Java EE application server or JTA transaction manager supports switching from one EJB to another while the transaction gets propagated from the outer EJB to the inner one,
and no connection leak false positive is being reported, then you should consider switching to |
7.12.2. User-provided connections
If the current Session
was created using the
SessionBuilder
and a JDBC Connection
was provided via the
SessionBuilder#connection
method, then the user-provided Connection
is going to be used, and
the connection handling mode will be IMMEDIATE_ACQUISITION_AND_HOLD
.
Therefore for user-provided connection, the connection is acquired right away and held until the current Session
is closed, without being influenced by the Jakarta Persistence or Hibernate transaction context.
7.13. Database Dialect
Although SQL is relatively standardized, each database vendor uses a subset and superset of ANSI SQL defined syntax.
This is referred to as the database’s dialect.
Hibernate handles variations across these dialects through its org.hibernate.dialect.Dialect
class and the various subclasses for each database vendor.
In most cases, Hibernate will be able to determine the proper Dialect to use by asking some questions of the JDBC Connection during bootstrap. For information on Hibernate’s ability to determine the proper Dialect to use (and your ability to influence that resolution), see Dialect resolution.
If for some reason it is not able to determine the proper one or you want to use a custom Dialect, you will need to set the hibernate.dialect
setting.
Dialect (short name) | Remarks |
---|---|
Cache71 |
Support for the Caché database, version 2007.1. |
CockroachDB192 |
Support for the CockroachDB database version 19.2. |
CockroachDB201 |
Support for the CockroachDB database version 20.1. |
CUBRID |
Support for the CUBRID database, version 8.3. May work with later versions. |
DB2 |
Support for the DB2 database, version 8.2. |
DB297 |
Support for the DB2 database, version 9.7. |
DB2390 |
Support for DB2 Universal Database for OS/390, also known as DB2/390. |
DB2400 |
Support for DB2 Universal Database for iSeries, also known as DB2/400. |
DB2400V7R3 |
Support for DB2 Universal Database for i, also known as DB2/400, version 7.3 |
DerbyTenFive |
Support for the Derby database, version 10.5 |
DerbyTenSix |
Support for the Derby database, version 10.6 |
DerbyTenSeven |
Support for the Derby database, version 10.7 |
Firebird |
Support for the Firebird database |
FrontBase |
Support for the Frontbase database |
H2 |
Support for the H2 database |
HANACloudColumnStore |
Support for the SAP HANA Cloud database column store. |
HANAColumnStore |
Support for the SAP HANA database column store, version 2.x. This is the recommended dialect for the SAP HANA database. May work with SAP HANA, version 1.x |
HANARowStore |
Support for the SAP HANA database row store, version 2.x. May work with SAP HANA, version 1.x |
HSQL |
Support for the HSQL (HyperSQL) database |
Informix |
Support for the Informix database |
Ingres |
Support for the Ingres database, version 9.2 |
Ingres9 |
Support for the Ingres database, version 9.3. May work with newer versions |
Ingres10 |
Support for the Ingres database, version 10. May work with newer versions |
Interbase |
Support for the Interbase database. |
JDataStore |
Support for the JDataStore database |
McKoi |
Support for the McKoi database |
Mimer |
Support for the Mimer database, version 9.2.1. May work with newer versions |
MySQL5 |
Support for the MySQL database, version 5.x |
MySQL5InnoDB |
Support for the MySQL database, version 5.x preferring the InnoDB storage engine when exporting tables. |
MySQL57InnoDB |
Support for the MySQL database, version 5.7 preferring the InnoDB storage engine when exporting tables. May work with newer versions |
MariaDB |
Support for the MariaDB database. May work with newer versions |
MariaDB53 |
Support for the MariaDB database, version 5.3 and newer. |
Oracle8i |
Support for the Oracle database, version 8i |
Oracle9i |
Support for the Oracle database, version 9i |
Oracle10g |
Support for the Oracle database, version 10g |
Pointbase |
Support for the Pointbase database |
PostgresPlus |
Support for the Postgres Plus database |
PostgreSQL81 |
Support for the PostgrSQL database, version 8.1 |
PostgreSQL82 |
Support for the PostgreSQL database, version 8.2 |
PostgreSQL9 |
Support for the PostgreSQL database, version 9. May work with later versions. |
Progress |
Support for the Progress database, version 9.1C. May work with newer versions. |
SAPDB |
Support for the SAPDB/MAXDB database. |
SQLServer |
Support for the SQL Server 2000 database |
SQLServer2005 |
Support for the SQL Server 2005 database |
SQLServer2008 |
Support for the SQL Server 2008 database |
Sybase11 |
Support for the Sybase database, up to version 11.9.2 |
SybaseAnywhere |
Support for the Sybase Anywhere database |
SybaseASE15 |
Support for the Sybase Adaptive Server Enterprise database, version 15 |
SybaseASE157 |
Support for the Sybase Adaptive Server Enterprise database, version 15.7. May work with newer versions. |
Teradata |
Support for the Teradata database |
TimesTen |
Support for the TimesTen database, version 5.1. May work with newer versions |
8. Transactions and concurrency control
It is important to understand that the term transaction has many different yet related meanings in regards to persistence and Object/Relational Mapping. In most use-cases these definitions align, but that is not always the case.
-
It might refer to the physical transaction with the database.
-
It might refer to the logical notion of a transaction as related to a persistence context.
-
It might refer to the application notion of a Unit-of-Work, as defined by the archetypal pattern.
This documentation largely treats the physical and logic notions of a transaction as one-in-the-same. |
8.1. Physical Transactions
Hibernate uses the JDBC API for persistence. In the world of Java, there are two well-defined mechanisms for dealing with transactions in JDBC: JDBC itself and JTA. Hibernate supports both mechanisms for integrating with transactions and allowing applications to manage physical transactions.
The transaction handling per Session
is handled by the org.hibernate.resource.transaction.spi.TransactionCoordinator
contract,
which are built by the org.hibernate.resource.transaction.spi.TransactionCoordinatorBuilder
service.
TransactionCoordinatorBuilder
represents a strategy for dealing with transactions whereas TransactionCoordinator represents one instance of that strategy related to a Session.
Which TransactionCoordinatorBuilder
implementation to use is defined by the hibernate.transaction.coordinator_class
setting.
jdbc
(the default for non-Jakarta Persistence applications)-
Manages transactions via calls to
java.sql.Connection
jta
-
Manages transactions via JTA. See Java EE bootstrapping
If a Jakarta Persistence application does not provide a setting for hibernate.transaction.coordinator_class
, Hibernate will
automatically build the proper transaction coordinator based on the transaction type for the persistence unit.
If a non-Jakarta Persistence application does not provide a setting for hibernate.transaction.coordinator_class
, Hibernate
will use jdbc
as the default. This default will cause problems if the application actually uses JTA-based transactions.
A non-Jakarta Persistence application that uses JTA-based transactions should explicitly set hibernate.transaction.coordinator_class=jta
or provide a custom org.hibernate.resource.transaction.TransactionCoordinatorBuilder
that builds a
org.hibernate.resource.transaction.TransactionCoordinator
that properly coordinates with JTA-based transactions.
For details on implementing a custom |
Hibernate uses JDBC connections and JTA resources directly, without adding any additional locking behavior.
Hibernate does not lock objects in memory.
The behavior defined by the isolation level of your database transactions does not change when you use Hibernate.
The Hibernate Session
acts as a transaction-scoped cache providing repeatable reads for lookup by identifier and queries that result in loading entities.
To reduce lock contention in the database, the physical database transaction needs to be as short as possible. Long-running database transactions prevent your application from scaling to a highly-concurrent load. Do not hold a database transaction open during end-user-level work, but open it after the end-user-level work is finished. This concept is referred to as |
8.2. JTA configuration
Interaction with a JTA system is consolidated behind a single contract named org.hibernate.engine.transaction.jta.platform.spi.JtaPlatform
which exposes access to the javax.transaction.TransactionManager
and javax.transaction.UserTransaction
for that system as well as exposing the ability to register javax.transaction.Synchronization
instances, check transaction status, etc.
Generally, |
Hibernate tries to discover the JtaPlatform
it should use through the use of another service named org.hibernate.engine.transaction.jta.platform.spi.JtaPlatformResolver
.
If that resolution does not work, or if you wish to provide a custom implementation you will need to specify the hibernate.transaction.jta.platform
setting.
Hibernate provides many implementations of the JtaPlatform
contract, all with short names:
Atomikos
-
JtaPlatform
for Atomikos. Borland
-
JtaPlatform
for the Borland Enterprise Server. Bitronix
-
JtaPlatform
for Bitronix. JBossAS
-
JtaPlatform
for Arjuna/JBossTransactions/Narayana when used within the JBoss/WildFly Application Server. JBossTS
-
JtaPlatform
for Arjuna/JBossTransactions/Narayana when used standalone. JOnAS
-
JtaPlatform
for JOTM when used within JOnAS. JOTM
-
JtaPlatform
for JOTM when used standalone. JRun4
-
JtaPlatform
for the JRun 4 Application Server. OC4J
-
JtaPlatform
for Oracle’s OC4J container. Orion
-
JtaPlatform
for the Orion Application Server. Resin
-
JtaPlatform
for the Resin Application Server. SapNetWeaver
-
JtaPlatform
for the SAP NetWeaver Application Server. SunOne
-
JtaPlatform
for the SunOne Application Server. Weblogic
-
JtaPlatform
for the Weblogic Application Server. WebSphere
-
JtaPlatform
for older versions of the WebSphere Application Server. WebSphereExtended
-
JtaPlatform
for newer versions of the WebSphere Application Server.
8.3. Hibernate Transaction API
Hibernate provides an API for helping to isolate applications from the differences in the underlying physical transaction system in use.
Based on the configured TransactionCoordinatorBuilder
, Hibernate will simply do the right thing when this transaction API is used by the application.
This allows your applications and components to be more portable to move around into different environments.
To use this API, you would obtain the org.hibernate.Transaction
from the Session. Transaction
allows for all the normal operations you’d expect: begin
, commit
and rollback
, and it even exposes some cool methods like:
markRollbackOnly
-
that works in both JTA and JDBC.
getTimeout
andsetTimeout
-
that again work in both JTA and JDBC.
registerSynchronization
-
that allows you to register JTA Synchronizations even in non-JTA environments. In fact, in both JTA and JDBC environments, these
Synchronizations
are kept locally by Hibernate. In JTA environments, Hibernate will only ever register one singleSynchronization
with theTransactionManager
to avoid ordering problems.
Additionally, it exposes a getStatus method that returns an org.hibernate.resource.transaction.spi.TransactionStatus
enum.
This method checks with the underlying transaction system if needed, so care should be taken to minimize its use; it can have a big performance impact in certain JTA setups.
Let’s take a look at using the Transaction API in the various environments.
StandardServiceRegistry serviceRegistry = new StandardServiceRegistryBuilder()
// "jdbc" is the default, but for explicitness
.applySetting(AvailableSettings.TRANSACTION_COORDINATOR_STRATEGY, "jdbc")
.build();
Metadata metadata = new MetadataSources(serviceRegistry)
.addAnnotatedClass(Customer.class)
.getMetadataBuilder()
.build();
SessionFactory sessionFactory = metadata.getSessionFactoryBuilder()
.build();
Session session = sessionFactory.openSession();
try {
// calls Connection#setAutoCommit(false) to
// signal start of transaction
session.getTransaction().begin();
session.createMutationQuery("UPDATE customer set NAME = 'Sir. '||NAME")
.executeUpdate();
// calls Connection#commit(), if an error
// happens we attempt a rollback
session.getTransaction().commit();
}
catch (Exception e) {
// we may need to rollback depending on
// where the exception happened
if (session.getTransaction().getStatus() == TransactionStatus.ACTIVE
|| session.getTransaction().getStatus() == TransactionStatus.MARKED_ROLLBACK) {
session.getTransaction().rollback();
}
// handle the underlying error
}
finally {
session.close();
sessionFactory.close();
}
StandardServiceRegistry serviceRegistry = new StandardServiceRegistryBuilder()
// "jdbc" is the default, but for explicitness
.applySetting(AvailableSettings.TRANSACTION_COORDINATOR_STRATEGY, "jta")
.build();
Metadata metadata = new MetadataSources(serviceRegistry)
.addAnnotatedClass(Customer.class)
.getMetadataBuilder()
.build();
SessionFactory sessionFactory = metadata.getSessionFactoryBuilder()
.build();
// Note: depending on the JtaPlatform used and some optional settings,
// the underlying transactions here will be controlled through either
// the JTA TransactionManager or UserTransaction
Session session = sessionFactory.openSession();
try {
// Since we are in CMT, a JTA transaction would
// already have been started. This call essentially
// no-ops
session.getTransaction().begin();
Number customerCount = (Number) session.createSelectionQuery("select count(c) from Customer c").uniqueResult();
// Since we did not start the transaction (CMT),
// we also will not end it. This call essentially
// no-ops in terms of transaction handling.
session.getTransaction().commit();
}
catch (Exception e) {
// again, the rollback call here would no-op (aside from
// marking the underlying CMT transaction for rollback only).
if (session.getTransaction().getStatus() == TransactionStatus.ACTIVE
|| session.getTransaction().getStatus() == TransactionStatus.MARKED_ROLLBACK) {
session.getTransaction().rollback();
}
// handle the underlying error
}
finally {
session.close();
sessionFactory.close();
}
StandardServiceRegistry serviceRegistry = new StandardServiceRegistryBuilder()
// "jdbc" is the default, but for explicitness
.applySetting(AvailableSettings.TRANSACTION_COORDINATOR_STRATEGY, "jta")
.build();
Metadata metadata = new MetadataSources(serviceRegistry)
.addAnnotatedClass(Customer.class)
.getMetadataBuilder()
.build();
SessionFactory sessionFactory = metadata.getSessionFactoryBuilder()
.build();
// Note: depending on the JtaPlatform used and some optional settings,
// the underlying transactions here will be controlled through either
// the JTA TransactionManager or UserTransaction
Session session = sessionFactory.openSession();
try {
// Assuming a JTA transaction is not already active,
// this call the TM/UT begin method. If a JTA
// transaction is already active, we remember that
// the Transaction associated with the Session did
// not "initiate" the JTA transaction and will later
// nop-op the commit and rollback calls...
session.getTransaction().begin();
session.persist(new Customer());
Customer customer = (Customer) session.createSelectionQuery("select c from Customer c").uniqueResult();
// calls TM/UT commit method, assuming we are initiator.
session.getTransaction().commit();
}
catch (Exception e) {
// we may need to rollback depending on
// where the exception happened
if (session.getTransaction().getStatus() == TransactionStatus.ACTIVE
|| session.getTransaction().getStatus() == TransactionStatus.MARKED_ROLLBACK) {
// calls TM/UT commit method, assuming we are initiator;
// otherwise marks the JTA transaction for rollback only
session.getTransaction().rollback();
}
// handle the underlying error
}
finally {
session.close();
sessionFactory.close();
}
In the CMT case, we really could have omitted all of the Transaction calls. But the point of the examples was to show that the Transaction API really does insulate your code from the underlying transaction mechanism. In fact, if you strip away the comments and the single configuration setting supplied at bootstrap, the code is exactly the same in all 3 examples. In other words, we could develop that code and drop it, as-is, in any of the 3 transaction environments.
The Transaction API tries hard to make the experience consistent across all environments. To that end, it generally defers to the JTA specification when there are differences (for example automatically trying rollback on a failed commit).
8.4. Contextual sessions
Most applications using Hibernate need some form of contextual session, where a given session is in effect throughout the scope of a given context.
However, across applications the definition of what constitutes a context is typically different; different contexts define different scopes to the notion of current.
Applications using Hibernate prior to version 3.0 tended to utilize either home-grown ThreadLocal
-based contextual sessions, helper classes such as HibernateUtil
, or utilized third-party frameworks, such as Spring or Pico, which provided proxy/interception-based contextual sessions.
Starting with version 3.0.1, Hibernate added the SessionFactory.getCurrentSession()
method.
Initially, this assumed usage of JTA
transactions, where the JTA
transaction defined both the scope and context of a current session.
Given the maturity of the numerous stand-alone JTA TransactionManager
implementations, most, if not all, applications should be using JTA
transaction management, whether or not they are deployed into a J2EE
container.
Based on that, the JTA
-based contextual sessions are all you need to use.
However, as of version 3.1, the processing behind SessionFactory.getCurrentSession()
is now pluggable.
To that end, a new extension interface, org.hibernate.context.spi.CurrentSessionContext
,
and a new configuration parameter, hibernate.current_session_context_class
, have been added to allow pluggability of the scope and context of defining current sessions.
See the Javadocs for the org.hibernate.context.spi.CurrentSessionContext
interface for a detailed discussion of its contract.
It defines a single method, currentSession()
, by which the implementation is responsible for tracking the current contextual session.
Out-of-the-box, Hibernate comes with three implementations of this interface:
org.hibernate.context.internal.JTASessionContext
-
current sessions are tracked and scoped by a
JTA
transaction. The processing here is exactly the same as in the older JTA-only approach. org.hibernate.context.internal.ThreadLocalSessionContext
-
current sessions are tracked by thread of execution. See the Javadocs for more details.
org.hibernate.context.internal.ManagedSessionContext
-
current sessions are tracked by thread of execution. However, you are responsible to bind and unbind a
Session
instance with static methods on this class; it does not open, flush, or close aSession
.
Typically, the value of this parameter would just name the implementation class to use. For the three out-of-the-box implementations, however, there are three corresponding short names: jta, thread, and managed.
The first two implementations provide a one session - one database transaction programming model.
This is also known and used as session-per-request.
The beginning and end of a Hibernate session is defined by the duration of a database transaction.
If you use programmatic transaction demarcation in plain Java SE without JTA, you are advised to use the Hibernate Transaction
API to hide the underlying transaction system from your code.
If you use JTA, you can utilize the JTA interfaces to demarcate transactions.
If you execute in an EJB container that supports CMT, transaction boundaries are defined declaratively and you do not need any transaction or session demarcation operations in your code.
The hibernate.current_session_context_class
configuration parameter defines which org.hibernate.context.spi.CurrentSessionContext
implementation should be used.
For backward compatibility, if this configuration parameter is not set but a org.hibernate.engine.transaction.jta.platform.spi.JtaPlatform
is configured, Hibernate will use the org.hibernate.context.internal.JTASessionContext
.
8.5. Transactional patterns (and anti-patterns)
8.5.1. Session-per-operation anti-pattern
This is an anti-pattern of opening and closing a Session
for each database call in a single thread.
It is also an anti-pattern in terms of database transactions.
Group your database calls into a planned sequence.
In the same way, do not auto-commit after every SQL statement in your application.
Hibernate disables or expects the application server to disable, auto-commit mode immediately.
Database transactions are never optional.
All communication with a database must be encapsulated by a transaction.
Avoid auto-commit behavior for reading data because many small transactions are unlikely to perform better than one clearly-defined unit of work, and are more difficult to maintain and extend.
Using auto-commit does not circumvent database transactions. Instead, when in auto-commit mode, JDBC drivers simply perform each call in an implicit transaction call. It is as if your application called commit after each and every JDBC call. |
8.5.2. Session-per-request pattern
This is the most common transaction pattern. The term request here relates to the concept of a system that reacts to a series of requests from a client/user. Web applications are a prime example of this type of system, though certainly not the only one. At the beginning of handling such a request, the application opens a Hibernate Session, starts a transaction, performs all data related work, ends the transaction and closes the Session. The crux of the pattern is the one-to-one relationship between the transaction and the Session.
Within this pattern, there is a common technique of defining a current session to simplify the need of passing this Session
around to all the application components that may need access to it.
Hibernate provides support for this technique through the getCurrentSession
method of the SessionFactory
.
The concept of a current session has to have a scope that defines the bounds in which the notion of current is valid.
This is the purpose of the org.hibernate.context.spi.CurrentSessionContext
contract.
There are 2 reliable defining scopes:
-
First is a JTA transaction because it allows a callback hook to know when it is ending, which gives Hibernate a chance to close the
Session
and clean up. This is represented by theorg.hibernate.context.internal.JTASessionContext
implementation of theorg.hibernate.context.spi.CurrentSessionContext
contract. Using this implementation, aSession
will be opened the first timegetCurrentSession
is called within that transaction. -
Secondly is this application request cycle itself. This is best represented with the
org.hibernate.context.internal.ManagedSessionContext
implementation of theorg.hibernate.context.spi.CurrentSessionContext
contract. Here an external component is responsible for managing the lifecycle and scoping of a current session. At the start of such a scope,ManagedSessionContext#bind()
method is called passing in theSession
. In the end, itsunbind()
method is called. Some common examples of such external components include:-
javax.servlet.Filter
implementation -
AOP interceptor with a pointcut on the service methods
-
A proxy/interception container
-
The |
8.5.3. Conversations (application-level transactions)
The session-per-request pattern is not the only valid way of designing units of work. Many business processes require a whole series of interactions with the user that are interleaved with database accesses. In web and enterprise applications, it is not acceptable for a database transaction to span a user interaction. Consider the following example:
The first screen of a dialog opens.
The data seen by the user is loaded in a particular Session
and database transaction.
The user is free to modify the objects.
The user uses a UI element to save their work after five minutes of editing. The modifications are made persistent. The user also expects to have exclusive access to the data during the edit session.
Even though we have multiple databases access here, from the point of view of the user, this series of steps represents a single unit of work. There are many ways to implement this in your application.
A first naive implementation might keep the Session
and database transaction open while the user is editing, using database-level locks to prevent other users from modifying the same data and to guarantee isolation and atomicity.
This is an anti-pattern because lock contention is a bottleneck which will prevent scalability in the future.
Several database transactions are used to implement the conversation. In this case, maintaining isolation of business processes becomes the partial responsibility of the application tier. A single conversation usually spans several database transactions. These multiple database accesses can only be atomic as a whole if only one of these database transactions (typically the last one) stores the updated data. All others only read data. A common way to receive this data is through a wizard-style dialog spanning several request/response cycles. Hibernate includes some features which make this easy to implement.
Automatic Versioning |
Hibernate can perform automatic optimistic concurrency control for you. It can automatically detect (at the end of the conversation) if a concurrent modification occurred during user think time. |
Detached Objects |
If you decide to use the session-per-request pattern, all loaded instances will be in the detached state during user think time. Hibernate allows you to reattach the objects and persist the modifications. The pattern is called session-per-request-with-detached-objects. Automatic versioning is used to isolate concurrent modifications. |
Extended |
The Hibernate |
Session-per-request-with-detached-objects and session-per-conversation each have advantages and disadvantages.
8.5.4. Session-per-application anti-pattern
The session-per-application is also considered an anti-pattern.
The Hibernate Session
, like the Jakarta Persistence EntityManager
, is not a thread-safe object and it is intended to be confined to a single thread at once.
If the Session
is shared among multiple threads, there will be race conditions as well as visibility issues, so beware of this.
An exception thrown by Hibernate means you have to rollback your database transaction and close the Session
immediately.
If your Session
is bound to the application, you have to stop the application.
Rolling back the database transaction does not put your business objects back into the state they were at the start of the transaction.
This means that the database state and the business objects will be out of sync.
Usually, this is not a problem because exceptions are not recoverable and you will have to start over after rollback anyway.
For more details, check out the exception handling section in Persistence Context chapter.
The Session
caches every object that is in a persistent state (watched and checked for dirty state by Hibernate).
If you keep it open for a long time or simply load too much data, it will grow endlessly until you get an OutOfMemoryException
.
One solution is to call clear()
and evict()
to manage the Session
cache, but you should consider a Stored Procedure if you need mass data operations.
Some solutions are shown in the Batching chapter.
Keeping a Session
open for the duration of a user session also means a higher probability of stale data.
9. JNDI
Hibernate does optionally interact with JNDI on the application’s behalf. Generally, it does this when the application:
-
has asked the SessionFactory be bound to JNDI
-
has specified a DataSource to use by JNDI name
-
is using JTA transactions and the
JtaPlatform
needs to do JNDI lookups forTransactionManager
,UserTransaction
, etc
All of these JNDI calls route through a single service whose role is org.hibernate.engine.jndi.spi.JndiService
.
The standard JndiService
accepts a number of configuration settings:
hibernate.jndi.class
-
names the
javax.naming.InitialContext
implementation class to use. Seejavax.naming.Context#INITIAL_CONTEXT_FACTORY
hibernate.jndi.url
-
names the JNDI InitialContext connection url. See
javax.naming.Context.PROVIDER_URL
Any other settings prefixed with hibernate.jndi.
will be collected and passed along to the JNDI provider.
The standard |
10. Locking
In a relational database, locking refers to actions taken to prevent data from changing between the time it is read and the time is used.
Your locking strategy can be either optimistic or pessimistic.
- Optimistic
-
Optimistic locking assumes that multiple transactions can complete without affecting each other, and that therefore transactions can proceed without locking the data resources that they affect. Before committing, each transaction verifies that no other transaction has modified its data. If the check reveals conflicting modifications, the committing transaction rolls back.
- Pessimistic
-
Pessimistic locking assumes that concurrent transactions will conflict with each other, and requires resources to be locked after they are read and only unlocked after the application has finished using the data.
Hibernate provides mechanisms for implementing both types of locking in your applications.
10.1. Optimistic
When your application uses long transactions or conversations that span several database transactions, you can store versioning data so that if the same entity is updated by two conversations, the last to commit changes is informed of the conflict, and does not override the other conversation’s work. This approach guarantees some isolation, but scales well and works particularly well in read-often-write-sometimes situations.
Hibernate provides two different mechanisms for storing versioning information, a dedicated version number or a timestamp.
A version or timestamp property can never be null for a detached instance. Hibernate detects any instance with a null version or timestamp as transient, regardless of other unsaved-value strategies that you specify. Declaring a nullable version or timestamp property is an easy way to avoid problems with transitive reattachment in Hibernate, especially useful if you use assigned identifiers or composite keys. |
10.1.1. Mapping optimistic locking
Jakarta Persistence defines support for optimistic locking based on either a version (sequential numeric) or timestamp strategy.
To enable this style of optimistic locking simply add the jakarta.persistence.Version
to the persistent attribute that defines the optimistic locking value.
According to Jakarta Persistence, the valid types for these attributes are limited to:
-
int
orInteger
-
short
orShort
-
long
orLong
-
java.sql.Timestamp
However, Hibernate allows you to use even Java 8 Date/Time types, such as Instant
.
@Version
annotation mapping@Entity(name = "Person")
public static class Person {
@Id
@GeneratedValue
private Long id;
@Column(name = "`name`")
private String name;
@Version
private long version;
//Getters and setters are omitted for brevity
}
@Entity(name = "Person")
public static class Person {
@Id
@GeneratedValue
private Long id;
@Column(name = "`name`")
private String name;
@Version
private Timestamp version;
//Getters and setters are omitted for brevity
}
@Entity(name = "Person")
public static class Person {
@Id
@GeneratedValue
private Long id;
@Column(name = "`name`")
private String name;
@Version
private Instant version;
//Getters and setters are omitted for brevity
}
Dedicated version number
The version number mechanism for optimistic locking is provided through a @Version
annotation.
@Version
private long version;
Here, the version property is mapped to the version
column, and the entity manager uses it to detect conflicting updates,
and prevent the loss of updates that would otherwise be overwritten by a last-commit-wins strategy.
The version column can be any kind of type, as long as you define and implement the appropriate UserVersionType
.
Your application is forbidden from altering the version number set by Hibernate.
To artificially increase the version number, see the documentation for properties LockModeType.OPTIMISTIC_FORCE_INCREMENT
or
LockModeType.PESSIMISTIC_FORCE_INCREMENT
check in the Hibernate Entity Manager reference documentation.
If the version number is generated by the database, such as a trigger, use the annotation |
Timestamp
Timestamps are a less reliable way of optimistic locking than version numbers but can be used by applications for other purposes as well.
Timestamping is automatically used if you the @Version
annotation on a Date
or Calendar
property type.
@Version
private Date version;
The timestamp can also be generated by the database, instead of by the VM, using the @CurrentTimestamp
annotation, or even @Generated(value = ALWAYS, sql = "current_timestamp")
.
@Entity(name = "Person")
public static class Person {
@Id
private Long id;
private String firstName;
private String lastName;
@Version @CurrentTimestamp
private LocalDateTime version;
Now, when persisting a Person
entity, Hibernate calls the database-specific current timestamp retrieval function:
Person person = new Person();
person.setId(1L);
person.setFirstName("John");
person.setLastName("Doe");
assertNull(person.getVersion());
entityManager.persist(person);
assertNotNull(person.getVersion());
CALL current_timestamp()
INSERT INTO
Person
(firstName, lastName, version, id)
VALUES
(?, ?, ?, ?)
-- binding parameter [1] as [VARCHAR] - [John]
-- binding parameter [2] as [VARCHAR] - [Doe]
-- binding parameter [3] as [TIMESTAMP] - [2017-05-18 12:03:03.808]
-- binding parameter [4] as [BIGINT] - [1]
Excluding attributes
By default, every entity attribute modification is going to trigger a version incrementation.
If there is an entity property which should not bump up the entity version,
then you need to annotate it with the Hibernate @OptimisticLock
annotation,
as illustrated in the following example.
@Entity(name = "Phone")
public static class Phone {
@Id
private Long id;
@Column(name = "`number`")
private String number;
@OptimisticLock(excluded = true)
private long callCount;
@Version
private Long version;
//Getters and setters are omitted for brevity
public void incrementCallCount() {
this.callCount++;
}
}
This way, if one thread modifies the Phone
number while a second thread increments the callCount
attribute,
the two concurrent transactions are not going to conflict as illustrated by the following example.
doInJPA(this::entityManagerFactory, entityManager -> {
Phone phone = entityManager.find(Phone.class, 1L);
phone.setNumber("+123-456-7890");
doInJPA(this::entityManagerFactory, _entityManager -> {
Phone _phone = _entityManager.find(Phone.class, 1L);
_phone.incrementCallCount();
log.info("Bob changes the Phone call count");
});
log.info("Alice changes the Phone number");
});
-- Bob changes the Phone call count
update
Phone
set
callCount = 1,
"number" = '123-456-7890',
version = 0
where
id = 1
and version = 0
-- Alice changes the Phone number
update
Phone
set
callCount = 0,
"number" = '+123-456-7890',
version = 1
where
id = 1
and version = 0
When Bob changes the Phone
entity callCount
, the entity version is not bumped up.
That’s why Alice’s UPDATE succeeds since the entity version is still 0, even if Bob has changed the record
since Alice loaded it.
Although there is no conflict between Bob and Alice, Alice’s UPDATE overrides Bob’s change to the For this reason, you should only use this feature if you can accommodate lost updates on the excluded entity properties. |
Versionless optimistic locking
Although the default @Version
property optimistic locking mechanism is sufficient in many situations,
sometimes, you need rely on the actual database row column values to prevent lost updates.
Hibernate supports a form of optimistic locking that does not require a dedicated "version attribute". This is also useful for use with modeling legacy schemas.
The idea is that you can get Hibernate to perform "version checks" using either all of the entity’s attributes or just the attributes that have changed.
This is achieved through the use of the
@OptimisticLocking
annotation which defines a single attribute of type
org.hibernate.annotations.OptimisticLockType
.
There are 4 available OptimisticLockTypes:
NONE
-
optimistic locking is disabled even if there is a
@Version
annotation present VERSION
(the default)-
performs optimistic locking based on a
@Version
as described above ALL
-
performs optimistic locking based on all fields as part of an expanded WHERE clause restriction for the UPDATE/DELETE SQL statements
DIRTY
-
performs optimistic locking based on dirty fields as part of an expanded WHERE clause restriction for the UPDATE/DELETE SQL statements
Versionless optimistic locking using OptimisticLockType.ALL
OptimisticLockType.ALL
mapping example@Entity(name = "Person")
@OptimisticLocking(type = OptimisticLockType.ALL)
@DynamicUpdate
public static class Person {
@Id
private Long id;
@Column(name = "`name`")
private String name;
private String country;
private String city;
@Column(name = "created_on")
private Timestamp createdOn;
//Getters and setters are omitted for brevity
}
When you need to modify the Person
entity above:
OptimisticLockType.ALL
update examplePerson person = entityManager.find(Person.class, 1L);
person.setCity("Washington D.C.");
UPDATE
Person
SET
city=?
WHERE
id=?
AND city=?
AND country=?
AND created_on=?
AND "name"=?
-- binding parameter [1] as [VARCHAR] - [Washington D.C.]
-- binding parameter [2] as [BIGINT] - [1]
-- binding parameter [3] as [VARCHAR] - [New York]
-- binding parameter [4] as [VARCHAR] - [US]
-- binding parameter [5] as [TIMESTAMP] - [2016-11-16 16:05:12.876]
-- binding parameter [6] as [VARCHAR] - [John Doe]
As you can see, all the columns of the associated database row are used in the WHERE
clause.
If any column has changed after the row was loaded, there won’t be any match, and a StaleStateException
or an OptimisticEntityLockException
is going to be thrown.
When using |
Versionless optimistic locking using OptimisticLockType.DIRTY
The OptimisticLockType.DIRTY
differs from OptimisticLockType.ALL
in that it only takes into consideration the entity properties that have changed
since the entity was loaded in the currently running Persistence Context.
OptimisticLockType.DIRTY
mapping example@Entity(name = "Person")
@OptimisticLocking(type = OptimisticLockType.DIRTY)
@DynamicUpdate
public static class Person {
@Id
private Long id;
@Column(name = "`name`")
private String name;
private String country;
private String city;
@Column(name = "created_on")
private Timestamp createdOn;
//Getters and setters are omitted for brevity
}
When you need to modify the Person
entity above:
OptimisticLockType.DIRTY
update examplePerson person = entityManager.find(Person.class, 1L);
person.setCity("Washington D.C.");
UPDATE
Person
SET
city=?
WHERE
id=?
and city=?
-- binding parameter [1] as [VARCHAR] - [Washington D.C.]
-- binding parameter [2] as [BIGINT] - [1]
-- binding parameter [3] as [VARCHAR] - [New York]
This time, only the database column that has changed was used in the WHERE
clause.
The main advantage of When using |
10.2. Pessimistic
Typically, you only need to specify an isolation level for the JDBC connections and let the database handle locking issues. If you do need to obtain exclusive pessimistic locks or re-obtain locks at the start of a new transaction, Hibernate gives you the tools you need.
Hibernate always uses the locking mechanism of the database, and never lock objects in memory. |
10.3. LockMode
and LockModeType
Long before Java Persistence 1.0, Hibernate already defined various explicit locking strategies through its LockMode
enumeration.
Jakarta Persistence comes with its own LockModeType
enumeration which defines similar strategies as the Hibernate-native LockMode
.
LockModeType |
LockMode |
Description |
---|---|---|
|
|
The absence of a lock. All objects switch to this lock mode at the end of a Transaction. Objects associated with the session via a call to |
|
|
The entity version is checked towards the end of the currently running transaction. |
|
|
The entity version is incremented automatically even if the entity has not changed. |
|
|
The entity is locked pessimistically and its version is incremented automatically even if the entity has not changed. |
|
|
The entity is locked pessimistically using a shared lock if the database supports such a feature. Otherwise, an explicit lock is used. |
|
|
The entity is locked using an explicit lock. |
|
|
The lock acquisition request fails fast if the row s already locked. |
|
|
The lock acquisition request skips the already locked rows. It uses a |
The explicit user request mentioned above occurs as a consequence of any of the following actions:
-
a call to
Session.load()
, specifying aLockMode
. -
a call to
Session.lock()
. -
a call to
Query.setLockMode()
.
If you call Session.load()
with option UPGRADE
, UPGRADE_NOWAIT
or UPGRADE_SKIPLOCKED
,
and the requested object is not already loaded by the session, the object is loaded using SELECT … FOR UPDATE
.
If you call load()
for an object that is already loaded with a less restrictive lock than the one you request, Hibernate calls lock()
for that object.
Session.lock(
) performs a version number check if the specified lock mode is READ
, UPGRADE
, UPGRADE_NOWAIT
or UPGRADE_SKIPLOCKED
.
In the case of UPGRADE
, UPGRADE_NOWAIT
or UPGRADE_SKIPLOCKED
, the SELECT … FOR UPDATE
syntax is used.
If the requested lock mode is not supported by the database, Hibernate uses an appropriate alternate mode instead of throwing an exception. This ensures that applications are portable.
10.4. Jakarta Persistence locking query hints
Jakarta Persistence defined two locking-related query hints:
- jakarta.persistence.lock.timeout
-
it gives the number of milliseconds a lock acquisition request will wait before throwing an exception
- jakarta.persistence.lock.scope
-
defines the scope of the lock acquisition request. The scope can either be
NORMAL
(default value) orEXTENDED
. TheEXTENDED
scope will cause a lock acquisition request to be passed to other owned table structured (e.g.@Inheritance(strategy=InheritanceType.JOINED)
,@ElementCollection
)
jakarta.persistence.lock.timeout
exampleentityManager.find(
Person.class, id, LockModeType.PESSIMISTIC_WRITE,
Collections.singletonMap("jakarta.persistence.lock.timeout", 200)
);
SELECT explicitlo0_.id AS id1_0_0_,
explicitlo0_."name" AS name2_0_0_
FROM person explicitlo0_
WHERE explicitlo0_.id = 1
FOR UPDATE wait 2
Not all JDBC database drivers support setting a timeout value for a locking request. If not supported, the Hibernate dialect ignores this query hint. |
The |
10.5. Session.lock()
The following example shows how to obtain a shared database lock.
session.lock()
examplePerson person = entityManager.find(Person.class, id);
Session session = entityManager.unwrap(Session.class);
LockOptions lockOptions = new LockOptions(LockMode.PESSIMISTIC_READ, LockOptions.NO_WAIT);
session.lock(person, lockOptions);
SELECT p1_0.id,
p1_0."name"
FROM Person p1_0
WHERE p1_0.id = 1
SELECT id
FROM Person
WHERE id = 1
FOR UPDATE
10.6. Follow-on-locking
When using Oracle, the FOR UPDATE
exclusive locking clause cannot be used with:
-
DISTINCT
-
GROUP BY
-
UNION
-
inlined views (derived tables), therefore, affecting the legacy Oracle pagination mechanism as well.
For this reason, Hibernate uses secondary selects to lock the previously fetched entities.
List<Person> persons = entityManager.createQuery(
"select DISTINCT p from Person p", Person.class)
.setLockMode(LockModeType.PESSIMISTIC_WRITE)
.getResultList();
SELECT DISTINCT p.id as id1_0_, p."name" as name2_0_
FROM Person p
SELECT id
FROM Person
WHERE id = 1 FOR UPDATE
SELECT id
FROM Person
WHERE id = 1 FOR UPDATE
To avoid the N+1 query problem, a separate query can be used to apply the lock using the associated entity identifiers. |
List<Person> persons = entityManager.createQuery(
"select DISTINCT p from Person p", Person.class)
.getResultList();
entityManager.createQuery(
"select p.id from Person p where p in :persons")
.setLockMode(LockModeType.PESSIMISTIC_WRITE)
.setParameter("persons", persons)
.getResultList();
SELECT DISTINCT p.id as id1_0_, p."name" as name2_0_
FROM Person p
SELECT p.id as col_0_0_
FROM Person p
WHERE p.id IN ( 1 , 2 )
FOR UPDATE
The lock request was moved from the original query to a secondary one which takes the previously fetched entities to lock their associated database records.
Prior to Hibernate 5.2.1, the follow-on-locking mechanism was applied uniformly to any locking query executing on Oracle. Since 5.2.1, the Oracle Dialect tries to figure out if the current query demands the follow-on-locking mechanism.
Even more important is that you can overrule the default follow-on-locking detection logic and explicitly enable or disable it on a per query basis.
List<Person> persons = entityManager.createQuery(
"select p from Person p", Person.class)
.setMaxResults(10)
.unwrap(Query.class)
.setLockOptions(
new LockOptions(LockMode.PESSIMISTIC_WRITE)
.setFollowOnLocking(false))
.getResultList();
SELECT *
FROM (
SELECT p.id as id1_0_, p."name" as name2_0_
FROM Person p
)
WHERE rownum <= 10
FOR UPDATE
The follow-on-locking mechanism should be explicitly enabled only if the currently executing query fails because the |
11. Fetching
Fetching, essentially, is the process of grabbing data from the database and making it available to the application. Tuning how an application does fetching is one of the biggest factors in determining how an application will perform. Fetching too much data, in terms of width (values/columns) and/or depth (results/rows), adds unnecessary overhead in terms of both JDBC communication and ResultSet processing. Fetching too little data might cause additional fetching to be needed. Tuning how an application fetches data presents a great opportunity to influence the overall application performance.
11.1. The basics
The concept of fetching breaks down into two different questions.
-
When should the data be fetched? Now? Later?
-
How should the data be fetched?
"Now" is generally termed eager or immediate while "later" is generally termed lazy or delayed. |
There are a number of scopes for defining fetching:
- static
-
Static definition of fetching strategies is done in the mappings. The statically-defined fetch strategies are used in the absence of any dynamically defined strategies.
- SELECT
-
Performs a separate SQL select to load the data. This can either be EAGER (the second select is issued immediately) or LAZY (the second select is delayed until the data is needed). This is the strategy generally termed N+1.
- JOIN
-
Inherently an EAGER style of fetching. The data to be fetched is obtained through the use of an SQL outer join.
- BATCH
-
Performs a separate SQL select to load a number of related data items using an IN-restriction as part of the SQL WHERE-clause based on a batch size. Again, this can either be EAGER (the second select is issued immediately) or LAZY (the second select is delayed until the data is needed).
- SUBSELECT
-
Performs a separate SQL select to load associated data based on the SQL restriction used to load the owner. Again, this can either be EAGER (the second select is issued immediately) or LAZY (the second select is delayed until the data is needed).
- dynamic (sometimes referred to as runtime)
-
The dynamic definition is really use-case centric. There are multiple ways to define dynamic fetching:
- fetch profiles
-
defined in mappings, but can be enabled/disabled on the
Session
. - HQL / JPQL
-
both Hibernate and Jakarta Persistence Criteria queries have the ability to specify fetching, specific to said query.
- entity graphs
-
using Jakarta Persistence EntityGraphs
|
11.2. Direct fetching vs. entity queries
To see the difference between direct fetching and entity queries in regard to eagerly fetched associations, consider the following entities:
@Entity(name = "Department")
public static class Department {
@Id
private Long id;
//Getters and setters omitted for brevity
}
@Entity(name = "Employee")
public static class Employee {
@Id
private Long id;
@NaturalId
private String username;
@ManyToOne(fetch = FetchType.EAGER)
private Department department;
//Getters and setters omitted for brevity
}
The Employee
entity has a @ManyToOne
association to a Department
which is fetched eagerly.
When issuing a direct entity fetch, Hibernate executed the following SQL query:
Employee employee = entityManager.find(Employee.class, 1L);
select
e.id as id1_1_0_,
e.department_id as departme3_1_0_,
e.username as username2_1_0_,
d.id as id1_0_1_
from
Employee e
left outer join
Department d
on e.department_id=d.id
where
e.id = 1
The LEFT OUTER JOIN
clause is added to the generated SQL query because this association is required to be fetched eagerly.
On the other hand, if you are using an entity query that does not contain a JOIN FETCH
directive to the Department
association:
Employee employee = entityManager.createQuery(
"select e " +
"from Employee e " +
"where e.id = :id", Employee.class)
.setParameter("id", 1L)
.getSingleResult();
select
e.id as id1_1_,
e.department_id as departme3_1_,
e.username as username2_1_
from
Employee e
where
e.id = 1
select
d.id as id1_0_0_
from
Department d
where
d.id = 1
Hibernate uses a secondary select instead. This is because the entity query fetch policy cannot be overridden, so Hibernate requires a secondary select to ensure that the EAGER association is fetched prior to returning the result to the user.
If you forget to JOIN FETCH all EAGER associations, Hibernate is going to issue a secondary select for each and every one of those which, in turn, can lead to N + 1 query issue. For this reason, you should prefer LAZY associations. |
11.3. Applying fetch strategies
Let’s consider these topics as it relates to a sample domain model and a few use cases.
@Entity(name = "Department")
public static class Department {
@Id
private Long id;
@OneToMany(mappedBy = "department")
private List<Employee> employees = new ArrayList<>();
//Getters and setters omitted for brevity
}
@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
}
@Entity(name = "Project")
public class Project {
@Id
private Long id;
@ManyToMany
private List<Employee> employees = new ArrayList<>();
//Getters and setters omitted for brevity
}
The Hibernate recommendation is to statically mark all associations lazy and to use dynamic fetching strategies for eagerness. This is unfortunately at odds with the Jakarta Persistence specification which defines that all one-to-one and many-to-one associations should be eagerly fetched by default. Hibernate, as a Jakarta Persistence provider, honors that default. |
11.4. No fetching
For the first use case, consider the application login process for an Employee
.
Let’s assume that login only requires access to the Employee
information, not Project
nor Department
information.
Employee employee = entityManager.createQuery(
"select e " +
"from Employee e " +
"where " +
" e.username = :username and " +
" e.password = :password",
Employee.class)
.setParameter("username", username)
.setParameter("password", password)
.getSingleResult();
In this example, the application gets the Employee
data.
However, because all associations from Employee
are declared as LAZY (Jakarta Persistence defines the default for collections as LAZY) no other data is fetched.
If the login process does not need access to the Employee
information specifically, another fetching optimization here would be to limit the width of the query results.
Integer accessLevel = entityManager.createQuery(
"select e.accessLevel " +
"from Employee e " +
"where " +
" e.username = :username and " +
" e.password = :password",
Integer.class)
.setParameter("username", username)
.setParameter("password", password)
.getSingleResult();
11.5. Dynamic fetching via queries
For the second use case, consider a screen displaying the Projects
for an Employee
.
Certainly access to the Employee
is needed, as is the collection of Projects
for that Employee. Information about Departments
, other Employees
or other Projects
is not needed.
Employee employee = entityManager.createQuery(
"select e " +
"from Employee e " +
"left join fetch e.projects " +
"where " +
" e.username = :username and " +
" e.password = :password",
Employee.class)
.setParameter("username", username)
.setParameter("password", password)
.getSingleResult();
CriteriaBuilder builder = entityManager.getCriteriaBuilder();
CriteriaQuery<Employee> query = builder.createQuery(Employee.class);
Root<Employee> root = query.from(Employee.class);
root.fetch("projects", JoinType.LEFT);
query.select(root).where(
builder.and(
builder.equal(root.get("username"), username),
builder.equal(root.get("password"), password)
)
);
Employee employee = entityManager.createQuery(query).getSingleResult();
In this example we have an Employee
and their Projects
loaded in a single query shown both as an HQL query and a Jakarta Persistence Criteria query.
In both cases, this resolves to exactly one database query to get all that information.
11.6. Dynamic fetching via Jakarta Persistence entity graph
Jakarta Persistence also supports a feature called EntityGraphs to provide the application developer has more control over fetch plans. It has two modes to choose from:
- fetch graph
-
In this case, all attributes specified in the entity graph will be treated as FetchType.EAGER, and all attributes not specified will ALWAYS be treated as FetchType.LAZY.
- load graph
-
In this case, all attributes specified in the entity graph will be treated as FetchType.EAGER, but attributes not specified use their static mapping specification.
Below is a fetch graph
dynamic fetching example:
@Entity(name = "Employee")
@NamedEntityGraph(name = "employee.projects",
attributeNodes = @NamedAttributeNode("projects")
)
Employee employee = entityManager.find(
Employee.class,
userId,
Collections.singletonMap(
"jakarta.persistence.fetchgraph",
entityManager.getEntityGraph("employee.projects")
)
);
When executing a JPQL query, if an EAGER association is omitted, Hibernate will issue a secondary select for every association needed to be fetched eagerly, which can lead to N+1 query issues. For this reason, it’s better to use LAZY associations, and only fetch them eagerly on a per-query basis. |
An EntityGraph is the root of a "load plan" and must correspond to an EntityType.
11.6.1. Jakarta Persistence (key) subgraphs
A sub-graph is used to control the fetching of sub-attributes of the AttributeNode it is applied to.
It is generally defined via the @NamedSubgraph
annotation.
If we have a Project
parent entity which has an employees
child associations,
and we’d like to fetch the department
for the Employee
child association.
@Entity(name = "Project")
@NamedEntityGraph(name = "project.employees",
attributeNodes = @NamedAttributeNode(
value = "employees",
subgraph = "project.employees.department"
),
subgraphs = @NamedSubgraph(
name = "project.employees.department",
attributeNodes = @NamedAttributeNode("department")
)
)
public static class Project {
@Id
private Long id;
@ManyToMany
private List<Employee> employees = new ArrayList<>();
//Getters and setters omitted for brevity
}
When fetching this entity graph, Hibernate generates the following SQL query:
Project project = doInJPA(this::entityManagerFactory, entityManager -> {
return entityManager.find(
Project.class,
1L,
Collections.singletonMap(
"jakarta.persistence.fetchgraph",
entityManager.getEntityGraph("project.employees")
)
);
});
select
p.id as id1_2_0_, e.id as id1_1_1_, d.id as id1_0_2_,
e.accessLevel as accessLe2_1_1_,
e.department_id as departme5_1_1_,
decrypt( 'AES', '00', e.pswd ) as pswd3_1_1_,
e.username as username4_1_1_,
p_e.projects_id as projects1_3_0__,
p_e.employees_id as employee2_3_0__
from
Project p
inner join
Project_Employee p_e
on p.id=p_e.projects_id
inner join
Employee e
on p_e.employees_id=e.id
inner join
Department d
on e.department_id=d.id
where
p.id = ?
-- binding parameter [1] as [BIGINT] - [1]
Specifying a sub-graph is only valid for an attribute (or its "key") whose type is a ManagedType. So while an EntityGraph must correspond to an EntityType, a Subgraph is legal for any ManagedType. An attribute’s key is defined as either:
-
For a singular attribute, the attribute’s type must be an IdentifiableType and that IdentifiableType must have a composite identifier. The "key sub-graph" is applied to the identifier type. The non-key sub-graph applies to the attribute’s value, which must be a ManagedType.
-
For a plural attribute, the attribute must be a Map and the Map’s key value must be a ManagedType. The "key sub-graph" is applied to the Map’s key type. In this case, the non-key sub-graph applies to the plural attribute’s value/element.
11.6.2. Jakarta Persistence SubGraph sub-typing
SubGraphs can also be sub-type specific. Given an attribute whose value is an inheritance hierarchy, we can refer to attributes of a specific sub-type using the forms of sub-graph definition that accept the sub-type Class.
11.6.3. Creating and applying Jakarta Persistence graphs from text representations
Hibernate allows the creation of Jakarta Persistence fetch/load graphs by parsing a textual representation
of the graph. Generally speaking, the textual representation of a graph is a comma-separated
list of attribute names, optionally including any sub-graph specifications.
org.hibernate.graph.EntityGraphParser
is the starting point for such parsing operations.
Parsing a textual representation of a graph is not (yet) a part of the Jakarta Persistence specification. So the syntax described here is specific to Hibernate. We do hope to eventually make this syntax part of the Jakarta Persistence specification proper. |
final EntityGraph<Project> graph = GraphParser.parse(
Project.class,
"employees(department)",
entityManager
);
This example actually functions exactly as Fetch graph with a subgraph mapping, just using a parsed graph rather than a named graph.
The syntax also supports defining "key sub-graphs". To specify a key sub-graph, .key
is added
to the end of the attribute name.
final EntityGraph<Movie> graph = GraphParser.parse(
Movie.class,
"cast.key(name)",
entityManager
);
final EntityGraph<Ticket> graph = GraphParser.parse(
Ticket.class,
"showing.key(movie(cast))",
entityManager
);
Parsing can also handle sub-type specific sub-graphs. For example, given an entity hierarchy of
LegalEntity
← (Corporation
| Person
| NonProfit
) and an attribute named responsibleParty
whose
type is the LegalEntity
base type we might have:
responsibleParty(Corporation: ceo)
We can even duplicate the attribute names to apply different sub-type sub-graphs:
responsibleParty(taxIdNumber), responsibleParty(Corporation: ceo), responsibleParty(NonProfit: sector)
The duplicated attribute names are handled according to the Jakarta Persistence specification which says that duplicate specification of the attribute node results in the originally registered AttributeNode to be re-used effectively merging the 2 AttributeNode specifications together. In other words, the above specification creates a single AttributeNode with 3 distinct SubGraphs. It is functionally the same as calling:
Class<Invoice> invoiceClass = ...;
jakarta.persistence.EntityGraph<Invoice> invoiceGraph = entityManager.createEntityGraph( invoiceClass );
invoiceGraph.addAttributeNode( "responsibleParty" );
invoiceGraph.addSubgraph( "responsibleParty" ).addAttributeNode( "taxIdNumber" );
invoiceGraph.addSubgraph( "responsibleParty", Corporation.class ).addAttributeNode( "ceo" );
invoiceGraph.addSubgraph( "responsibleParty", NonProfit.class ).addAttributeNode( "sector" );
11.6.4. Combining multiple Jakarta Persistence entity graphs into one
Multiple entity graphs can be combined into a single "super graph" that acts as a union. Graph from the previous example can also be built by combining separate aspect graphs into one, such as:
final EntityGraph<Project> a = GraphParser.parse(
Project.class, "employees(username)", entityManager
);
final EntityGraph<Project> b = GraphParser.parse(
Project.class, "employees(password, accessLevel)", entityManager
);
final EntityGraph<Project> c = GraphParser.parse(
Project.class, "employees(department(employees(username)))", entityManager
);
final EntityGraph<Project> all = EntityGraphs.merge(entityManager, Project.class, a, b, c);
11.7. Dynamic fetching via Hibernate profiles
Suppose we wanted to leverage loading by natural-id to obtain the Employee
information in the "projects for and employee" use-case.
Loading by natural-id uses the statically defined fetching strategies, but does not expose a means to define load-specific fetching.
So we would leverage a fetch profile.
@Entity(name = "Employee")
@FetchProfile(
name = "employee.projects",
fetchOverrides = {
@FetchProfile.FetchOverride(
entity = Employee.class,
association = "projects",
mode = FetchMode.JOIN
)
}
)
session.enableFetchProfile("employee.projects");
Employee employee = session.bySimpleNaturalId(Employee.class).load(username);
Here the Employee
is obtained by natural-id lookup and the Employee’s Project
data is fetched eagerly.
If the Employee
data is resolved from cache, the Project
data is resolved on its own.
However, if the Employee
data is not resolved in cache, the Employee
and Project
data is resolved in one SQL query via join as we saw above.
11.8. Batch fetching
Hibernate offers the @BatchSize
annotation,
which can be used when fetching uninitialized entity proxies.
Considering the following entity mapping:
@BatchSize
mapping example@Entity(name = "Department")
public static class Department {
@Id
private Long id;
@OneToMany(mappedBy = "department")
//@BatchSize(size = 5)
private List<Employee> employees = new ArrayList<>();
//Getters and setters omitted for brevity
}
@Entity(name = "Employee")
public static class Employee {
@Id
private Long id;
@NaturalId
private String name;
@ManyToOne(fetch = FetchType.LAZY)
private Department department;
//Getters and setters omitted for brevity
}
Considering that we have previously fetched several Department
entities,
and now we need to initialize the employees
entity collection for each particular Department
,
the @BatchSize
annotations allows us to load multiple Employee
entities in a single database roundtrip.
@BatchSize
fetching exampleList<Department> departments = entityManager.createQuery(
"select d " +
"from Department d " +
"inner join d.employees e " +
"where e.name like 'John%'", Department.class)
.getResultList();
for (Department department : departments) {
log.infof(
"Department %d has {} employees",
department.getId(),
department.getEmployees().size()
);
}
SELECT
d.id as id1_0_
FROM
Department d
INNER JOIN
Employee employees1_
ON d.id=employees1_.department_id
SELECT
e.department_id as departme3_1_1_,
e.id as id1_1_1_,
e.id as id1_1_0_,
e.department_id as departme3_1_0_,
e.name as name2_1_0_
FROM
Employee e
WHERE
e.department_id IN (
0, 2, 3, 4, 5
)
SELECT
e.department_id as departme3_1_1_,
e.id as id1_1_1_,
e.id as id1_1_0_,
e.department_id as departme3_1_0_,
e.name as name2_1_0_
FROM
Employee e
WHERE
e.department_id IN (
6, 7, 8, 9, 1
)
As you can see in the example above, there are only two SQL statements used to fetch the Employee
entities associated with multiple Department
entities.
Without However, although |
11.9. The @Fetch
annotation mapping
Besides the FetchType.LAZY
or FetchType.EAGER
Jakarta Persistence annotations,
you can also use the Hibernate-specific @Fetch
annotation that accepts one of the following FetchMode
s:
- SELECT
-
The association is going to be fetched using a secondary select for each individual entity, collection, or join load. This mode can be used for either
FetchType.EAGER
orFetchType.LAZY
. - JOIN
-
Use an outer join to load the related entities, collections or joins when using direct fetching. This mode can only be used for
FetchType.EAGER
. - SUBSELECT
-
Available for collections only. When accessing a non-initialized collection, this fetch mode will trigger loading all elements of all collections of the same role for all owners associated with the persistence context using a single secondary select.
11.10. FetchMode.SELECT
To demonstrate how FetchMode.SELECT
works, consider the following entity mapping:
FetchMode.SELECT
mapping example@Entity(name = "Department")
public static class Department {
@Id
private Long id;
@OneToMany(mappedBy = "department", fetch = FetchType.LAZY)
@Fetch(FetchMode.SELECT)
private List<Employee> employees = new ArrayList<>();
//Getters and setters omitted for brevity
}
@Entity(name = "Employee")
public static class Employee {
@Id
@GeneratedValue
private Long id;
@NaturalId
private String username;
@ManyToOne(fetch = FetchType.LAZY)
private Department department;
//Getters and setters omitted for brevity
}
Considering there are multiple Department
entities, each one having multiple Employee
entities,
when executing the following test case, Hibernate fetches every uninitialized Employee
collection using a secondary SELECT
statement upon accessing the child collection for the first time:
FetchMode.SELECT
mapping exampleList<Department> departments = entityManager.createQuery(
"select d from Department d", Department.class)
.getResultList();
log.infof("Fetched %d Departments", departments.size());
for (Department department : departments) {
assertEquals(3, department.getEmployees().size());
}
SELECT
d.id as id1_0_
FROM
Department d
-- Fetched 2 Departments
SELECT
e.department_id as departme3_1_0_,
e.id as id1_1_0_,
e.id as id1_1_1_,
e.department_id as departme3_1_1_,
e.username as username2_1_1_
FROM
Employee e
WHERE
e.department_id = 1
SELECT
e.department_id as departme3_1_0_,
e.id as id1_1_0_,
e.id as id1_1_1_,
e.department_id as departme3_1_1_,
e.username as username2_1_1_
FROM
Employee e
WHERE
e.department_id = 2
The more Department
entities are fetched by the first query, the more secondary SELECT
statements are executed to initialize the employees
collections.
Therefore, FetchMode.SELECT
can lead to N + 1 query issue.
11.11. FetchMode.SUBSELECT
To demonstrate how FetchMode.SUBSELECT
works, we are going to modify the FetchMode.SELECT
mapping example to use
FetchMode.SUBSELECT
:
FetchMode.SUBSELECT
mapping example@OneToMany(mappedBy = "department", fetch = FetchType.LAZY)
@Fetch(FetchMode.SUBSELECT)
private List<Employee> employees = new ArrayList<>();
Now, we are going to fetch all Department
entities that match a given filtering predicate
and then navigate their employees
collections.
Hibernate is going to avoid the N + 1 query issue by generating a single SQL statement to initialize all employees
collections
for all Department
entities that were previously fetched.
Instead of passing all entity identifiers, Hibernate simply reruns the previous query that fetched the Department
entities.
FetchMode.SUBSELECT
mapping exampleList<Department> departments = entityManager.createQuery(
"select d " +
"from Department d " +
"where d.name like :token", Department.class)
.setParameter("token", "Department%")
.getResultList();
log.infof("Fetched %d Departments", departments.size());
for (Department department : departments) {
assertEquals(3, department.getEmployees().size());
}
SELECT
d.id as id1_0_
FROM
Department d
where
d.name like 'Department%'
-- Fetched 2 Departments
SELECT
e.department_id as departme3_1_1_,
e.id as id1_1_1_,
e.id as id1_1_0_,
e.department_id as departme3_1_0_,
e.username as username2_1_0_
FROM
Employee e
WHERE
e.department_id in (
SELECT
fetchmodes0_.id
FROM
Department fetchmodes0_
WHERE
d.name like 'Department%'
)
11.12. FetchMode.JOIN
To demonstrate how FetchMode.JOIN
works, we are going to modify the FetchMode.SELECT
mapping example to use
FetchMode.JOIN
instead:
FetchMode.JOIN
mapping example@OneToMany(mappedBy = "department")
@Fetch(FetchMode.JOIN)
private List<Employee> employees = new ArrayList<>();
Now, we are going to fetch one Department
and navigate its employees
collections.
The reason why we are not using a JPQL query to fetch multiple To fetch multiple relationships with a JPQL query, the Therefore, Also, the |
Hibernate is going to avoid the secondary query by issuing an OUTER JOIN for the employees
collection.
FetchMode.JOIN
mapping exampleDepartment department = entityManager.find(Department.class, 1L);
log.infof("Fetched department: %s", department.getId());
assertEquals(3, department.getEmployees().size());
SELECT
d.id as id1_0_0_,
e.department_id as departme3_1_1_,
e.id as id1_1_1_,
e.id as id1_1_2_,
e.department_id as departme3_1_2_,
e.username as username2_1_2_
FROM
Department d
LEFT OUTER JOIN
Employee e
on d.id = e.department_id
WHERE
d.id = 1
-- Fetched department: 1
This time, there was no secondary query because the child collection was loaded along with the parent entity.
11.13. @LazyCollection
The @LazyCollection
annotation is used to specify the lazy fetching behavior of a given collection.
The possible values are given by the LazyCollectionOption
enumeration:
TRUE
-
Load it when the state is requested.
FALSE
-
Eagerly load it.
EXTRA
-
Prefer extra queries over full collection loading.
The TRUE
and FALSE
values are deprecated since you should be using the Jakarta Persistence FetchType
attribute of the @ElementCollection
, @OneToMany
, or @ManyToMany
collection.
The EXTRA
value has no equivalent in the Jakarta Persistence specification, and it’s used to avoid loading the entire collection even when the collection is accessed for the first time.
Each element is fetched individually using a secondary query.
LazyCollectionOption.EXTRA
mapping example@Entity(name = "Department")
public static class Department {
@Id
private Long id;
@OneToMany(mappedBy = "department", cascade = CascadeType.ALL)
@OrderColumn(name = "order_id")
@LazyCollection(LazyCollectionOption.EXTRA)
private List<Employee> employees = new ArrayList<>();
//Getters and setters omitted for brevity
}
@Entity(name = "Employee")
public static class Employee {
@Id
private Long id;
@NaturalId
private String username;
@ManyToOne(fetch = FetchType.LAZY)
private Department department;
//Getters and setters omitted for brevity
}
For bags (e.g. regular List(s) of entities that do not preserve any certain ordering),
the |
Now, considering we have the following entities:
LazyCollectionOption.EXTRA
Domain Model exampleDepartment department = new Department();
department.setId(1L);
entityManager.persist(department);
for (long i = 1; i <= 3; i++) {
Employee employee = new Employee();
employee.setId(i);
employee.setUsername(String.format("user_%d", i));
department.addEmployee(employee);
}
When fetching the employee
collection entries by their position in the List
,
Hibernate generates the following SQL statements:
LazyCollectionOption.EXTRA
fetching exampleDepartment department = entityManager.find(Department.class, 1L);
int employeeCount = department.getEmployees().size();
for(int i = 0; i < employeeCount; i++) {
log.infof("Fetched employee: %s", department.getEmployees().get(i).getUsername());
}
SELECT
max(order_id) + 1
FROM
Employee
WHERE
department_id = ?
-- binding parameter [1] as [BIGINT] - [1]
SELECT
e.id as id1_1_0_,
e.department_id as departme3_1_0_,
e.username as username2_1_0_
FROM
Employee e
WHERE
e.department_id=?
AND e.order_id=?
-- binding parameter [1] as [BIGINT] - [1]
-- binding parameter [2] as [INTEGER] - [0]
SELECT
e.id as id1_1_0_,
e.department_id as departme3_1_0_,
e.username as username2_1_0_
FROM
Employee e
WHERE
e.department_id=?
AND e.order_id=?
-- binding parameter [1] as [BIGINT] - [1]
-- binding parameter [2] as [INTEGER] - [1]
SELECT
e.id as id1_1_0_,
e.department_id as departme3_1_0_,
e.username as username2_1_0_
FROM
Employee e
WHERE
e.department_id=?
AND e.order_id=?
-- binding parameter [1] as [BIGINT] - [1]
-- binding parameter [2] as [INTEGER] - [2]
Therefore, the child entities were fetched one after the other without triggering a full collection initialization. For this reason, caution is advised since accessing all elements using |
12. Batching
12.1. JDBC batching
JDBC offers support for batching together SQL statements that can be represented as a single PreparedStatement. Implementation wise this generally means that drivers will send the batched operation to the server in one call, which can save on network calls to the database. Hibernate can leverage JDBC batching. The following settings control this behavior.
hibernate.jdbc.batch_size
-
Controls the maximum number of statements Hibernate will batch together before asking the driver to execute the batch. Zero or a negative number disables this feature.
hibernate.jdbc.batch_versioned_data
-
Some JDBC drivers return incorrect row counts when a batch is executed. If your JDBC driver falls into this category this setting should be set to
false
. Otherwise, it is safe to enable this which will allow Hibernate to still batch the DML for versioned entities and still use the returned row counts for optimistic lock checks. Since 5.0, it defaults to true. Previously (versions 3.x and 4.x), it used to be false. hibernate.jdbc.batch.builder
-
Names the implementation class used to manage batching capabilities. It is almost never a good idea to switch from Hibernate’s default implementation. But if you wish to, this setting would name the
org.hibernate.engine.jdbc.batch.spi.BatchBuilder
implementation to use. hibernate.order_updates
-
Forces Hibernate to order SQL updates by the entity type and the primary key value of the items being updated. This allows for more batching to be used. It will also result in fewer transaction deadlocks in highly concurrent systems. Comes with a performance hit, so benchmark before and after to see if this actually helps or hurts your application.
hibernate.order_inserts
-
Forces Hibernate to order inserts to allow for more batching to be used. Comes with a performance hit, so benchmark before and after to see if this actually helps or hurts your application.
Since version 5.2, Hibernate allows overriding the global JDBC batch size given by the |
Session
basisentityManager
.unwrap(Session.class)
.setJdbcBatchSize(10);
12.2. Session batching
The following example shows an anti-pattern for batch inserts.
EntityManager entityManager = null;
EntityTransaction txn = null;
try {
entityManager = entityManagerFactory().createEntityManager();
txn = entityManager.getTransaction();
txn.begin();
for (int i = 0; i < 100_000; i++) {
Person Person = new Person(String.format("Person %d", i));
entityManager.persist(Person);
}
txn.commit();
} catch (RuntimeException e) {
if (txn != null && txn.isActive()) txn.rollback();
throw e;
} finally {
if (entityManager != null) {
entityManager.close();
}
}
There are several problems associated with this example:
-
Hibernate caches all the newly inserted
Customer
instances in the session-level cache, so, when the transaction ends, 100 000 entities are managed by the persistence context. If the maximum memory allocated to the JVM is rather low, this example could fail with anOutOfMemoryException
. The Java 1.8 JVM allocated either 1/4 of available RAM or 1Gb, which can easily accommodate 100 000 objects on the heap. -
long-running transactions can deplete a connection pool so other transactions don’t get a chance to proceed.
-
JDBC batching is not enabled by default, so every insert statement requires a database roundtrip. To enable JDBC batching, set the
hibernate.jdbc.batch_size
property to an integer between 10 and 50.
Hibernate disables insert batching at the JDBC level transparently if you use an identity identifier generator. |
12.2.1. Batch inserts
When you make new objects persistent, employ methods flush()
and clear()
to the session regularly, to control the size of the first-level cache.
Session
EntityManager entityManager = null;
EntityTransaction txn = null;
try {
entityManager = entityManagerFactory().createEntityManager();
txn = entityManager.getTransaction();
txn.begin();
int batchSize = 25;
for (int i = 0; i < entityCount; i++) {
if (i > 0 && i % batchSize == 0) {
//flush a batch of inserts and release memory
entityManager.flush();
entityManager.clear();
}
Person Person = new Person(String.format("Person %d", i));
entityManager.persist(Person);
}
txn.commit();
} catch (RuntimeException e) {
if (txn != null && txn.isActive()) txn.rollback();
throw e;
} finally {
if (entityManager != null) {
entityManager.close();
}
}
12.2.2. Session scroll
When you retrieve and update data, flush()
and clear()
the session regularly.
In addition, use method scroll()
to take advantage of server-side cursors for queries that return many rows of data.
scroll()
EntityManager entityManager = null;
EntityTransaction txn = null;
ScrollableResults scrollableResults = null;
try {
entityManager = entityManagerFactory().createEntityManager();
txn = entityManager.getTransaction();
txn.begin();
int batchSize = 25;
Session session = entityManager.unwrap(Session.class);
scrollableResults = session
.createSelectionQuery("select p from Person p")
.setCacheMode(CacheMode.IGNORE)
.scroll(ScrollMode.FORWARD_ONLY);
int count = 0;
while (scrollableResults.next()) {
Person Person = (Person) scrollableResults.get();
processPerson(Person);
if (++count % batchSize == 0) {
//flush a batch of updates and release memory:
entityManager.flush();
entityManager.clear();
}
}
txn.commit();
} catch (RuntimeException e) {
if (txn != null && txn.isActive()) txn.rollback();
throw e;
} finally {
if (scrollableResults != null) {
scrollableResults.close();
}
if (entityManager != null) {
entityManager.close();
}
}
If left unclosed by the application, Hibernate will automatically close the underlying resources (e.g. However, it is good practice to close the |
12.2.3. StatelessSession
StatelessSession
is an alternative to Session
and provides:
-
a command-oriented API
-
with no associated persistence context.
Thus, a stateless session is a slightly lower-level abstraction that’s closer to the underlying JDBC activity:
-
there’s no first-level cache,
-
the stateless session does not interact with any second-level or query cache, and
-
there’s no transactional write-behind or automatic dirty checking.
Instead, persistence operations occur synchronously when a method of StatelessSession
is invoked, and entities returned by a stateless session are always detached.
A stateless session may be used to stream data to and from the database in the form of detached objects. With a stateless session, there’s no need to explicitly manage the size of the first-level cache by explicitly clearing the persistence context. |
The StatelessSession
API comes with certain limitations:
-
operations performed using a stateless session never cascade to associated instances,
-
collections are ignored by a stateless session,
-
lazy loading of associations is not transparent, and is only available via an explicit operation named
fetch()
, and -
operations performed via a stateless session bypass Hibernate’s event model and interceptors.
Due to the lack of a first-level cache, stateless sessions are vulnerable to data aliasing effects. |
StatelessSession
StatelessSession statelessSession = null;
Transaction txn = null;
ScrollableResults scrollableResults = null;
try {
SessionFactory sessionFactory = entityManagerFactory().unwrap(SessionFactory.class);
statelessSession = sessionFactory.openStatelessSession();
txn = statelessSession.getTransaction();
txn.begin();
scrollableResults = statelessSession
.createSelectionQuery("select p from Person p")
.scroll(ScrollMode.FORWARD_ONLY);
while (scrollableResults.next()) {
Person Person = (Person) scrollableResults.get();
processPerson(Person);
statelessSession.update(Person);
}
txn.commit();
} catch (RuntimeException e) {
if (txn != null && txn.getStatus() == TransactionStatus.ACTIVE) txn.rollback();
throw e;
} finally {
if (scrollableResults != null) {
scrollableResults.close();
}
if (statelessSession != null) {
statelessSession.close();
}
}
The Customer
instances returned by the query are immediately detached.
They are never associated with any persistence context.
The |
12.3. Hibernate Query Language for DML
DML, or Data Manipulation Language, refers to SQL statements such as INSERT
, UPDATE
, and DELETE
.
Hibernate provides methods for bulk SQL-style DML statement execution, in the form of Hibernate Query Language (HQL).
12.3.1. HQL/JPQL for UPDATE and DELETE
Both the Hibernate native Query Language and JPQL (Java Persistence Query Language) provide support for bulk UPDATE and DELETE.
UPDATE FROM EntityName e WHERE e.name = ?
DELETE FROM EntityName e WHERE e.name = ?
Although the |
The FROM
clause can only refer to a single entity, which can be aliased.
If the entity name is aliased, any property references must be qualified using that alias.
If the entity name is not aliased, then it is illegal for any property references to be qualified.
Joins, either implicit or explicit, are prohibited in a bulk HQL query.
You can use sub-queries in the |
UPDATE
, using the Query.executeUpdate()
int updatedEntities = entityManager.createQuery(
"update Person p " +
"set p.name = :newName " +
"where p.name = :oldName")
.setParameter("oldName", oldName)
.setParameter("newName", newName)
.executeUpdate();
UPDATE
, using the Query.executeUpdate()
int updatedEntities = session.createMutationQuery(
"update Person " +
"set name = :newName " +
"where name = :oldName")
.setParameter("oldName", oldName)
.setParameter("newName", newName)
.executeUpdate();
In keeping with the EJB3 specification, HQL UPDATE
statements, by default, do not effect the version or the timestamp property values for the affected entities.
You can use a versioned update to force Hibernate to reset the version or timestamp property values, by adding the VERSIONED
keyword after the UPDATE
keyword.
int updatedEntities = session.createMutationQuery(
"update versioned Person " +
"set name = :newName " +
"where name = :oldName")
.setParameter("oldName", oldName)
.setParameter("newName", newName)
.executeUpdate();
If you use the This feature is only available in HQL since it’s not standardized by Jakarta Persistence. |
DELETE
statementint deletedEntities = entityManager.createQuery(
"delete Person p " +
"where p.name = :name")
.setParameter("name", name)
.executeUpdate();
DELETE
statementint deletedEntities = session.createMutationQuery(
"delete Person " +
"where name = :name")
.setParameter("name", name)
.executeUpdate();
Method Query.executeUpdate()
returns an int
value, which indicates the number of entities affected by the operation.
This may or may not correlate to the number of rows affected in the database.
A JPQL/HQL bulk operation might result in multiple SQL statements being executed, such as for joined-subclass.
In the example of joined-subclass, a DELETE
against one of the subclasses may actually result in deletes in the tables underlying the join, or further down the inheritance hierarchy.
12.3.2. HQL syntax for INSERT
INSERT INTO EntityName
properties_list
SELECT select_list
FROM ...
Alternatively one can also declare individual values
INSERT INTO EntityName
properties_list
VALUES values_list
The properties_list
is analogous to the column specification in the SQL
INSERT
statement.
Note that INSERT
statements are inherently non-polymorphic, so it is not possible to use an EntityName
which is abstract or refer to subclass properties.
The SELECT statement can be any valid HQL select query, but the return types must match the types expected by the INSERT.
Hibernate verifies the return types during query compilation, instead of expecting the database to check it.
Problems might result from Hibernate types which are equivalent, rather than equal.
One such example is a mismatch between a property defined as an org.hibernate.type.StandardBasicTypes.DATE
and a property defined as an org.hibernate.type.StandardBasicTypes.TIMESTAMP
,
even though the database may not make a distinction, or may be capable of handling the conversion.
If id property is not specified in the properties_list
, Hibernate generates a value automatically.
Automatic generation is only available if you use ID generators which operate on the database.
Otherwise, Hibernate throws an exception during parsing. Available in-database generators implement
org.hibernate.id.PostInsertIdentifierGenerator
.
For properties mapped as either version or timestamp, the insert statement gives you two options.
You can either specify the property in the properties_list, in which case its value is taken from the corresponding select expressions or omit it from the properties_list,
in which case the seed value defined by the org.hibernate.type.descriptor.java.VersionJavaType
is used.
int insertedEntities = session.createMutationQuery(
"insert into Partner (id, name) " +
"select p.id, p.name " +
"from Person p ")
.executeUpdate();
This section is only a brief overview of HQL. For more information, see Hibernate Query Language.
12.3.3. Bulk mutation strategies
When a bulk mutation involves multiple tables, Hibernate has to issue individual DML statements to the respective tables. Since the mutation itself could have an effect on the conditions used in the statement, it’s generally not possible to simply execute parts of the DML statement against the respective tables. Instead, Hibernate has to temporarily remember which rows will be affected, and execute the DML statements based on these rows.
Usually, Hibernate will make use of local or global temporary tables to remember the primary keys of the rows.
For some databases, currently only PostgreSQL and DB2, a more advanced strategy (CteMutationStrategy
) is used,
which makes use of DML in CTE support to execute the whole operation in one SQL statement.
The chosen strategy, unless overridden through the hibernate.query.mutation_strategy
setting, is based on the
Dialect support through org.hibernate.dialect.Dialect.getFallbackSqmMutationStrategy
.
Class diagram
Considering we have the following entities:
The Person
entity is the base class of this entity inheritance model, and is mapped as follows:
@Entity(name = "Person")
@Inheritance(strategy = InheritanceType.JOINED)
public static class Person implements Serializable {
@Id
private Integer id;
@Id
private String companyName;
private String name;
private boolean employed;
//Getters and setters are omitted for brevity
}
Both the Doctor
and Engineer
entity classes extend the Person
base class:
@Entity(name = "Doctor")
public static class Doctor extends Person {
}
@Entity(name = "Engineer")
public static class Engineer extends Person {
private boolean fellow;
public boolean isFellow() {
return fellow;
}
public void setFellow(boolean fellow) {
this.fellow = fellow;
}
}
Inheritance tree bulk processing
Now, when you try to execute a bulk entity delete query:
int updateCount = session.createQuery(
"delete from Person where employed = :employed" )
.setParameter( "employed", false )
.executeUpdate();
create temporary table
HT_Person
(
id int4 not null,
companyName varchar(255) not null
)
insert
into
HT_Person
select
p.id as id,
p.companyName as companyName
from
Person p
where
p.employed = ?
delete
from
Engineer
where
(
id, companyName
) IN (
select
id,
companyName
from
HT_Person
)
delete
from
Doctor
where
(
id, companyName
) IN (
select
id,
companyName
from
HT_Person
)
delete
from
Person
where
(
id, companyName
) IN (
select
id,
companyName
from
HT_Person
)
HT_Person
is a temporary table that Hibernate creates to hold all the entity identifiers that are to be updated or deleted by the bulk operation.
The temporary table can be either global or local, depending on the underlying database capabilities.
Non-temporary table bulk mutation strategies
When the temporary table strategy can not be used because the database user lacks privilege to create temporary tables,
the InlineMutationStrategy
must be used.
To use this strategy, you need to configure the following configuration property:
<property name="hibernate.query.mutation_strategy"
value="org.hibernate.query.sqm.mutation.internal.inline.InlineMutationStrategy"
/>
Now, when running the previous test case, Hibernate generates the following SQL statements:
InlineIdsInClauseBulkIdStrategy
delete entity query exampleselect
p.id as id,
p.companyName as companyName
from
Person p
where
p.employed = ?
delete
from
Engineer
where
( id, companyName )
in (
( 1,'Red Hat USA' ),
( 3,'Red Hat USA' ),
( 1,'Red Hat Europe' ),
( 3,'Red Hat Europe' )
)
delete
from
Doctor
where
( id, companyName )
in (
( 1,'Red Hat USA' ),
( 3,'Red Hat USA' ),
( 1,'Red Hat Europe' ),
( 3,'Red Hat Europe' )
)
delete
from
Person
where
( id, companyName )
in (
( 1,'Red Hat USA' ),
( 3,'Red Hat USA' ),
( 1,'Red Hat Europe' ),
( 3,'Red Hat Europe' )
)
So, the entity identifiers are selected first and used for each particular update or delete statement.
13. Caching
At runtime, Hibernate handles moving data into and out of the second-level cache in response to the operations performed by the Session
, which acts as a transaction-level cache of persistent data.
Once an entity becomes managed, that object is added to the internal cache of the current persistence context (EntityManager
or Session
).
The persistence context is also called the first-level cache, and it’s enabled by default.
It is possible to configure a JVM-level (SessionFactory
-level) or even a cluster cache on a class-by-class and collection-by-collection basis.
Be aware that Hibernate caches are not aware of changes made to the persistent store by other applications. To address this limitation, you can configure a TTL (Time To Live) retention policy at the second-level cache region level so that the underlying cache entries expire regularly. |
13.1. Configuring second-level caching
Hibernate can integrate with various caching providers for the purpose of caching data outside the context of a particular Session
.
This section defines the settings which control this behavior.
13.1.1. RegionFactory
org.hibernate.cache.spi.RegionFactory
defines the integration between Hibernate and a pluggable caching provider.
hibernate.cache.region.factory_class
is used to declare the provider to use.
Hibernate comes with built-in support for the Java caching standard JCache
and also the popular caching library: Infinispan.
Detailed information is provided later in this chapter.
13.1.2. Caching configuration properties
Besides provider specific configuration, there are a number of configurations options on the Hibernate side of the integration that control various caching behaviors:
hibernate.cache.use_second_level_cache
-
Enable or disable second level caching overall. By default, if the currently configured
RegionFactory
is not theNoCachingRegionFactory
, then the second-level cache is going to be enabled. Otherwise, the second-level cache is disabled. hibernate.cache.use_query_cache
-
Enable or disable second level caching of query results. The default is false.
hibernate.cache.query_cache_factory
-
Query result caching is handled by a special contract that deals with staleness-based invalidation of the results. The default implementation does not allow stale results at all. Use this for applications that would like to relax that. Names an implementation of
org.hibernate.cache.spi.TimestampsCacheFactory
. hibernate.cache.use_minimal_puts
-
Optimizes second-level cache operations to minimize writes, at the cost of more frequent reads. Providers typically set this appropriately.
hibernate.cache.region_prefix
-
Defines a name to be used as a prefix to all second-level cache region names.
hibernate.cache.default_cache_concurrency_strategy
-
In Hibernate second-level caching, all regions can be configured differently including the concurrency strategy to use when accessing that particular region. This setting allows defining a default strategy to be used. This setting is very rarely required as the pluggable providers do specify the default strategy to use. Valid values include:
-
read-only,
-
read-write,
-
nonstrict-read-write,
-
transactional
-
hibernate.cache.use_structured_entries
-
If
true
, forces Hibernate to store data in the second-level cache in a more human-friendly format. Can be useful if you’d like to be able to "browse" the data directly in your cache, but does have a performance impact. hibernate.cache.auto_evict_collection_cache
-
Enables or disables the automatic eviction of a bidirectional association’s collection cache entry when the association is changed just from the owning side. This is disabled by default, as it has a performance impact to track this state. However, if your application does not manage both sides of bidirectional association where the collection side is cached, the alternative is to have stale data in that collection cache.
hibernate.cache.use_reference_entries
-
Enable direct storage of entity references into the second level cache for read-only or immutable entities.
hibernate.cache.keys_factory
-
When storing entries into the second-level cache as a key-value pair, the identifiers can be wrapped into tuples <entity type, tenant, identifier> to guarantee uniqueness in case that second-level cache stores all entities in single space. These tuples are then used as keys in the cache. When the second-level cache implementation (incl. its configuration) guarantees that different entity types are stored separately and multi-tenancy is not used, you can omit this wrapping to achieve better performance. Currently, this property is only supported when Infinispan is configured as the second-level cache implementation. Valid values are:
-
default
(wraps identitifers in the tuple) -
simple
(uses identifiers as keys without any wrapping) -
fully qualified class name that implements
org.hibernate.cache.spi.CacheKeysFactory
-
13.2. Configuring second-level cache mappings
The cache mappings can be configured via Jakarta Persistence annotations or XML descriptors or using the Hibernate-specific mapping files.
By default, entities are not part of the second level cache and we recommend you to stick to this setting.
However, you can override this by setting the shared-cache-mode
element in your persistence.xml
file
or by using the jakarta.persistence.sharedCache.mode
property in your configuration file.
The following values are possible:
ENABLE_SELECTIVE
(Default and recommended value)-
Entities are not cached unless explicitly marked as cacheable (with the
@Cacheable
annotation). DISABLE_SELECTIVE
-
Entities are cached unless explicitly marked as non-cacheable.
ALL
-
Entities are always cached even if marked as non-cacheable.
NONE
-
No entity is cached even if marked as cacheable. This option can make sense to disable second-level cache altogether.
The cache concurrency strategy used by default can be set globally via the hibernate.cache.default_cache_concurrency_strategy
configuration property.
The values for this property are:
- read-only
-
If your application needs to read, but not modify, instances of a persistent class, a read-only cache is the best choice. Application can still delete entities and these changes should be reflected in second-level cache so that the cache does not provide stale entities. Implementations may use performance optimizations based on the immutability of entities.
- read-write
-
If the application needs to update data, a read-write cache might be appropriate. This strategy provides consistent access to single entity, but not a serializable transaction isolation level; e.g. when TX1 reads looks up an entity and does not find it, TX2 inserts the entity into cache and TX1 looks it up again, the new entity can be read in TX1.
- nonstrict-read-write
-
Similar to read-write strategy but there might be occasional stale reads upon concurrent access to an entity. The choice of this strategy might be appropriate if the application rarely updates the same data simultaneously and strict transaction isolation is not required. Implementations may use performance optimizations that make use of the relaxed consistency guarantee.
- transactional
-
Provides serializable transaction isolation level.
Rather than using a global setting, it is recommended to define the cache concurrency strategy on a per entity basis. Use the |
The @Cache
annotation define three attributes:
- usage
-
Defines the
CacheConcurrencyStrategy
- region
-
Defines a cache region where entries will be stored
- include
-
If lazy properties should be included in the second level cache. The default value is
all
so lazy properties are cacheable. The other possible value isnon-lazy
so lazy properties are not cacheable.
13.3. Entity inheritance and second-level cache mapping
Traditionally, when using entity inheritance, Hibernate required an entity hierarchy to be either cached entirely or not cached at all.
Therefore, if you wanted to cache a subclass belonging to a given entity hierarchy,
the Jakarta Persistence @Cacheable
and the Hibernate-specific @Cache
annotations would have to be declared at the root-entity level only.
Although we still believe that all entities belonging to a given entity hierarchy should share the same caching semantics,
the Jakarta Persistence specification says that the @Cacheable
annotation could be overwritten by a subclass:
The value of the
Cacheable
annotation is inherited by subclasses; it can be overridden by specifyingCacheable
on a subclass.
As of Hibernate ORM 5.3, you can now override a base class However, the Hibernate cache concurrency strategy (e.g. read-only, nonstrict-read-write, read-write, transactional) is still defined at the root entity level and cannot be overridden. |
Nevertheless, the reasons why we advise you to have all entities belonging to an inheritance tree share the same caching definition can be summed as follows:
-
from a performance perspective, adding an additional check on a per entity type level slows the bootstrap process.
-
providing different caching semantics for subclasses would violate the Liskov substitution principle.
13.4. Entity cache
@Entity(name = "Phone")
@Cacheable
@org.hibernate.annotations.Cache(usage = CacheConcurrencyStrategy.NONSTRICT_READ_WRITE)
public static class Phone {
@Id
@GeneratedValue
private Long id;
private String mobile;
@ManyToOne
private Person person;
@Version
private int version;
//Getters and setters are omitted for brevity
}
Hibernate stores cached entities in a dehydrated form, which is similar to the database representation.
Aside from the foreign key column values of the @ManyToOne
or @OneToOne
child-side associations,
entity relationships are not stored in the cache,
Once an entity is stored in the second-level cache, you can avoid a database hit and load the entity from the cache alone:
Person person = entityManager.find(Person.class, 1L);
Person person = session.get(Person.class, 1L);
The Hibernate second-level cache can also load entities by their natural id:
@Entity(name = "Person")
@Cacheable
@org.hibernate.annotations.Cache(usage = CacheConcurrencyStrategy.READ_WRITE)
public static class Person {
@Id
@GeneratedValue