- 5. Persistence Context
- 5.1. Accessing Hibernate APIs from JPA
- 5.2. Bytecode Enhancement
- 5.3. Making entities persistent
- 5.4. Deleting (removing) entities
- 5.5. Obtain an entity reference without initializing its data
- 5.6. Obtain an entity with its data initialized
- 5.7. Obtain multiple entities by their identifiers
- 5.8. Obtain an entity by natural-id
- 5.9. Filtering entities and associations
- 5.10. Modifying managed/persistent state
- 5.11. Refresh entity state
- 5.12. Working with detached data
- 5.13. Checking persistent state
- 5.14. Evicting entities
- 5.15. Cascading entity state transitions
- 5.16. Exception handling
5. Persistence Context
Both the org.hibernate.Session
API and javax.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
or persistent
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 javax.persistence.EntityManager
methods deal with moving entities among these states.
5.1. Accessing Hibernate APIs from JPA
JPA defines an incredibly useful method to allow applications access to the APIs of the underlying provider.
Example 294. Accessing Hibernate APIs from JPA
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.
5.2.1. Capabilities
Hibernate supports the enhancement of an application Java domain model for the purpose of adding various persistence-related capabilities directly into the class.
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.
Example 295. @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 |
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.
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:
Example 296. 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
}
Example 297. Incorrect normal Java usage
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:
Example 298. Correct normal Java usage
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.
Internal performance optimizations
Additionally, we use the enhancement process to add some additional code that allows us to optimized certain performance characteristics of the persistence context. These are hard to discuss without diving into a discussion of Hibernate internals.
5.2.2. Performing enhancement
Runtime enhancement
Currently, runtime enhancement of the domain model is only supported in managed JPA environments following the JPA-defined SPI for performing class transformations.
Even then, this support is disabled by default. To enable runtime enhancement, specify one of the following configuration properties:
**hibernate.enhancer.enableDirtyTracking**
(e.g. true
or false
(default value))
Enable dirty tracking feature in runtime bytecode enhancement.
**hibernate.enhancer.enableLazyInitialization**
(e.g. true
or false
(default value))
Enable lazy loading feature in runtime bytecode enhancement. This way, even basic types (e.g. @Basic(fetch = FetchType.LAZY
)) can be fetched lazily.
**hibernate.enhancer.enableAssociationManagement**
(e.g. true
or false
(default value))
Enable association management feature in runtime bytecode enhancement which automatically synchronizes a bidirectional association when only one side is changed.
Also, at the moment, only annotated classes support runtime enhancement. |
Gradle plugin
Hibernate provides a Gradle plugin that is capable of providing build-time enhancement of the domain model as they are compiled as part of a Gradle build. To use the plugin, a project would first need to apply it:
Example 299. Apply the Gradle plugin
apply plugin: 'org.hibernate.orm'
ext {
hibernateVersion = 'hibernate-version-you-want'
}
buildscript {
dependencies {
classpath "org.hibernate:hibernate-gradle-plugin:$hibernateVersion"
}
}
hibernate {
enhance {
enableLazyInitialization = true
enableDirtyTracking = true
enableAssociationManagement = true
}
}
The configuration that is available is exposed through a registered Gradle DSL extension:
enableLazyInitialization
Whether enhancement for lazy attribute loading should be done.
enableDirtyTracking
Whether enhancement for self-dirty tracking should be done.
enableAssociationManagement
Whether enhancement for bi-directional association management should be done.
The default value for all 3 configuration settings is false
.
The enhance { }
block is required in order for enhancement to occur. Enhancement is disabled by default in preparation for additions capabilities (hbm2ddl, etc) in the plugin.
Maven plugin
Hibernate provides a Maven plugin capable of providing build-time enhancement of the domain model as they are compiled as part of a Maven build. See the section on the Gradle plugin for details on the configuration settings. Again, the default for those 3 is false
.
The Maven plugin supports one additional configuration settings: failOnError, which controls what happens in case of error. The default behavior is to fail the build, but it can be set so that only a warning is issued.
Example 300. Apply the Maven plugin
<build>
<plugins>
[...]
<plugin>
<groupId>org.hibernate.orm.tooling</groupId>
<artifactId>hibernate-enhance-maven-plugin</artifactId>
<version>$currentHibernateVersion</version>
<executions>
<execution>
<configuration>
<failOnError>true</failOnError>
<enableLazyInitialization>true</enableLazyInitialization>
<enableDirtyTracking>true</enableDirtyTracking>
<enableAssociationManagement>true</enableAssociationManagement>
</configuration>
<goals>
<goal>enhance</goal>
</goals>
</execution>
</executions>
</plugin>
[...]
</plugins>
</build>
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 javax.persistence.EntityManager
.
Example 301. Making an entity persistent with JPA
Person person = new Person();
person.setId( 1L );
person.setName("John Doe");
entityManager.persist( person );
Example 302. Making an entity persistent with Hibernate API
Person person = new Person();
person.setId( 1L );
person.setName("John Doe");
session.save( person );
org.hibernate.Session
also has a method named persist which follows the exact semantics defined in the JPA specification for the persist method. It is this org.hibernate.Session
method to which the Hibernate javax.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.
Example 303. Deleting an entity with JPA
entityManager.remove( person );
Example 304. Deleting an entity with the Hibernate API
session.delete( person );
Hibernate itself can handle deleting entities in detached state. JPA, 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.
Example 305. Obtaining an entity reference without initializing its data with JPA
Book book = new Book();
book.setAuthor( entityManager.getReference( Person.class, personId ) );
Example 306. Obtaining an entity reference without initializing its data with Hibernate API
Book book = new Book();
book.setId( 1L );
book.setIsbn( "123-456-7890" );
entityManager.persist( book );
book.setAuthor( session.load( 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).
Example 307. Obtaining an entity reference with its data initialized with JPA
Person person = entityManager.find( Person.class, personId );
Example 308. Obtaining an entity reference with its data initialized with Hibernate API
Person person = session.get( Person.class, personId );
Example 309. Obtaining an entity reference with its data initialized using the byId()
Hibernate API
Person 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:
Example 310. Obtaining an Optional entity reference with its data initialized using the byId()
Hibernate API
Optional<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 JPA 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 return List
is ordered and positional relative to the incoming ids. In other words, a request to multiLoad([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 Hibernate Session
, the cached entity will be added to the returned List
, 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
or Session.remove
method, but the Session
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 returning List
.
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 JPA EntityGraph
contract, and this method allows you to pass a specific RootGraph
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:
Example 311. Loading multiple entities using the byMultipleIds()
Hibernate API
Session 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.
Example 312. Loading multiple entities from the second-level cache
SessionFactory sessionFactory = entityManagerFactory().unwrap( SessionFactory.class );
Statistics statistics = sessionFactory.getStatistics();
sessionFactory.getCache().evictAll();
statistics.clear();
sqlStatementInterceptor.clear();
assertEquals( 0, statistics.getQueryExecutionCount() );
doInJPA( this::entityManagerFactory, 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, sqlStatementInterceptor.getSqlQueries().size() );
doInJPA( this::entityManagerFactory, entityManager -> {
Session session = entityManager.unwrap( Session.class );
sqlStatementInterceptor.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, sqlStatementInterceptor.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.
Example 313. Natural-id mapping
@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.
Example 314. Get entity reference by simple natural-id
Book book = session.bySimpleNaturalId( Book.class ).getReference( isbn );
Example 315. Load entity by natural-id
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:
Example 316. Load an Optional entity by 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.
Example 317. @Where
mapping usage
public 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:
Example 318. Persisting and fetching entities with a @Where
mapping
doInJPA( 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.
Example 319. Query entities mapped with @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.
Example 320. Traversing collections mapped with @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).
Example 321. @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:
Example 322. @WhereJoinTable
test data
Book 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:
Example 323. @WhereJoinTable
fetch example
Book 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:
Example 324. @Filter
mapping entity-level usage
@Entity(name = "Account")
@FilterDef(
name="activeAccount",
parameters = @ParamDef(
name="active",
type="boolean"
)
)
@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:
Example 325. @Filter
mapping collection-level usage
@Entity(name = "Client")
public static class Client {
@Id
private Long id;
private String name;
@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:
Example 326. Persisting and fetching entities with a @Filter
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 )
.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.
Example 327. Query entities mapped without activating the @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.
Example 328. Query entities mapped with @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, contrary to an entity query, the filter does not prevent 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
.
Example 329. Traversing collections without activating the @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.
Example 330. Traversing collections mapped with @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.
Example 331. @SqlFragmentAlias
mapping usage
@Entity(name = "Account")
@Table(name = "account")
@SecondaryTable(
name = "account_details"
)
@SQLDelete(
sql = "UPDATE account_details SET deleted = true WHERE id = ? "
)
@FilterDef(
name="activeAccount",
parameters = @ParamDef(
name="active",
type="boolean"
)
)
@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:
Example 332. Fetching a collection filtered with @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:
Example 333. @FilterJoinTable
mapping usage
@Entity(name = "Client")
@FilterDef(
name="firstAccounts",
parameters=@ParamDef(
name="maxOrderId",
type="int"
)
)
@Filter(
name="firstAccounts",
condition="order_id <= :maxOrderId"
)
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:
Example 334. Persisting and fetching entities with a @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
.
Example 335. Traversing collections mapped with @FilterJoinTable
without enabling the filter
Client 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
.
Example 336. Traversing collections mapped with @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.
Example 337. Modifying managed state with JPA
Person person = entityManager.find( Person.class, personId );
person.setName("John Doe");
entityManager.flush();
Example 338. Modifying managed state with Hibernate API
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:
Example 339. 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:
Example 340. Persisting a Product
entity
Product 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:
Example 341. Modifying the Product
entity
doInJPA( 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:
Example 342. 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:
Example 343. Modifying the Product
entity with a dynamic update
UPDATE
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.
Example 344. Refreshing entity state with JPA
Person person = entityManager.find( Person.class, personId );
entityManager.createQuery( "update Person set name = UPPER(name)" ).executeUpdate();
entityManager.refresh( person );
assertEquals("JOHN DOE", person.getName() );
Example 345. Refreshing entity state with Hibernate API
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, JPA 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:
Example 346. Refreshing entity state gotcha
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.
JPA does not support reattaching detached data. This is only available through Hibernate |
Example 347. Reattaching a detached entity using 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 );
Example 348. Reattaching a detached entity using 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.saveOrUpdate( 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.
Example 349. Visualizing merge
public Person merge(Person detached) {
Person newReference = session.byId( Person.class ).load( detached.getId() );
newReference.setName( detached.getName() );
return newReference;
}
Example 350. Merging a detached entity with JPA
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 );
Example 351. Merging a detached entity with Hibernate API
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 JPA |
5.13. Checking persistent state
An application can verify the state of entities and collections in relation to the persistence context.
Example 352. Verifying managed state with JPA
boolean contained = entityManager.contains( person );
Example 353. Verifying managed state with Hibernate API
boolean contained = session.contains( person );
Example 354. Verifying laziness with JPA
PersistenceUnitUtil persistenceUnitUtil = entityManager.getEntityManagerFactory().getPersistenceUnitUtil();
boolean personInitialized = persistenceUnitUtil.isLoaded( person );
boolean personBooksInitialized = persistenceUnitUtil.isLoaded( person.getBooks() );
boolean personNameInitialized = persistenceUnitUtil.isLoaded( person, "name" );
Example 355. Verifying laziness with Hibernate API
boolean personInitialized = Hibernate.isInitialized( person );
boolean personBooksInitialized = Hibernate.isInitialized( person.getBooks() );
boolean personNameInitialized = Hibernate.isPropertyInitialized( person, "name" );
In JPA there is an alternative means to check laziness using the following javax.persistence.PersistenceUtil
pattern (which is recommended wherever possible).
Example 356. Alternative JPA means to verify laziness
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.
Example 357. Detaching an entity from the EntityManager
for(Person person : entityManager.createQuery("select p from Person p", Person.class)
.getResultList()) {
dtos.add(toDTO(person));
entityManager.detach( person );
}
Example 358. Evicting an entity from the Hibernate Session
Session session = entityManager.unwrap( Session.class );
for(Person person : (List<Person>) session.createQuery("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.
Example 359. Clearing the persistence context
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.
Example 360. Verify if an entity is contained in a persistence context
entityManager.contains( person );
session.contains( person );
5.15. Cascading entity state transitions
JPA allows you to propagate the state transition from a parent entity to a child. For this purpose, the JPA javax.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.
Example 361. CascadeType.PERSIST
example
Person 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.
Example 362. CascadeType.MERGE
example
Phone 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.
Example 363. CascadeType.REMOVE
example
Person 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.
Example 364. CascadeType.DETACH
example
Person 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 javax.persistence.lock.scope
property.
However, CascadeType.LOCK
allows us to reattach a parent entity along with its children to the currently running Persistence Context.
Example 365. CascadeType.LOCK
example
Person 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 )
.buildLockRequest( new LockOptions( LockMode.NONE ) )
.lock( person );
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.
Example 366. CascadeType.REFRESH
example
Person 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.
Example 367. CascadeType.REPLICATE
example
Person 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.
Example 368. @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 = FetchType.LAZY)
@OnDelete( action = OnDeleteAction.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.
Example 369. @OnDelete
@ManyToOne
delete example
Person 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.
Example 370. @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.
Example 371. @OnDelete
@ManyToOne
delete example
Person 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 JPA 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 JPA 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 JPA 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.
However, it is also possible to plug in a custom implementation. See the hibernate.jdbc.sql_exception_converter
configuration property for more details.
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 |