Skip to content

Commit

Permalink
HHH-12944 - MultiIdentifierLoadAccess ignores the 2nd level cache
Browse files Browse the repository at this point in the history
  • Loading branch information
barnabycourt authored and vladmihalcea committed Oct 31, 2018
1 parent ac03494 commit 512dfa5
Show file tree
Hide file tree
Showing 10 changed files with 1,012 additions and 366 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,110 @@ include::{sourcedir}/PersistenceContextTest.java[tags=pc-find-optional-by-id-nat
----
====

[[pc-by-multiple-ids]]
=== 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
https://docs.jboss.org/hibernate/orm/{majorMinorVersion}/javadocs/org/hibernate/Session.html#byMultipleIds-java.lang.Class-[`byMultipleIds` method] of the Hibernate `Session`.

The `byMultipleIds` method returns a
https://docs.jboss.org/hibernate/orm/{majorMinorVersion}/javadocs/org/hibernate/MultiIdentifierLoadAccess.html[`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
https://docs.jboss.org/hibernate/orm/{majorMinorVersion}/javadocs/org/hibernate/LockOptions.html[`LockOptions`] mode to the multi-load query.
`with(CacheMode cacheMode)`::
This setting allows you to pass a given
https://docs.jboss.org/hibernate/orm/{majorMinorVersion}/javadocs/org/hibernate/CacheMode.html[`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:

[[tag::pc-by-multiple-ids-example]]
.Loading multiple entities using the `byMultipleIds()` Hibernate API
====
[source, JAVA, indent=0]
----
include::{sourcedir}/MultiLoadIdTest.java[tags=pc-by-multiple-ids-example]
----
[source, SQL, indent=0]
----
include::{extrasdir}/pc-by-multiple-ids-example.sql[]
----
====

Notice that only one SQL SELECT statement was executed since the second call uses the
https://docs.jboss.org/hibernate/orm/{majorMinorVersion}/javadocs/org/hibernate/MultiIdentifierLoadAccess.html#enableSessionCheck-boolean-[`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
https://docs.jboss.org/hibernate/orm/{majorMinorVersion}/javadocs/org/hibernate/MultiIdentifierLoadAccess.html#with-org.hibernate.CacheMode-[`with(CacheMode)`] method of the `MultiIdentifierLoadAccess` object.

[[tag::pc-by-multiple-ids-second-level-cache-example]]
.Loading multiple entities from the second-level cache
====
[source, JAVA, indent=0]
----
include::{sourcedir}/MultiLoadIdTest.java[tags=pc-by-multiple-ids-second-level-cache-example]
----
====

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
https://docs.jboss.org/hibernate/orm/{majorMinorVersion}/javadocs/org/hibernate/CacheMode.html#NORMAL[`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.

[[pc-find-natural-id]]
=== Obtain an entity by natural-id

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
SELECT p.id AS id1_0_0_,
p.name AS name2_0_0_
FROM Person p
WHERE p.id IN ( 1, 2, 3 )
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
/*
* Hibernate, Relational Persistence for Idiomatic Java
*
* License: GNU Lesser General Public License (LGPL), version 2.1 or later.
* See the lgpl.txt file in the root directory or <http://www.gnu.org/licenses/lgpl-2.1.html>.
*/
package org.hibernate.userguide.pc;

import java.util.List;
import java.util.Map;
import javax.persistence.Cacheable;
import javax.persistence.Entity;
import javax.persistence.Id;

import org.hibernate.CacheMode;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.hibernate.annotations.Cache;
import org.hibernate.annotations.CacheConcurrencyStrategy;
import org.hibernate.boot.SessionFactoryBuilder;
import org.hibernate.cfg.AvailableSettings;
import org.hibernate.jpa.test.BaseEntityManagerFunctionalTestCase;
import org.hibernate.stat.Statistics;

import org.hibernate.testing.jdbc.SQLStatementInterceptor;
import org.junit.Test;

import static org.hibernate.testing.transaction.TransactionUtil.doInJPA;
import static org.junit.Assert.assertEquals;

/**
* @author Vlad Mihalcea
*/
public class MultiLoadIdTest extends BaseEntityManagerFunctionalTestCase {

private SQLStatementInterceptor sqlStatementInterceptor;

@Override
protected Class<?>[] getAnnotatedClasses() {
return new Class<?>[] {
Person.class,
};
}

@Override
protected void addMappings(Map settings) {
settings.put( AvailableSettings.USE_SECOND_LEVEL_CACHE, true );
settings.put( AvailableSettings.CACHE_REGION_FACTORY, "jcache" );
settings.put( AvailableSettings.GENERATE_STATISTICS, Boolean.TRUE.toString() );
sqlStatementInterceptor = new SQLStatementInterceptor( settings );
}

@Override
protected void afterEntityManagerFactoryBuilt() {
doInJPA( this::entityManagerFactory, entityManager -> {

Person person1 = new Person();
person1.setId( 1L );
person1.setName("John Doe Sr.");

entityManager.persist( person1 );

Person person2 = new Person();
person2.setId( 2L );
person2.setName("John Doe");

entityManager.persist( person2 );

Person person3 = new Person();
person3.setId( 3L );
person3.setName("John Doe Jr.");

entityManager.persist( person3 );
} );
}

@Test
public void testSessionCheck() {
doInJPA( this::entityManagerFactory, entityManager -> {
//tag::pc-by-multiple-ids-example[]
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 );
//end::pc-by-multiple-ids-example[]
} );
}

@Test
public void testSecondLevelCacheCheck() {
//tag::pc-by-multiple-ids-second-level-cache-example[]
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() );
//end::pc-by-multiple-ids-second-level-cache-example[]
}

@Entity(name = "Person")
@Cache(usage = CacheConcurrencyStrategy.READ_WRITE)
public static class Person {

@Id
private Long id;

private String name;

public Long getId() {
return id;
}

public void setId(Long id) {
this.id = id;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ default MultiIdentifierLoadAccess<T> with(RootGraph<T> graph) {
MultiIdentifierLoadAccess<T> withBatchSize(int batchSize);

/**
* Specify whether we should check the Session to see whether it already contains any of the
* Specify whether we should check the {@link Session} to see whether the first-level cache already contains any of the
* entities to be loaded in a managed state <b>for the purpose of not including those
* ids to the batch-load SQL</b>.
*
Expand Down
Loading

0 comments on commit 512dfa5

Please sign in to comment.