diff --git a/hibernate-core/src/main/java/org/hibernate/IdentifierLoadAccess.java b/hibernate-core/src/main/java/org/hibernate/IdentifierLoadAccess.java index 7315c272581f..9569a45d1613 100644 --- a/hibernate-core/src/main/java/org/hibernate/IdentifierLoadAccess.java +++ b/hibernate-core/src/main/java/org/hibernate/IdentifierLoadAccess.java @@ -7,9 +7,10 @@ package org.hibernate; import java.io.Serializable; +import java.util.List; /** - * Loads an entity by its primary identifier. + * Loads an entity (or multiple) by its primary identifier. * * @author Eric Dalquist * @author Steve Ebersole @@ -22,7 +23,16 @@ public interface IdentifierLoadAccess { * * @return {@code this}, for method chaining */ - public IdentifierLoadAccess with(LockOptions lockOptions); + IdentifierLoadAccess 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 with(CacheMode cacheMode); /** * Return the persistent instance with the given identifier, assuming that the instance exists. This method @@ -36,7 +46,7 @@ public interface IdentifierLoadAccess { * * @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 +57,25 @@ public interface IdentifierLoadAccess { * * @return The persistent instance or {@code null} */ - public T load(Serializable id); + T load(Serializable id); + + /** + * Perform a load of multiple entities by identifiers + * + * @param ids The ids to load + * @param The identifier type + * + * @return The persistent entities. + */ + List multiLoad(K... ids); + + /** + * Perform a load of multiple entities by identifiers + * + * @param ids The ids to load + * @param The identifier type + * + * @return The persistent entities. + */ + List multiLoad(List ids); } diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/Dialect.java b/hibernate-core/src/main/java/org/hibernate/dialect/Dialect.java index 54d044b6e867..1b36e245fb85 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/Dialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/Dialect.java @@ -77,6 +77,7 @@ 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 CallableStatementSupport getCallableStatementSupport() { 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; + } } diff --git a/hibernate-core/src/main/java/org/hibernate/internal/SessionImpl.java b/hibernate-core/src/main/java/org/hibernate/internal/SessionImpl.java index a44c21f16363..3a663217c565 100644 --- a/hibernate-core/src/main/java/org/hibernate/internal/SessionImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/internal/SessionImpl.java @@ -17,6 +17,7 @@ import java.sql.Connection; import java.sql.NClob; import java.sql.SQLException; +import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashSet; @@ -129,6 +130,8 @@ 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.loader.entity.DynamicBatchingEntityLoaderBuilder.DynamicBatchingEntityLoader; import org.hibernate.persister.collection.CollectionPersister; import org.hibernate.persister.entity.EntityPersister; import org.hibernate.persister.entity.OuterJoinLoadable; @@ -2577,6 +2580,7 @@ public void lock(Object object) throws HibernateException { private class IdentifierLoadAccessImpl implements IdentifierLoadAccess { private final EntityPersister entityPersister; private LockOptions lockOptions; + private CacheMode cacheMode; private IdentifierLoadAccessImpl(EntityPersister entityPersister) { this.entityPersister = entityPersister; @@ -2597,8 +2601,37 @@ public final IdentifierLoadAccessImpl with(LockOptions lockOptions) { } @Override - @SuppressWarnings("unchecked") + public IdentifierLoadAccess 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") + private T doGetReference(Serializable id) { if ( this.lockOptions != null ) { LoadEvent event = new LoadEvent( id, entityPersister.getEntityName(), lockOptions, SessionImpl.this ); fireLoad( event, LoadEventListener.LOAD ); @@ -2624,8 +2657,31 @@ public final T getReference(Serializable id) { } @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") + public final T doLoad(Serializable id) { if ( this.lockOptions != null ) { LoadEvent event = new LoadEvent( id, entityPersister.getEntityName(), lockOptions, SessionImpl.this ); fireLoad( event, LoadEventListener.GET ); @@ -2646,6 +2702,64 @@ public final T load(Serializable id) { } return (T) event.getResult(); } + + @Override + public List 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, + SessionImpl.this + ); + } + finally { + if ( cacheModeChanged ) { + // change it back + setCacheMode( sessionCacheMode ); + } + } + } + + @Override + public List multiLoad(List 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, + SessionImpl.this + ); + } + finally { + if ( cacheModeChanged ) { + // change it back + setCacheMode( sessionCacheMode ); + } + } + } } private EntityPersister locateEntityPersister(Class entityClass) { diff --git a/hibernate-core/src/main/java/org/hibernate/loader/BatchLoadSizingStrategy.java b/hibernate-core/src/main/java/org/hibernate/loader/BatchLoadSizingStrategy.java new file mode 100644 index 000000000000..1e17a79d9fb2 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/loader/BatchLoadSizingStrategy.java @@ -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 . + */ +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); +} diff --git a/hibernate-core/src/main/java/org/hibernate/loader/entity/DynamicBatchingEntityLoaderBuilder.java b/hibernate-core/src/main/java/org/hibernate/loader/entity/DynamicBatchingEntityLoaderBuilder.java index e0ddfda6d44d..a04585bc38cd 100644 --- a/hibernate-core/src/main/java/org/hibernate/loader/entity/DynamicBatchingEntityLoaderBuilder.java +++ b/hibernate-core/src/main/java/org/hibernate/loader/entity/DynamicBatchingEntityLoaderBuilder.java @@ -11,6 +11,7 @@ import java.sql.SQLException; import java.sql.Statement; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import org.hibernate.LockMode; @@ -24,9 +25,11 @@ 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 +44,66 @@ public class DynamicBatchingEntityLoaderBuilder extends BatchingEntityLoaderBuil public static final DynamicBatchingEntityLoaderBuilder INSTANCE = new DynamicBatchingEntityLoaderBuilder(); + @SuppressWarnings("unchecked") + public List multiLoad( + OuterJoinLoadable persister, + K[] ids, + LockOptions lockOptions, + SessionImplementor session) { + List result = CollectionHelper.arrayList( ids.length ); + + if ( lockOptions == null ) { + lockOptions = new LockOptions( LockMode.NONE ); + } + + int numberOfIdsLeft = ids.length; + int optimalMaxBatchSize = session.getFactory().getDialect().getDefaultBatchLoadSizingStrategy().determineOptimalBatchLoadSize( + persister.getIdentifierType().getColumnSpan( session.getFactory() ), + numberOfIdsLeft + ); + + int idPosition = 0; + while ( numberOfIdsLeft > 0 ) { + int batchSize = Math.min( numberOfIdsLeft, optimalMaxBatchSize ); + 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, diff --git a/hibernate-core/src/test/java/org/hibernate/test/ops/multiLoad/MultiLoadTest.java b/hibernate-core/src/test/java/org/hibernate/test/ops/multiLoad/MultiLoadTest.java new file mode 100644 index 000000000000..24bcb5c025b4 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/test/ops/multiLoad/MultiLoadTest.java @@ -0,0 +1,102 @@ +/* + * 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 . + */ +package org.hibernate.test.ops.multiLoad; + +import java.io.Serializable; +import java.util.List; +import javax.persistence.Entity; +import javax.persistence.Id; +import javax.persistence.Table; + +import org.hibernate.Session; + +import org.hibernate.testing.junit4.BaseNonConfigCoreFunctionalTestCase; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; + +/** + * @author Steve Ebersole + */ +public class MultiLoadTest extends BaseNonConfigCoreFunctionalTestCase { + @Override + protected Class[] getAnnotatedClasses() { + return new Class[] { SimpleEntity.class }; + } + + @Before + public void before() { + Session session = sessionFactory().openSession(); + session.getTransaction().begin(); + for ( int i = 0; 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 list = session.byId( SimpleEntity.class ).multiLoad( ids(56) ); + assertEquals( 56, list.size() ); + session.getTransaction().commit(); + session.close(); + } + + private Integer[] ids(int count) { + Integer[] ids = new Integer[count]; + for ( int i = 0; i < count; i++ ) { + ids[i] = i; + } + return ids; + } + + @Entity( name = "SimpleEntity" ) + @Table( name = "SimpleEntity" ) + 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; + } + } +}