HHH-12944 - MultiIdentifierLoadAccess ignores the 2nd level cache

This commit is contained in:
Barnaby Court 2018-09-03 12:04:49 -04:00 committed by Vlad Mihalcea
parent ac03494e70
commit 512dfa574d
10 changed files with 1012 additions and 366 deletions

View File

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

View File

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

View File

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

View File

@ -59,7 +59,7 @@ public interface MultiIdentifierLoadAccess<T> {
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>.
*

View File

@ -13,42 +13,26 @@ import org.hibernate.LockMode;
import org.hibernate.NonUniqueObjectException;
import org.hibernate.PersistentObjectException;
import org.hibernate.TypeMismatchException;
import org.hibernate.WrongClassException;
import org.hibernate.action.internal.DelayedPostInsertIdentifier;
import org.hibernate.cache.spi.access.EntityDataAccess;
import org.hibernate.cache.spi.access.SoftLock;
import org.hibernate.cache.spi.entry.CacheEntry;
import org.hibernate.cache.spi.entry.ReferenceCacheEntryImpl;
import org.hibernate.cache.spi.entry.StandardCacheEntryImpl;
import org.hibernate.engine.internal.CacheHelper;
import org.hibernate.engine.internal.StatefulPersistenceContext;
import org.hibernate.engine.internal.TwoPhaseLoad;
import org.hibernate.engine.internal.Versioning;
import org.hibernate.engine.spi.EntityEntry;
import org.hibernate.engine.spi.EntityKey;
import org.hibernate.engine.spi.ManagedEntity;
import org.hibernate.engine.spi.PersistenceContext;
import org.hibernate.engine.spi.SessionFactoryImplementor;
import org.hibernate.engine.spi.SessionImplementor;
import org.hibernate.engine.spi.Status;
import org.hibernate.event.service.spi.EventListenerRegistry;
import org.hibernate.event.spi.EventSource;
import org.hibernate.event.spi.EventType;
import org.hibernate.event.spi.LoadEvent;
import org.hibernate.event.spi.LoadEventListener;
import org.hibernate.event.spi.PostLoadEvent;
import org.hibernate.event.spi.PostLoadEventListener;
import org.hibernate.internal.CoreLogging;
import org.hibernate.internal.CoreMessageLogger;
import org.hibernate.loader.entity.CacheEntityLoaderHelper;
import org.hibernate.persister.entity.EntityPersister;
import org.hibernate.pretty.MessageHelper;
import org.hibernate.proxy.HibernateProxy;
import org.hibernate.proxy.LazyInitializer;
import org.hibernate.stat.internal.StatsHelper;
import org.hibernate.type.EmbeddedComponentType;
import org.hibernate.type.EntityType;
import org.hibernate.type.Type;
import org.hibernate.type.TypeHelper;
/**
* Defines the default load event listeners used by hibernate for loading entities
@ -56,10 +40,7 @@ import org.hibernate.type.TypeHelper;
*
* @author Steve Ebersole
*/
public class DefaultLoadEventListener extends AbstractLockUpgradeEventListener implements LoadEventListener {
public static final Object REMOVED_ENTITY_MARKER = new Object();
public static final Object INCONSISTENT_RTN_CLASS_MARKER = new Object();
public static final LockMode DEFAULT_LOCK_MODE = LockMode.NONE;
public class DefaultLoadEventListener implements LoadEventListener {
private static final CoreMessageLogger LOG = CoreLogging.messageLogger( DefaultLoadEventListener.class );
@ -441,28 +422,18 @@ public class DefaultLoadEventListener extends AbstractLockUpgradeEventListener i
);
}
Object entity = loadFromSessionCache( event, keyToLoad, options );
if ( entity == REMOVED_ENTITY_MARKER ) {
LOG.debug( "Load request found matching entity in context, but it is scheduled for removal; returning null" );
return null;
}
if ( entity == INCONSISTENT_RTN_CLASS_MARKER ) {
LOG.debug(
"Load request found matching entity in context, but the matched entity was of an inconsistent return type; returning null"
);
return null;
}
CacheEntityLoaderHelper.PersistenceContextEntry persistenceContextEntry = CacheEntityLoaderHelper.INSTANCE.loadFromSessionCache(
event,
keyToLoad,
options
);
Object entity = persistenceContextEntry.getEntity();
if ( entity != null ) {
if ( traceEnabled ) {
LOG.tracev(
"Resolved object in session cache: {0}",
MessageHelper.infoString( persister, event.getEntityId(), event.getSession().getFactory() )
);
}
return entity;
return persistenceContextEntry.isManaged() ? entity : null;
}
entity = loadFromSecondLevelCache( event, persister, keyToLoad );
entity = CacheEntityLoaderHelper.INSTANCE.loadFromSecondLevelCache( event, persister, keyToLoad );
if ( entity != null ) {
if ( traceEnabled ) {
LOG.tracev(
@ -522,302 +493,4 @@ public class DefaultLoadEventListener extends AbstractLockUpgradeEventListener i
return entity;
}
/**
* Attempts to locate the entity in the session-level cache.
* <p/>
* If allowed to return nulls, then if the entity happens to be found in
* the session cache, we check the entity type for proper handling
* of entity hierarchies.
* <p/>
* If checkDeleted was set to true, then if the entity is found in the
* session-level cache, it's current status within the session cache
* is checked to see if it has previously been scheduled for deletion.
*
* @param event The load event
* @param keyToLoad The EntityKey representing the entity to be loaded.
* @param options The load options.
*
* @return The entity from the session-level cache, or null.
*
* @throws HibernateException Generally indicates problems applying a lock-mode.
*/
protected Object loadFromSessionCache(
final LoadEvent event,
final EntityKey keyToLoad,
final LoadEventListener.LoadType options) throws HibernateException {
SessionImplementor session = event.getSession();
Object old = session.getEntityUsingInterceptor( keyToLoad );
if ( old != null ) {
// this object was already loaded
EntityEntry oldEntry = session.getPersistenceContext().getEntry( old );
if ( options.isCheckDeleted() ) {
Status status = oldEntry.getStatus();
if ( status == Status.DELETED || status == Status.GONE ) {
return REMOVED_ENTITY_MARKER;
}
}
if ( options.isAllowNulls() ) {
final EntityPersister persister = event.getSession()
.getFactory()
.getEntityPersister( keyToLoad.getEntityName() );
if ( !persister.isInstance( old ) ) {
return INCONSISTENT_RTN_CLASS_MARKER;
}
}
upgradeLock( old, oldEntry, event.getLockOptions(), event.getSession() );
}
return old;
}
/**
* Attempts to load the entity from the second-level cache.
*
* @param event The load event
* @param persister The persister for the entity being requested for load
*
* @return The entity from the second-level cache, or null.
*/
private Object loadFromSecondLevelCache(
final LoadEvent event,
final EntityPersister persister,
final EntityKey entityKey) {
final SessionImplementor source = event.getSession();
final boolean useCache = persister.canReadFromCache()
&& source.getCacheMode().isGetEnabled()
&& event.getLockMode().lessThan( LockMode.READ );
if ( !useCache ) {
// we can't use cache here
return null;
}
final Object ce = getFromSharedCache( event, persister, source );
if ( ce == null ) {
// nothing was found in cache
return null;
}
return processCachedEntry( event, persister, ce, source, entityKey );
}
private Object processCachedEntry(
final LoadEvent event,
final EntityPersister persister,
final Object ce,
final SessionImplementor source,
final EntityKey entityKey) {
CacheEntry entry = (CacheEntry) persister.getCacheEntryStructure().destructure( ce, source.getFactory() );
if(entry.isReferenceEntry()) {
if( event.getInstanceToLoad() != null ) {
throw new HibernateException(
"Attempt to load entity [%s] from cache using provided object instance, but cache " +
"is storing references: "+ event.getEntityId());
}
else {
return convertCacheReferenceEntryToEntity( (ReferenceCacheEntryImpl) entry,
event.getSession(), entityKey);
}
}
else {
Object entity = convertCacheEntryToEntity( entry, event.getEntityId(), persister, event, entityKey );
if ( !persister.isInstance( entity ) ) {
throw new WrongClassException(
"loaded object was of wrong class " + entity.getClass(),
event.getEntityId(),
persister.getEntityName()
);
}
return entity;
}
}
private Object getFromSharedCache(
final LoadEvent event,
final EntityPersister persister,
SessionImplementor source ) {
final EntityDataAccess cache = persister.getCacheAccessStrategy();
final Object ck = cache.generateCacheKey(
event.getEntityId(),
persister,
source.getFactory(),
source.getTenantIdentifier()
);
final Object ce = CacheHelper.fromSharedCache( source, ck, persister.getCacheAccessStrategy() );
if ( source.getFactory().getStatistics().isStatisticsEnabled() ) {
if ( ce == null ) {
source.getFactory().getStatistics().entityCacheMiss(
StatsHelper.INSTANCE.getRootEntityRole( persister ),
cache.getRegion().getName()
);
}
else {
source.getFactory().getStatistics().entityCacheHit(
StatsHelper.INSTANCE.getRootEntityRole( persister ),
cache.getRegion().getName()
);
}
}
return ce;
}
private Object convertCacheReferenceEntryToEntity(
ReferenceCacheEntryImpl referenceCacheEntry,
EventSource session,
EntityKey entityKey) {
final Object entity = referenceCacheEntry.getReference();
if ( entity == null ) {
throw new IllegalStateException(
"Reference cache entry contained null : " + referenceCacheEntry.toString());
}
else {
makeEntityCircularReferenceSafe( referenceCacheEntry, session, entity, entityKey );
return entity;
}
}
private void makeEntityCircularReferenceSafe(ReferenceCacheEntryImpl referenceCacheEntry,
EventSource session,
Object entity,
EntityKey entityKey) {
// make it circular-reference safe
final StatefulPersistenceContext statefulPersistenceContext = (StatefulPersistenceContext) session.getPersistenceContext();
if ( (entity instanceof ManagedEntity) ) {
statefulPersistenceContext.addReferenceEntry(
entity,
Status.READ_ONLY
);
}
else {
TwoPhaseLoad.addUninitializedCachedEntity(
entityKey,
entity,
referenceCacheEntry.getSubclassPersister(),
LockMode.NONE,
referenceCacheEntry.getVersion(),
session
);
}
statefulPersistenceContext.initializeNonLazyCollections();
}
private Object convertCacheEntryToEntity(
CacheEntry entry,
Serializable entityId,
EntityPersister persister,
LoadEvent event,
EntityKey entityKey) {
final EventSource session = event.getSession();
final SessionFactoryImplementor factory = session.getFactory();
final EntityPersister subclassPersister;
if ( traceEnabled ) {
LOG.tracef(
"Converting second-level cache entry [%s] into entity : %s",
entry,
MessageHelper.infoString( persister, entityId, factory )
);
}
final Object entity;
subclassPersister = factory.getEntityPersister( entry.getSubclass() );
final Object optionalObject = event.getInstanceToLoad();
entity = optionalObject == null
? session.instantiate( subclassPersister, entityId )
: optionalObject;
// make it circular-reference safe
TwoPhaseLoad.addUninitializedCachedEntity(
entityKey,
entity,
subclassPersister,
LockMode.NONE,
entry.getVersion(),
session
);
final PersistenceContext persistenceContext = session.getPersistenceContext();
final Object[] values;
final Object version;
final boolean isReadOnly;
final Type[] types = subclassPersister.getPropertyTypes();
// initializes the entity by (desired) side-effect
values = ( (StandardCacheEntryImpl) entry ).assemble(
entity, entityId, subclassPersister, session.getInterceptor(), session
);
if ( ( (StandardCacheEntryImpl) entry ).isDeepCopyNeeded() ) {
TypeHelper.deepCopy(
values,
types,
subclassPersister.getPropertyUpdateability(),
values,
session
);
}
version = Versioning.getVersion( values, subclassPersister );
LOG.tracef( "Cached Version : %s", version );
final Object proxy = persistenceContext.getProxy( entityKey );
if ( proxy != null ) {
// there is already a proxy for this impl
// only set the status to read-only if the proxy is read-only
isReadOnly = ( (HibernateProxy) proxy ).getHibernateLazyInitializer().isReadOnly();
}
else {
isReadOnly = session.isDefaultReadOnly();
}
persistenceContext.addEntry(
entity,
( isReadOnly ? Status.READ_ONLY : Status.MANAGED ),
values,
null,
entityId,
version,
LockMode.NONE,
true,
subclassPersister,
false
);
subclassPersister.afterInitialize( entity, session );
persistenceContext.initializeNonLazyCollections();
//PostLoad is needed for EJB3
PostLoadEvent postLoadEvent = event.getPostLoadEvent()
.setEntity( entity )
.setId( entityId )
.setPersister( persister );
for ( PostLoadEventListener listener : postLoadEventListeners( session ) ) {
listener.onPostLoad( postLoadEvent );
}
return entity;
}
private Iterable<PostLoadEventListener> postLoadEventListeners(EventSource session) {
return session
.getFactory()
.getServiceRegistry()
.getService( EventListenerRegistry.class )
.getEventListenerGroup( EventType.POST_LOAD )
.listeners();
}
}

View File

@ -2969,6 +2969,11 @@ public final class SessionImpl
return sessionCheckingEnabled;
}
@Override
public boolean isSecondLevelCacheCheckingEnabled() {
return cacheMode == CacheMode.NORMAL || cacheMode == CacheMode.GET;
}
@Override
public MultiIdentifierLoadAccess<T> enableSessionCheck(boolean enabled) {
this.sessionCheckingEnabled = enabled;

View File

@ -0,0 +1,393 @@
/*
* 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.loader.entity;
import java.io.Serializable;
import org.hibernate.HibernateException;
import org.hibernate.LockMode;
import org.hibernate.WrongClassException;
import org.hibernate.cache.spi.access.EntityDataAccess;
import org.hibernate.cache.spi.entry.CacheEntry;
import org.hibernate.cache.spi.entry.ReferenceCacheEntryImpl;
import org.hibernate.cache.spi.entry.StandardCacheEntryImpl;
import org.hibernate.engine.internal.CacheHelper;
import org.hibernate.engine.internal.StatefulPersistenceContext;
import org.hibernate.engine.internal.TwoPhaseLoad;
import org.hibernate.engine.internal.Versioning;
import org.hibernate.engine.spi.EntityEntry;
import org.hibernate.engine.spi.EntityKey;
import org.hibernate.engine.spi.ManagedEntity;
import org.hibernate.engine.spi.PersistenceContext;
import org.hibernate.engine.spi.SessionFactoryImplementor;
import org.hibernate.engine.spi.SessionImplementor;
import org.hibernate.engine.spi.Status;
import org.hibernate.event.internal.AbstractLockUpgradeEventListener;
import org.hibernate.event.service.spi.EventListenerRegistry;
import org.hibernate.event.spi.EventSource;
import org.hibernate.event.spi.EventType;
import org.hibernate.event.spi.LoadEvent;
import org.hibernate.event.spi.LoadEventListener;
import org.hibernate.event.spi.PostLoadEvent;
import org.hibernate.event.spi.PostLoadEventListener;
import org.hibernate.internal.CoreLogging;
import org.hibernate.internal.CoreMessageLogger;
import org.hibernate.persister.entity.EntityPersister;
import org.hibernate.pretty.MessageHelper;
import org.hibernate.proxy.HibernateProxy;
import org.hibernate.stat.internal.StatsHelper;
import org.hibernate.type.Type;
import org.hibernate.type.TypeHelper;
/**
* @author Vlad Mihalcea
*/
public class CacheEntityLoaderHelper extends AbstractLockUpgradeEventListener {
public static final CacheEntityLoaderHelper INSTANCE = new CacheEntityLoaderHelper();
private static final CoreMessageLogger LOG = CoreLogging.messageLogger( CacheEntityLoaderHelper.class );
private static final boolean traceEnabled = LOG.isTraceEnabled();
public enum EntityStatus {
MANAGED,
REMOVED_ENTITY_MARKER,
INCONSISTENT_RTN_CLASS_MARKER
}
private CacheEntityLoaderHelper() {
}
/**
* Attempts to locate the entity in the session-level cache.
* <p/>
* If allowed to return nulls, then if the entity happens to be found in
* the session cache, we check the entity type for proper handling
* of entity hierarchies.
* <p/>
* If checkDeleted was set to true, then if the entity is found in the
* session-level cache, it's current status within the session cache
* is checked to see if it has previously been scheduled for deletion.
*
* @param event The load event
* @param keyToLoad The EntityKey representing the entity to be loaded.
* @param options The load options.
*
* @return The entity from the session-level cache, or null.
*
* @throws HibernateException Generally indicates problems applying a lock-mode.
*/
public PersistenceContextEntry loadFromSessionCache(
final LoadEvent event,
final EntityKey keyToLoad,
final LoadEventListener.LoadType options) throws HibernateException {
SessionImplementor session = event.getSession();
Object old = session.getEntityUsingInterceptor( keyToLoad );
if ( old != null ) {
// this object was already loaded
EntityEntry oldEntry = session.getPersistenceContext().getEntry( old );
if ( options.isCheckDeleted() ) {
Status status = oldEntry.getStatus();
if ( status == Status.DELETED || status == Status.GONE ) {
LOG.debug(
"Load request found matching entity in context, but it is scheduled for removal; returning null" );
return new PersistenceContextEntry( old, EntityStatus.REMOVED_ENTITY_MARKER );
}
}
if ( options.isAllowNulls() ) {
final EntityPersister persister = event.getSession()
.getFactory()
.getEntityPersister( keyToLoad.getEntityName() );
if ( !persister.isInstance( old ) ) {
LOG.debug(
"Load request found matching entity in context, but the matched entity was of an inconsistent return type; returning null"
);
return new PersistenceContextEntry( old, EntityStatus.INCONSISTENT_RTN_CLASS_MARKER );
}
}
upgradeLock( old, oldEntry, event.getLockOptions(), event.getSession() );
}
return new PersistenceContextEntry( old, EntityStatus.MANAGED );
}
/**
* Attempts to load the entity from the second-level cache.
*
* @param event The load event
* @param persister The persister for the entity being requested for load
*
* @return The entity from the second-level cache, or null.
*/
public Object loadFromSecondLevelCache(
final LoadEvent event,
final EntityPersister persister,
final EntityKey entityKey) {
final SessionImplementor source = event.getSession();
final boolean useCache = persister.canReadFromCache()
&& source.getCacheMode().isGetEnabled()
&& event.getLockMode().lessThan( LockMode.READ );
if ( !useCache ) {
// we can't use cache here
return null;
}
final Object ce = getFromSharedCache( event, persister, source );
if ( ce == null ) {
// nothing was found in cache
return null;
}
return processCachedEntry( event, persister, ce, source, entityKey );
}
private Object processCachedEntry(
final LoadEvent event,
final EntityPersister persister,
final Object ce,
final SessionImplementor source,
final EntityKey entityKey) {
CacheEntry entry = (CacheEntry) persister.getCacheEntryStructure().destructure( ce, source.getFactory() );
if ( entry.isReferenceEntry() ) {
if ( event.getInstanceToLoad() != null ) {
throw new HibernateException(
"Attempt to load entity [%s] from cache using provided object instance, but cache " +
"is storing references: " + event.getEntityId() );
}
else {
return convertCacheReferenceEntryToEntity(
(ReferenceCacheEntryImpl) entry,
event.getSession(),
entityKey
);
}
}
else {
Object entity = convertCacheEntryToEntity( entry, event.getEntityId(), persister, event, entityKey );
if ( !persister.isInstance( entity ) ) {
throw new WrongClassException(
"loaded object was of wrong class " + entity.getClass(),
event.getEntityId(),
persister.getEntityName()
);
}
return entity;
}
}
private Object getFromSharedCache(
final LoadEvent event,
final EntityPersister persister,
SessionImplementor source) {
final EntityDataAccess cache = persister.getCacheAccessStrategy();
final Object ck = cache.generateCacheKey(
event.getEntityId(),
persister,
source.getFactory(),
source.getTenantIdentifier()
);
final Object ce = CacheHelper.fromSharedCache( source, ck, persister.getCacheAccessStrategy() );
if ( source.getFactory().getStatistics().isStatisticsEnabled() ) {
if ( ce == null ) {
source.getFactory().getStatistics().entityCacheMiss(
StatsHelper.INSTANCE.getRootEntityRole( persister ),
cache.getRegion().getName()
);
}
else {
source.getFactory().getStatistics().entityCacheHit(
StatsHelper.INSTANCE.getRootEntityRole( persister ),
cache.getRegion().getName()
);
}
}
return ce;
}
private Object convertCacheReferenceEntryToEntity(
ReferenceCacheEntryImpl referenceCacheEntry,
EventSource session,
EntityKey entityKey) {
final Object entity = referenceCacheEntry.getReference();
if ( entity == null ) {
throw new IllegalStateException(
"Reference cache entry contained null : " + referenceCacheEntry.toString() );
}
else {
makeEntityCircularReferenceSafe( referenceCacheEntry, session, entity, entityKey );
return entity;
}
}
private void makeEntityCircularReferenceSafe(
ReferenceCacheEntryImpl referenceCacheEntry,
EventSource session,
Object entity,
EntityKey entityKey) {
// make it circular-reference safe
final StatefulPersistenceContext statefulPersistenceContext = (StatefulPersistenceContext) session.getPersistenceContext();
if ( ( entity instanceof ManagedEntity ) ) {
statefulPersistenceContext.addReferenceEntry(
entity,
Status.READ_ONLY
);
}
else {
TwoPhaseLoad.addUninitializedCachedEntity(
entityKey,
entity,
referenceCacheEntry.getSubclassPersister(),
LockMode.NONE,
referenceCacheEntry.getVersion(),
session
);
}
statefulPersistenceContext.initializeNonLazyCollections();
}
private Object convertCacheEntryToEntity(
CacheEntry entry,
Serializable entityId,
EntityPersister persister,
LoadEvent event,
EntityKey entityKey) {
final EventSource session = event.getSession();
final SessionFactoryImplementor factory = session.getFactory();
final EntityPersister subclassPersister;
if ( traceEnabled ) {
LOG.tracef(
"Converting second-level cache entry [%s] into entity : %s",
entry,
MessageHelper.infoString( persister, entityId, factory )
);
}
final Object entity;
subclassPersister = factory.getEntityPersister( entry.getSubclass() );
final Object optionalObject = event.getInstanceToLoad();
entity = optionalObject == null
? session.instantiate( subclassPersister, entityId )
: optionalObject;
// make it circular-reference safe
TwoPhaseLoad.addUninitializedCachedEntity(
entityKey,
entity,
subclassPersister,
LockMode.NONE,
entry.getVersion(),
session
);
final PersistenceContext persistenceContext = session.getPersistenceContext();
final Object[] values;
final Object version;
final boolean isReadOnly;
final Type[] types = subclassPersister.getPropertyTypes();
// initializes the entity by (desired) side-effect
values = ( (StandardCacheEntryImpl) entry ).assemble(
entity, entityId, subclassPersister, session.getInterceptor(), session
);
if ( ( (StandardCacheEntryImpl) entry ).isDeepCopyNeeded() ) {
TypeHelper.deepCopy(
values,
types,
subclassPersister.getPropertyUpdateability(),
values,
session
);
}
version = Versioning.getVersion( values, subclassPersister );
LOG.tracef( "Cached Version : %s", version );
final Object proxy = persistenceContext.getProxy( entityKey );
if ( proxy != null ) {
// there is already a proxy for this impl
// only set the status to read-only if the proxy is read-only
isReadOnly = ( (HibernateProxy) proxy ).getHibernateLazyInitializer().isReadOnly();
}
else {
isReadOnly = session.isDefaultReadOnly();
}
persistenceContext.addEntry(
entity,
( isReadOnly ? Status.READ_ONLY : Status.MANAGED ),
values,
null,
entityId,
version,
LockMode.NONE,
true,
subclassPersister,
false
);
subclassPersister.afterInitialize( entity, session );
persistenceContext.initializeNonLazyCollections();
//PostLoad is needed for EJB3
PostLoadEvent postLoadEvent = event.getPostLoadEvent()
.setEntity( entity )
.setId( entityId )
.setPersister( persister );
for ( PostLoadEventListener listener : postLoadEventListeners( session ) ) {
listener.onPostLoad( postLoadEvent );
}
return entity;
}
private Iterable<PostLoadEventListener> postLoadEventListeners(EventSource session) {
return session
.getFactory()
.getServiceRegistry()
.getService( EventListenerRegistry.class )
.getEventListenerGroup( EventType.POST_LOAD )
.listeners();
}
public static class PersistenceContextEntry {
private final Object entity;
private EntityStatus status;
public PersistenceContextEntry(Object entity, EntityStatus status) {
this.entity = entity;
this.status = status;
}
public Object getEntity() {
return entity;
}
public EntityStatus getStatus() {
return status;
}
public boolean isManaged() {
return EntityStatus.MANAGED == status;
}
}
}

View File

@ -28,6 +28,9 @@ import org.hibernate.engine.spi.RowSelection;
import org.hibernate.engine.spi.SessionFactoryImplementor;
import org.hibernate.engine.spi.SharedSessionContractImplementor;
import org.hibernate.engine.spi.Status;
import org.hibernate.event.spi.EventSource;
import org.hibernate.event.spi.LoadEvent;
import org.hibernate.event.spi.LoadEventListener;
import org.hibernate.internal.util.StringHelper;
import org.hibernate.internal.util.collections.ArrayHelper;
import org.hibernate.internal.util.collections.CollectionHelper;
@ -95,20 +98,44 @@ public class DynamicBatchingEntityLoaderBuilder extends BatchingEntityLoaderBuil
final Serializable id = ids[i];
final EntityKey entityKey = new EntityKey( id, persister );
if ( loadOptions.isSessionCheckingEnabled() ) {
// look for it in the Session first
final Object managedEntity = session.getPersistenceContext().getEntity( entityKey );
if ( managedEntity != null ) {
if ( !loadOptions.isReturnOfDeletedEntitiesEnabled() ) {
final EntityEntry entry = session.getPersistenceContext().getEntry( managedEntity );
if ( entry.getStatus() == Status.DELETED || entry.getStatus() == Status.GONE ) {
// put a null in the result
result.add( i, null );
continue;
}
if ( loadOptions.isSessionCheckingEnabled() || loadOptions.isSecondLevelCacheCheckingEnabled() ) {
LoadEvent loadEvent = new LoadEvent(
id,
persister.getMappedClass().getName(),
lockOptions,
(EventSource) session
);
Object managedEntity = null;
if ( loadOptions.isSessionCheckingEnabled() ) {
// look for it in the Session first
CacheEntityLoaderHelper.PersistenceContextEntry persistenceContextEntry = CacheEntityLoaderHelper.INSTANCE
.loadFromSessionCache(
loadEvent,
entityKey,
LoadEventListener.GET
);
managedEntity = persistenceContextEntry.getEntity();
if ( managedEntity != null && !loadOptions.isReturnOfDeletedEntitiesEnabled() && !persistenceContextEntry
.isManaged() ) {
// put a null in the result
result.add( i, null );
continue;
}
// if we did not hit the continue above, there is already an
// entry in the PC for that entity, so use it...
}
if ( managedEntity == null && loadOptions.isSecondLevelCacheCheckingEnabled() ) {
// look for it in the SessionFactory
managedEntity = CacheEntityLoaderHelper.INSTANCE.loadFromSecondLevelCache(
loadEvent,
persister,
entityKey
);
}
if ( managedEntity != null ) {
result.add( i, managedEntity );
continue;
}
@ -182,7 +209,11 @@ public class DynamicBatchingEntityLoaderBuilder extends BatchingEntityLoaderBuil
final List result = CollectionHelper.arrayList( ids.length );
if ( loadOptions.isSessionCheckingEnabled() ) {
final LockOptions lockOptions = (loadOptions.getLockOptions() == null)
? new LockOptions( LockMode.NONE )
: loadOptions.getLockOptions();
if ( loadOptions.isSessionCheckingEnabled() || loadOptions.isSecondLevelCacheCheckingEnabled() ) {
// the user requested that we exclude ids corresponding to already managed
// entities from the generated load SQL. So here we will iterate all
// incoming id values and see whether it corresponds to an existing
@ -192,14 +223,43 @@ public class DynamicBatchingEntityLoaderBuilder extends BatchingEntityLoaderBuil
final List<Serializable> nonManagedIds = new ArrayList<Serializable>();
for ( Serializable id : ids ) {
final EntityKey entityKey = new EntityKey( id, persister );
final Object managedEntity = session.getPersistenceContext().getEntity( entityKey );
if ( managedEntity != null ) {
if ( !loadOptions.isReturnOfDeletedEntitiesEnabled() ) {
final EntityEntry entry = session.getPersistenceContext().getEntry( managedEntity );
if ( entry.getStatus() == Status.DELETED || entry.getStatus() == Status.GONE ) {
continue;
}
LoadEvent loadEvent = new LoadEvent(
id,
persister.getMappedClass().getName(),
lockOptions,
(EventSource) session
);
Object managedEntity = null;
// look for it in the Session first
CacheEntityLoaderHelper.PersistenceContextEntry persistenceContextEntry = CacheEntityLoaderHelper.INSTANCE
.loadFromSessionCache(
loadEvent,
entityKey,
LoadEventListener.GET
);
if ( loadOptions.isSessionCheckingEnabled() ) {
managedEntity = persistenceContextEntry.getEntity();
if ( managedEntity != null && !loadOptions.isReturnOfDeletedEntitiesEnabled() && !persistenceContextEntry
.isManaged() ) {
foundAnyManagedEntities = true;
result.add( null );
continue;
}
}
if ( managedEntity == null && loadOptions.isSecondLevelCacheCheckingEnabled() ) {
managedEntity = CacheEntityLoaderHelper.INSTANCE.loadFromSecondLevelCache(
loadEvent,
persister,
entityKey
);
}
if ( managedEntity != null ) {
foundAnyManagedEntities = true;
result.add( managedEntity );
}
@ -226,10 +286,6 @@ public class DynamicBatchingEntityLoaderBuilder extends BatchingEntityLoaderBuil
}
}
final LockOptions lockOptions = (loadOptions.getLockOptions() == null)
? new LockOptions( LockMode.NONE )
: loadOptions.getLockOptions();
int numberOfIdsLeft = ids.length;
final int maxBatchSize;
if ( loadOptions.getBatchSize() != null && loadOptions.getBatchSize() > 0 ) {

View File

@ -14,11 +14,47 @@ import org.hibernate.LockOptions;
* @author Steve Ebersole
*/
public interface MultiLoadOptions {
/**
* Check the first-level cache first, and only if the entity is not found in the cache
* should Hibernate hit the database.
*
* @return the session cache is checked first
*/
boolean isSessionCheckingEnabled();
/**
* Check the second-level cache first, and only if the entity is not found in the cache
* should Hibernate hit the database.
*
* @return the session factory cache is checked first
*/
boolean isSecondLevelCacheCheckingEnabled();
/**
* Should we returned entities that are scheduled for deletion.
*
* @return entities that are scheduled for deletion are returned as well.
*/
boolean isReturnOfDeletedEntitiesEnabled();
/**
* Should the entities be returned in the same order as their associated entity identifiers were provided.
*
* @return entities follow the provided identifier order
*/
boolean isOrderReturnEnabled();
/**
* Specify the lock options applied during loading.
*
* @return lock options applied during loading.
*/
LockOptions getLockOptions();
/**
* Batch size to use when loading entities from the database.
*
* @return JDBC batch size
*/
Integer getBatchSize();
}

View File

@ -7,6 +7,7 @@
package org.hibernate.test.ops.multiLoad;
import java.util.List;
import java.util.Objects;
import javax.persistence.Cacheable;
import javax.persistence.Entity;
import javax.persistence.Id;
@ -17,13 +18,19 @@ import org.hibernate.CacheMode;
import org.hibernate.Session;
import org.hibernate.annotations.BatchSize;
import org.hibernate.boot.MetadataBuilder;
import org.hibernate.boot.SessionFactoryBuilder;
import org.hibernate.boot.registry.StandardServiceRegistryBuilder;
import org.hibernate.cache.spi.access.AccessType;
import org.hibernate.cfg.AvailableSettings;
import org.hibernate.engine.spi.EntityEntry;
import org.hibernate.engine.spi.EntityKey;
import org.hibernate.engine.spi.SessionImplementor;
import org.hibernate.engine.spi.SharedSessionContractImplementor;
import org.hibernate.engine.spi.Status;
import org.hibernate.stat.Statistics;
import org.hibernate.testing.TestForIssue;
import org.hibernate.testing.jdbc.SQLStatementInterceptor;
import org.hibernate.testing.junit4.BaseNonConfigCoreFunctionalTestCase;
import org.junit.After;
import org.junit.Before;
@ -41,6 +48,14 @@ import static org.junit.Assert.assertTrue;
* @author Steve Ebersole
*/
public class MultiLoadTest extends BaseNonConfigCoreFunctionalTestCase {
private SQLStatementInterceptor sqlStatementInterceptor;
@Override
protected void configureSessionFactoryBuilder(SessionFactoryBuilder sfb) {
sqlStatementInterceptor = new SQLStatementInterceptor( sfb );
}
@Override
protected Class[] getAnnotatedClasses() {
return new Class[] { SimpleEntity.class };
@ -50,6 +65,7 @@ public class MultiLoadTest extends BaseNonConfigCoreFunctionalTestCase {
protected void configureStandardServiceRegistryBuilder(StandardServiceRegistryBuilder ssrb) {
super.configureStandardServiceRegistryBuilder( ssrb );
ssrb.applySetting( AvailableSettings.USE_SECOND_LEVEL_CACHE, true );
ssrb.applySetting( AvailableSettings.GENERATE_STATISTICS, Boolean.TRUE.toString() );
}
@Override
@ -85,8 +101,13 @@ public class MultiLoadTest extends BaseNonConfigCoreFunctionalTestCase {
public void testBasicMultiLoad() {
doInHibernate(
this::sessionFactory, session -> {
List<SimpleEntity> list = session.byMultipleIds( SimpleEntity.class ).multiLoad( ids(56) );
assertEquals( 56, list.size() );
sqlStatementInterceptor.getSqlQueries().clear();
List<SimpleEntity> list = session.byMultipleIds( SimpleEntity.class ).multiLoad( ids( 5 ) );
assertEquals( 5, list.size() );
assertTrue( sqlStatementInterceptor.getSqlQueries().getFirst().endsWith( "id in (?,?,?,?,?)" ) );
}
);
}
@ -177,6 +198,193 @@ public class MultiLoadTest extends BaseNonConfigCoreFunctionalTestCase {
session.close();
}
@Test
@TestForIssue(jiraKey = "HHH-12944")
public void testMultiLoadFrom2ndLevelCache() {
Statistics statistics = sessionFactory().getStatistics();
sessionFactory().getCache().evictAll();
statistics.clear();
doInHibernate( this::sessionFactory, session -> {
// Load 1 of the items directly
SimpleEntity entity = session.get( SimpleEntity.class, 2 );
assertNotNull( entity );
assertEquals( 1, statistics.getSecondLevelCacheMissCount() );
assertEquals( 0, statistics.getSecondLevelCacheHitCount() );
assertEquals( 1, statistics.getSecondLevelCachePutCount() );
assertTrue( session.getSessionFactory().getCache().containsEntity( SimpleEntity.class, 2 ) );
} );
statistics.clear();
doInHibernate( this::sessionFactory, session -> {
// Validate that the entity is still in the Level 2 cache
assertTrue( session.getSessionFactory().getCache().containsEntity( SimpleEntity.class, 2 ) );
sqlStatementInterceptor.getSqlQueries().clear();
// Multiload 3 items and ensure that multiload pulls 2 from the database & 1 from the cache.
List<SimpleEntity> entities = session.byMultipleIds( SimpleEntity.class )
.with( CacheMode.NORMAL )
.enableSessionCheck( true )
.multiLoad( ids( 3 ) );
assertEquals( 3, entities.size() );
assertEquals( 1, statistics.getSecondLevelCacheHitCount() );
for(SimpleEntity entity: entities) {
assertTrue( session.contains( entity ) );
}
assertTrue( sqlStatementInterceptor.getSqlQueries().getFirst().endsWith( "id in (?,?)" ) );
} );
}
@Test
@TestForIssue(jiraKey = "HHH-12944")
public void testUnorderedMultiLoadFrom2ndLevelCache() {
Statistics statistics = sessionFactory().getStatistics();
sessionFactory().getCache().evictAll();
statistics.clear();
doInHibernate( this::sessionFactory, session -> {
// Load 1 of the items directly
SimpleEntity entity = session.get( SimpleEntity.class, 2 );
assertNotNull( entity );
assertEquals( 1, statistics.getSecondLevelCacheMissCount() );
assertEquals( 0, statistics.getSecondLevelCacheHitCount() );
assertEquals( 1, statistics.getSecondLevelCachePutCount() );
assertTrue( session.getSessionFactory().getCache().containsEntity( SimpleEntity.class, 2 ) );
} );
statistics.clear();
doInHibernate( this::sessionFactory, session -> {
// Validate that the entity is still in the Level 2 cache
assertTrue( session.getSessionFactory().getCache().containsEntity( SimpleEntity.class, 2 ) );
sqlStatementInterceptor.getSqlQueries().clear();
// Multiload 3 items and ensure that multiload pulls 2 from the database & 1 from the cache.
List<SimpleEntity> entities = session.byMultipleIds( SimpleEntity.class )
.with( CacheMode.NORMAL )
.enableSessionCheck( true )
.enableOrderedReturn( false )
.multiLoad( ids( 3 ) );
assertEquals( 3, entities.size() );
assertEquals( 1, statistics.getSecondLevelCacheHitCount() );
for(SimpleEntity entity: entities) {
assertTrue( session.contains( entity ) );
}
assertTrue( sqlStatementInterceptor.getSqlQueries().getFirst().endsWith( "id in (?,?)" ) );
} );
}
@Test
@TestForIssue(jiraKey = "HHH-12944")
public void testOrderedMultiLoadFrom2ndLevelCachePendingDelete() {
doInHibernate( this::sessionFactory, session -> {
session.remove( session.find( SimpleEntity.class, 2 ) );
sqlStatementInterceptor.getSqlQueries().clear();
// Multiload 3 items and ensure that multiload pulls 2 from the database & 1 from the cache.
List<SimpleEntity> entities = session.byMultipleIds( SimpleEntity.class )
.with( CacheMode.NORMAL )
.enableSessionCheck( true )
.enableOrderedReturn( true )
.multiLoad( ids( 3 ) );
assertEquals( 3, entities.size() );
assertNull( entities.get(1) );
assertTrue( sqlStatementInterceptor.getSqlQueries().getFirst().endsWith( "id in (?,?)" ) );
} );
}
@Test
@TestForIssue(jiraKey = "HHH-12944")
public void testOrderedMultiLoadFrom2ndLevelCachePendingDeleteReturnRemoved() {
doInHibernate( this::sessionFactory, session -> {
session.remove( session.find( SimpleEntity.class, 2 ) );
sqlStatementInterceptor.getSqlQueries().clear();
// Multiload 3 items and ensure that multiload pulls 2 from the database & 1 from the cache.
List<SimpleEntity> entities = session.byMultipleIds( SimpleEntity.class )
.with( CacheMode.NORMAL )
.enableSessionCheck( true )
.enableOrderedReturn( true )
.enableReturnOfDeletedEntities( true )
.multiLoad( ids( 3 ) );
assertEquals( 3, entities.size() );
SimpleEntity deletedEntity = entities.get(1);
assertNotNull( deletedEntity );
final EntityEntry entry = ((SharedSessionContractImplementor) session).getPersistenceContext().getEntry( deletedEntity );
assertTrue( entry.getStatus() == Status.DELETED || entry.getStatus() == Status.GONE );
assertTrue( sqlStatementInterceptor.getSqlQueries().getFirst().endsWith( "id in (?,?)" ) );
} );
}
@Test
@TestForIssue(jiraKey = "HHH-12944")
public void testUnorderedMultiLoadFrom2ndLevelCachePendingDelete() {
doInHibernate( this::sessionFactory, session -> {
session.remove( session.find( SimpleEntity.class, 2 ) );
sqlStatementInterceptor.getSqlQueries().clear();
// Multiload 3 items and ensure that multiload pulls 2 from the database & 1 from the cache.
List<SimpleEntity> entities = session.byMultipleIds( SimpleEntity.class )
.with( CacheMode.NORMAL )
.enableSessionCheck( true )
.enableOrderedReturn( false )
.multiLoad( ids( 3 ) );
assertEquals( 3, entities.size() );
assertTrue( entities.stream().anyMatch( Objects::isNull ) );
assertTrue( sqlStatementInterceptor.getSqlQueries().getFirst().endsWith( "id in (?,?)" ) );
} );
}
@Test
@TestForIssue(jiraKey = "HHH-12944")
public void testUnorderedMultiLoadFrom2ndLevelCachePendingDeleteReturnRemoved() {
doInHibernate( this::sessionFactory, session -> {
session.remove( session.find( SimpleEntity.class, 2 ) );
sqlStatementInterceptor.getSqlQueries().clear();
// Multiload 3 items and ensure that multiload pulls 2 from the database & 1 from the cache.
List<SimpleEntity> entities = session.byMultipleIds( SimpleEntity.class )
.with( CacheMode.NORMAL )
.enableSessionCheck( true )
.enableOrderedReturn( false )
.enableReturnOfDeletedEntities( true )
.multiLoad( ids( 3 ) );
assertEquals( 3, entities.size() );
SimpleEntity deletedEntity = entities.stream().filter( simpleEntity -> simpleEntity.getId().equals( 2 ) ).findAny().orElse( null );
assertNotNull( deletedEntity );
final EntityEntry entry = ((SharedSessionContractImplementor) session).getPersistenceContext().getEntry( deletedEntity );
assertTrue( entry.getStatus() == Status.DELETED || entry.getStatus() == Status.GONE );
assertTrue( sqlStatementInterceptor.getSqlQueries().getFirst().endsWith( "id in (?,?)" ) );
} );
}
@Test
public void testMultiLoadWithCacheModeIgnore() {
// do the multi-load, telling Hibernate to IGNORE the L2 cache -