HHH-7572 - Develop API for load-by-multiple-ids

This commit is contained in:
Steve Ebersole 2015-11-05 15:43:57 -06:00
parent 2b563794c7
commit 134eb06fba
9 changed files with 608 additions and 5 deletions

View File

@ -22,7 +22,16 @@ public interface IdentifierLoadAccess<T> {
*
* @return {@code this}, for method chaining
*/
public IdentifierLoadAccess<T> with(LockOptions lockOptions);
IdentifierLoadAccess<T> with(LockOptions lockOptions);
/**
* Specify the {@link CacheMode} to use when retrieving the entity.
*
* @param cacheMode The CacheMode to use.
*
* @return {@code this}, for method chaining
*/
IdentifierLoadAccess<T> with(CacheMode cacheMode);
/**
* Return the persistent instance with the given identifier, assuming that the instance exists. This method
@ -36,7 +45,7 @@ public interface IdentifierLoadAccess<T> {
*
* @return the persistent instance or proxy
*/
public T getReference(Serializable id);
T getReference(Serializable id);
/**
* Return the persistent instance with the given identifier, or null if there is no such persistent instance.
@ -47,5 +56,5 @@ public interface IdentifierLoadAccess<T> {
*
* @return The persistent instance or {@code null}
*/
public T load(Serializable id);
T load(Serializable id);
}

View File

@ -0,0 +1,78 @@
/*
* 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;
import java.io.Serializable;
import java.util.List;
/**
* Loads multiple entities at once by identifiers
*
* @author Steve Ebersole
*/
public interface MultiIdentifierLoadAccess<T> {
/**
* Specify the {@link LockOptions} to use when retrieving the entity.
*
* @param lockOptions The lock options to use.
*
* @return {@code this}, for method chaining
*/
MultiIdentifierLoadAccess<T> with(LockOptions lockOptions);
/**
* Specify the {@link CacheMode} to use when retrieving the entity.
*
* @param cacheMode The CacheMode to use.
*
* @return {@code this}, for method chaining
*/
MultiIdentifierLoadAccess<T> with(CacheMode cacheMode);
/**
* Specify a batch size for loading the entities (how many at a time). The default is
* to use a batch sizing strategy defined by the Dialect in use. Any greater-than-one
* value here will override that default behavior. If giving an explicit value here,
* care should be taken to not exceed the capabilities of of the underlying database.
*
* @param batchSize The batch size
*
* @return {@code this}, for method chaining
*/
MultiIdentifierLoadAccess<T> withBatchSize(int batchSize);
/**
* Should we check the Session to see whether it 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>
*
* @param enabled {@code true} enables this checking; {@code false} disables it.
*
* @return {@code this}, for method chaining
*/
MultiIdentifierLoadAccess<T> enableSessionCheck(boolean enabled);
/**
* Perform a load of multiple entities by identifiers
*
* @param ids The ids to load
* @param <K> The identifier type
*
* @return The persistent entities.
*/
<K extends Serializable> List<T> multiLoad(K... ids);
/**
* Perform a load of multiple entities by identifiers
*
* @param ids The ids to load
* @param <K> The identifier type
*
* @return The persistent entities.
*/
<K extends Serializable> List<T> multiLoad(List<K> ids);
}

View File

