HHH-12944 - MultiIdentifierLoadAccess ignores the 2nd level cache
This commit is contained in:
parent
ac03494e70
commit
512dfa574d
|
@ -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
|
||||
|
||||
|
|
|
@ -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 )
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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>.
|
||||
*
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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 ) {
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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 -
|
||||
|
|
Loading…
Reference in New Issue