@ -782,6 +782,30 @@ public interface Session extends SharedSessionContract, java.io.Closeable {
*/
IdentifierLoadAccess byId(String entityName);
/**
* Create a {@link MultiIdentifierLoadAccess} instance to retrieve multiple entities at once
* as specified by primary key values.
*
* @param entityClass The entity type to be retrieved
*
* @return load delegate for loading the specified entity type by primary key values
*
* @throws HibernateException If the specified Class cannot be resolved as a mapped entity
*/
<T> MultiIdentifierLoadAccess<T> byMultipleIds(Class<T> entityClass);
/**
* Create a {@link MultiIdentifierLoadAccess} instance to retrieve multiple entities at once
* as specified by primary key values.
*
* @param entityName The entity name of the entity type to be retrieved
*
* @return load delegate for loading the specified entity type by primary key values
*
* @throws HibernateException If the specified entity name cannot be resolved as an entity name
*/
MultiIdentifierLoadAccess byMultipleIds(String entityName);
/**
* Create an {@link IdentifierLoadAccess} instance to retrieve the specified entity by
* primary key.

View File

@ -77,6 +77,7 @@ import org.hibernate.internal.util.ReflectHelper;
import org.hibernate.internal.util.StringHelper;
import org.hibernate.internal.util.collections.ArrayHelper;
import org.hibernate.internal.util.io.StreamCopier;
import org.hibernate.loader.BatchLoadSizingStrategy;
import org.hibernate.mapping.Column;
import org.hibernate.mapping.Constraint;
import org.hibernate.mapping.ForeignKey;
@ -2813,4 +2814,15 @@ public abstract class Dialect implements ConversionContext {
public NameQualifierSupport getNameQualifierSupport() {
return null;
}
protected final BatchLoadSizingStrategy STANDARD_DEFAULT_BATCH_LOAD_SIZING_STRATEGY = new BatchLoadSizingStrategy() {
@Override
public int determineOptimalBatchLoadSize(int numberOfKeyColumns, int numberOfKeys) {
return 50;
}
};
public BatchLoadSizingStrategy getDefaultBatchLoadSizingStrategy() {
return STANDARD_DEFAULT_BATCH_LOAD_SIZING_STRATEGY;
}
}

View File

@ -21,6 +21,7 @@ import org.hibernate.Interceptor;
import org.hibernate.LobHelper;
import org.hibernate.LockMode;
import org.hibernate.LockOptions;
import org.hibernate.MultiIdentifierLoadAccess;
import org.hibernate.NaturalIdLoadAccess;
import org.hibernate.Query;
import org.hibernate.ReplicationMode;
@ -662,6 +663,16 @@ public class SessionDelegatorBaseImpl implements SessionImplementor, Session {
return session.byId( entityName );
}
@Override
public <T> MultiIdentifierLoadAccess<T> byMultipleIds(Class<T> entityClass) {
return session.byMultipleIds( entityClass );
}
@Override
public MultiIdentifierLoadAccess byMultipleIds(String entityName) {
return session.byMultipleIds( entityName );
}
@Override
public <T> IdentifierLoadAccess<T> byId(Class<T> entityClass) {
return session.byId( entityClass );

View File

@ -42,6 +42,7 @@ import org.hibernate.LobHelper;
import org.hibernate.LockMode;
import org.hibernate.LockOptions;
import org.hibernate.MappingException;
import org.hibernate.MultiIdentifierLoadAccess;
import org.hibernate.NaturalIdLoadAccess;
import org.hibernate.ObjectDeletedException;
import org.hibernate.ObjectNotFoundException;
@ -129,6 +130,7 @@ import org.hibernate.jdbc.WorkExecutorVisitable;
import org.hibernate.loader.criteria.CriteriaLoader;
import org.hibernate.loader.custom.CustomLoader;
import org.hibernate.loader.custom.CustomQuery;
import org.hibernate.loader.entity.DynamicBatchingEntityLoaderBuilder;
import org.hibernate.persister.collection.CollectionPersister;
import org.hibernate.persister.entity.EntityPersister;
import org.hibernate.persister.entity.OuterJoinLoadable;
@ -1044,6 +1046,16 @@ public final class SessionImpl extends AbstractSessionImpl implements EventSourc
return new IdentifierLoadAccessImpl<T>( entityClass );
}
@Override
public <T> MultiIdentifierLoadAccess<T> byMultipleIds(Class<T> entityClass) {
return new MultiIdentifierLoadAccessImpl<T>( locateEntityPersister( entityClass ) );
}
@Override
public MultiIdentifierLoadAccess byMultipleIds(String entityName) {
return new MultiIdentifierLoadAccessImpl( locateEntityPersister( entityName ) );
}
@Override
public NaturalIdLoadAccess byNaturalId(String entityName) {
return new NaturalIdLoadAccessImpl( entityName );
@ -2577,6 +2589,7 @@ public final class SessionImpl extends AbstractSessionImpl implements EventSourc
private class IdentifierLoadAccessImpl<T> implements IdentifierLoadAccess<T> {
private final EntityPersister entityPersister;
private LockOptions lockOptions;
private CacheMode cacheMode;
private IdentifierLoadAccessImpl(EntityPersister entityPersister) {
this.entityPersister = entityPersister;
@ -2597,8 +2610,37 @@ public final class SessionImpl extends AbstractSessionImpl implements EventSourc
}
@Override
@SuppressWarnings("unchecked")
public IdentifierLoadAccess<T> with(CacheMode cacheMode) {
this.cacheMode = cacheMode;
return this;
}
@Override
public final T getReference(Serializable id) {
CacheMode sessionCacheMode = getCacheMode();
boolean cacheModeChanged = false;
if ( cacheMode != null ) {
// naive check for now...
// todo : account for "conceptually equal"
if ( cacheMode != sessionCacheMode ) {
setCacheMode( cacheMode );
cacheModeChanged = true;
}
}
try {
return doGetReference( id );
}
finally {
if ( cacheModeChanged ) {
// change it back
setCacheMode( sessionCacheMode );
}
}
}
@SuppressWarnings("unchecked")
protected T doGetReference(Serializable id) {
if ( this.lockOptions != null ) {
LoadEvent event = new LoadEvent( id, entityPersister.getEntityName(), lockOptions, SessionImpl.this );
fireLoad( event, LoadEventListener.LOAD );
@ -2624,8 +2666,31 @@ public final class SessionImpl extends AbstractSessionImpl implements EventSourc
}
@Override
@SuppressWarnings("unchecked")
public final T load(Serializable id) {
CacheMode sessionCacheMode = getCacheMode();
boolean cacheModeChanged = false;
if ( cacheMode != null ) {
// naive check for now...
// todo : account for "conceptually equal"
if ( cacheMode != sessionCacheMode ) {
setCacheMode( cacheMode );
cacheModeChanged = true;
}
}
try {
return doLoad( id );
}
finally {
if ( cacheModeChanged ) {
// change it back
setCacheMode( sessionCacheMode );
}
}
}
@SuppressWarnings("unchecked")
protected final T doLoad(Serializable id) {
if ( this.lockOptions != null ) {
LoadEvent event = new LoadEvent( id, entityPersister.getEntityName(), lockOptions, SessionImpl.this );
fireLoad( event, LoadEventListener.GET );
@ -2648,6 +2713,109 @@ public final class SessionImpl extends AbstractSessionImpl implements EventSourc
}
}
private class MultiIdentifierLoadAccessImpl<T> implements MultiIdentifierLoadAccess<T> {
private final EntityPersister entityPersister;
private LockOptions lockOptions;
private CacheMode cacheMode;
private Integer batchSize;
private boolean sessionCheckingEnabled;
public MultiIdentifierLoadAccessImpl(EntityPersister entityPersister) {
this.entityPersister = entityPersister;
}
@Override
public final MultiIdentifierLoadAccessImpl<T> with(LockOptions lockOptions) {
this.lockOptions = lockOptions;
return this;
}
@Override
public MultiIdentifierLoadAccessImpl<T> with(CacheMode cacheMode) {
this.cacheMode = cacheMode;
return this;
}
@Override
public MultiIdentifierLoadAccess<T> withBatchSize(int batchSize) {
if ( batchSize < 1 ) {
this.batchSize = null;
}
else {
this.batchSize = batchSize;
}
return this;
}
@Override
public MultiIdentifierLoadAccess<T> enableSessionCheck(boolean enabled) {
this.sessionCheckingEnabled = enabled;
return this;
}
@Override
public <K extends Serializable> List<T> multiLoad(K... ids) {
CacheMode sessionCacheMode = getCacheMode();
boolean cacheModeChanged = false;
if ( cacheMode != null ) {
// naive check for now...
// todo : account for "conceptually equal"
if ( cacheMode != sessionCacheMode ) {
setCacheMode( cacheMode );
cacheModeChanged = true;
}
}
try {
return DynamicBatchingEntityLoaderBuilder.INSTANCE.multiLoad(
(OuterJoinLoadable) entityPersister,
ids,
lockOptions,
batchSize,
sessionCheckingEnabled,
SessionImpl.this
);
}
finally {
if ( cacheModeChanged ) {
// change it back
setCacheMode( sessionCacheMode );
}
}
}
@Override
public <K extends Serializable> List<T> multiLoad(List<K> ids) {
CacheMode sessionCacheMode = getCacheMode();
boolean cacheModeChanged = false;
if ( cacheMode != null ) {
// naive check for now...
// todo : account for "conceptually equal"
if ( cacheMode != sessionCacheMode ) {
setCacheMode( cacheMode );
cacheModeChanged = true;
}
}
try {
return DynamicBatchingEntityLoaderBuilder.INSTANCE.multiLoad(
(OuterJoinLoadable) entityPersister,
ids.toArray( new Serializable[ ids.size() ] ),
lockOptions,
batchSize,
sessionCheckingEnabled,
SessionImpl.this
);
}
finally {
if ( cacheModeChanged ) {
// change it back
setCacheMode( sessionCacheMode );
}
}
}
}
private EntityPersister locateEntityPersister(Class entityClass) {
return factory.locateEntityPersister( entityClass );
}

View File

@ -0,0 +1,16 @@
/*
* 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;
/**
* Strategy (pluggable) for determining an optimal size for batch loads.
*
* @author Steve Ebersole
*/
public interface BatchLoadSizingStrategy {
int determineOptimalBatchLoadSize(int numberOfKeyColumns, int numberOfKeys);
}

View File

@ -7,15 +7,18 @@
package org.hibernate.loader.entity;
import java.io.Serializable;
import java.lang.reflect.Array;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import org.hibernate.LockMode;
import org.hibernate.LockOptions;
import org.hibernate.dialect.pagination.LimitHelper;
import org.hibernate.engine.spi.EntityKey;
import org.hibernate.engine.spi.LoadQueryInfluencers;
import org.hibernate.engine.spi.PersistenceContext;
import org.hibernate.engine.spi.QueryParameters;
@ -24,9 +27,11 @@ import org.hibernate.engine.spi.SessionFactoryImplementor;
import org.hibernate.engine.spi.SessionImplementor;
import org.hibernate.internal.util.StringHelper;
import org.hibernate.internal.util.collections.ArrayHelper;
import org.hibernate.internal.util.collections.CollectionHelper;
import org.hibernate.loader.spi.AfterLoadAction;
import org.hibernate.persister.entity.OuterJoinLoadable;
import org.hibernate.pretty.MessageHelper;
import org.hibernate.type.Type;
import org.jboss.logging.Logger;
@ -41,6 +46,114 @@ public class DynamicBatchingEntityLoaderBuilder extends BatchingEntityLoaderBuil
public static final DynamicBatchingEntityLoaderBuilder INSTANCE = new DynamicBatchingEntityLoaderBuilder();
@SuppressWarnings("unchecked")
public <T, K extends Serializable> List<T> multiLoad(
OuterJoinLoadable persister,
K[] ids,
LockOptions lockOptions,
Integer explicitBatchSize,
boolean sessionCheckingEnabled,
SessionImplementor session) {
List<T> result = CollectionHelper.arrayList( ids.length );
if ( sessionCheckingEnabled ) {
// 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
// entity associated with the PC - if it does we add it to the result
// list immediately and remove its id from the group of ids to load.
boolean foundAnyManagedEntities = false;
final List<K> nonManagedIds = new ArrayList<K>();
for ( K id : ids ) {
final EntityKey entityKey = new EntityKey( id, persister );
final T managedEntity = (T) session.getPersistenceContext().getEntity( entityKey );
if ( managedEntity != null ) {
foundAnyManagedEntities = true;
result.add( managedEntity );
}
else {
nonManagedIds.add( id );
}
}
if ( foundAnyManagedEntities ) {
if ( nonManagedIds.isEmpty() ) {
// all of the given ids were already associated with the Session
return result;
}
else {
// over-write the ids to be loaded with the collection of
// just non-managed ones
ids = nonManagedIds.toArray(
(K[]) Array.newInstance(
ids.getClass().getComponentType(),
nonManagedIds.size()
)
);
}
}
}
if ( lockOptions == null ) {
lockOptions = new LockOptions( LockMode.NONE );
}
int numberOfIdsLeft = ids.length;
final int maxBatchSize;
if ( explicitBatchSize != null && explicitBatchSize > 0 ) {
maxBatchSize = explicitBatchSize;
}
else {
maxBatchSize = session.getFactory().getDialect().getDefaultBatchLoadSizingStrategy().determineOptimalBatchLoadSize(
persister.getIdentifierType().getColumnSpan( session.getFactory() ),
numberOfIdsLeft
);
}
int idPosition = 0;
while ( numberOfIdsLeft > 0 ) {
int batchSize = Math.min( numberOfIdsLeft, maxBatchSize );
final DynamicEntityLoader batchingLoader = new DynamicEntityLoader(
persister,
batchSize,
lockOptions,
session.getFactory(),
session.getLoadQueryInfluencers()
);
Serializable[] idsInBatch = new Serializable[batchSize];
System.arraycopy( ids, idPosition, idsInBatch, 0, batchSize );
QueryParameters qp = buildMultiLoadQueryParameters( persister, idsInBatch, lockOptions );
result.addAll( batchingLoader.doEntityBatchFetch( session, qp, idsInBatch ) );
numberOfIdsLeft = numberOfIdsLeft - batchSize;
idPosition += batchSize;
}
return result;
}
public static QueryParameters buildMultiLoadQueryParameters(
OuterJoinLoadable persister,
Serializable[] ids,
LockOptions lockOptions) {
Type[] types = new Type[ids.length];
Arrays.fill( types, persister.getIdentifierType() );
QueryParameters qp = new QueryParameters();
qp.setOptionalEntityName( persister.getEntityName() );
qp.setPositionalParameterTypes( types );
qp.setPositionalParameterValues( ids );
qp.setLockOptions( lockOptions );
qp.setOptionalObject( null );
qp.setOptionalId( null );
return qp;
}
@Override
protected UniqueEntityLoader buildBatchingLoader(
OuterJoinLoadable persister,

View File

@ -0,0 +1,172 @@
/*
* 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.test.ops.multiLoad;
import java.util.List;
import javax.persistence.Cacheable;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.SharedCacheMode;
import javax.persistence.Table;
import org.hibernate.CacheMode;
import org.hibernate.Session;
import org.hibernate.boot.MetadataBuilder;
import org.hibernate.boot.registry.StandardServiceRegistryBuilder;
import org.hibernate.cache.spi.access.AccessType;
import org.hibernate.cfg.AvailableSettings;
import org.hibernate.testing.junit4.BaseNonConfigCoreFunctionalTestCase;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertSame;
/**
* @author Steve Ebersole
*/
public class MultiLoadTest extends BaseNonConfigCoreFunctionalTestCase {
@Override
protected Class[] getAnnotatedClasses() {
return new Class[] { SimpleEntity.class };
}
@Override
protected void configureStandardServiceRegistryBuilder(StandardServiceRegistryBuilder ssrb) {
super.configureStandardServiceRegistryBuilder( ssrb );
ssrb.applySetting( AvailableSettings.USE_SECOND_LEVEL_CACHE, true );
}
@Override
protected void configureMetadataBuilder(MetadataBuilder metadataBuilder) {
super.configureMetadataBuilder( metadataBuilder );
metadataBuilder.applySharedCacheMode( SharedCacheMode.ENABLE_SELECTIVE );
metadataBuilder.applyAccessType( AccessType.READ_WRITE );
}
@Before
public void before() {
Session session = sessionFactory().openSession();
session.getTransaction().begin();
session.setCacheMode( CacheMode.IGNORE );
for ( int i = 1; i <= 60; i++ ) {
session.save( new SimpleEntity( i, "Entity #" + i ) );
}
session.getTransaction().commit();
session.close();
}
@After
public void after() {
Session session = sessionFactory().openSession();
session.getTransaction().begin();
session.createQuery( "delete SimpleEntity" ).executeUpdate();
session.getTransaction().commit();
session.close();
}
@Test
public void testBasicMultiLoad() {
Session session = openSession();
session.getTransaction().begin();
List<SimpleEntity> list = session.byMultipleIds( SimpleEntity.class ).multiLoad( ids(56) );
assertEquals( 56, list.size() );
session.getTransaction().commit();
session.close();
}
@Test
public void testBasicMultiLoadWithManagedAndNoChecking() {
Session session = openSession();
session.getTransaction().begin();
SimpleEntity first = session.byId( SimpleEntity.class ).load( 1 );
List<SimpleEntity> list = session.byMultipleIds( SimpleEntity.class ).multiLoad( ids(56) );
assertEquals( 56, list.size() );
// this check is HIGHLY specific to implementation in the batch loader
// which puts existing managed entities first...
assertSame( first, list.get( 0 ) );
session.getTransaction().commit();
session.close();
}
@Test
public void testBasicMultiLoadWithManagedAndChecking() {
Session session = openSession();
session.getTransaction().begin();
SimpleEntity first = session.byId( SimpleEntity.class ).load( 1 );
List<SimpleEntity> list = session.byMultipleIds( SimpleEntity.class ).enableSessionCheck( true ).multiLoad( ids(56) );
assertEquals( 56, list.size() );
// this check is HIGHLY specific to implementation in the batch loader
// which puts existing managed entities first...
assertSame( first, list.get( 0 ) );
session.getTransaction().commit();
session.close();
}
@Test
public void testMultiLoadWithCacheModeIgnore() {
// do the multi-load, telling Hibernate to IGNORE the L2 cache -
// the end result should be that the cache is (still) empty afterwards
Session session = openSession();
session.getTransaction().begin();
List<SimpleEntity> list = session.byMultipleIds( SimpleEntity.class )
.with( CacheMode.IGNORE )
.multiLoad( ids(56) );
session.getTransaction().commit();
session.close();
assertEquals( 56, list.size() );
for ( SimpleEntity entity : list ) {
assertFalse( sessionFactory().getCache().containsEntity( SimpleEntity.class, entity.getId() ) );
}
}
private Integer[] ids(int count) {
Integer[] ids = new Integer[count];
for ( int i = 1; i <= count; i++ ) {
ids[i-1] = i;
}
return ids;
}
@Entity( name = "SimpleEntity" )
@Table( name = "SimpleEntity" )
@Cacheable()
public static class SimpleEntity {
Integer id;
String text;
public SimpleEntity() {
}
public SimpleEntity(Integer id, String text) {
this.id = id;
this.text = text;
}
@Id
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getText() {
return text;
}
public void setText(String text) {
this.text = text;
}
}
}