HHH-11970 : @NotFound(IGNORE) and @BatchSize

This commit is contained in:
Gail Badner 2017-09-22 18:10:19 -07:00 committed by Andrea Boriero
parent 87a37b02f4
commit 19087d9f15
9 changed files with 539 additions and 5 deletions

View File

@ -0,0 +1,86 @@
/*
* 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.engine.internal;
import java.io.Serializable;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import org.hibernate.engine.spi.BatchFetchQueue;
import org.hibernate.engine.spi.EntityKey;
import org.hibernate.engine.spi.SharedSessionContractImplementor;
import org.hibernate.internal.CoreMessageLogger;
import org.hibernate.persister.entity.EntityPersister;
import org.jboss.logging.Logger;
/**
* @author Gail Badner
*/
public class BatchFetchQueueHelper {
private static final CoreMessageLogger LOG = Logger.getMessageLogger(
CoreMessageLogger.class,
BatchFetchQueueHelper.class.getName()
);
private BatchFetchQueueHelper(){
}
/**
* Finds the IDs for entities that were not found when the batch was loaded, and removes
* the corresponding entity keys from the {@link BatchFetchQueue}.
*
* @param ids - the IDs for the entities that were batch loaded
* @param results - the results from loading the batch
* @param persister - the entity persister for the entities in batch
* @param session - the session
*/
public static void removeNotFoundBatchLoadableEntityKeys(
Serializable[] ids,
List<?> results,
EntityPersister persister,
SharedSessionContractImplementor session) {
if ( !persister.isBatchLoadable() ) {
return;
}
if ( ids.length == results.size() ) {
return;
}
LOG.debug( "Not all entities were loaded." );
Set<Serializable> idSet = new HashSet<>( Arrays.asList( ids ) );
for ( Object result : results ) {
final Serializable id = session.getPersistenceContext().getEntry( result ).getId();
idSet.remove( id );
}
assert idSet.size() == ids.length - results.size();
if ( LOG.isDebugEnabled() ) {
LOG.debug( "Entities of type [" + persister.getEntityName() + "] not found; IDs: " + idSet );
}
for ( Serializable id : idSet ) {
removeBatchLoadableEntityKey( id, persister, session );
}
}
/**
* Remove the entity key with the specified {@code id} and {@code persister} from
* the batch loadable entities {@link BatchFetchQueue}.
*
* @param id - the ID for the entity to be removed
* @param persister - the entity persister
* @param session - the session
*/
public static void removeBatchLoadableEntityKey(
Serializable id,
EntityPersister persister,
SharedSessionContractImplementor session) {
final EntityKey entityKey = session.generateEntityKey( id, persister );
final BatchFetchQueue batchFetchQueue = session.getPersistenceContext().getBatchFetchQueue();
batchFetchQueue.removeBatchLoadableEntityKey( entityKey );
}
}

View File

@ -12,6 +12,7 @@ import java.util.Arrays;
import java.util.List; import java.util.List;
import org.hibernate.LockOptions; import org.hibernate.LockOptions;
import org.hibernate.engine.internal.BatchFetchQueueHelper;
import org.hibernate.engine.spi.QueryParameters; import org.hibernate.engine.spi.QueryParameters;
import org.hibernate.engine.spi.SharedSessionContractImplementor; import org.hibernate.engine.spi.SharedSessionContractImplementor;
import org.hibernate.loader.Loader; import org.hibernate.loader.Loader;
@ -97,6 +98,15 @@ public abstract class BatchingEntityLoader implements UniqueEntityLoader {
try { try {
final List results = loaderToUse.doQueryAndInitializeNonLazyCollections( session, qp, false ); final List results = loaderToUse.doQueryAndInitializeNonLazyCollections( session, qp, false );
log.debug( "Done entity batch load" ); log.debug( "Done entity batch load" );
// The EntityKey for any entity that is not found will remain in the batch.
// Explicitly remove the EntityKeys for entities that were not found to
// avoid including them in future batches that get executed.
BatchFetchQueueHelper.removeNotFoundBatchLoadableEntityKeys(
ids,
results,
persister(),
session
);
return getObjectFromList(results, id, session); return getObjectFromList(results, id, session);
} }
catch ( SQLException sqle ) { catch ( SQLException sqle ) {

View File

@ -18,6 +18,7 @@ import java.util.List;
import org.hibernate.LockMode; import org.hibernate.LockMode;
import org.hibernate.LockOptions; import org.hibernate.LockOptions;
import org.hibernate.dialect.pagination.LimitHelper; import org.hibernate.dialect.pagination.LimitHelper;
import org.hibernate.engine.internal.BatchFetchQueueHelper;
import org.hibernate.engine.spi.EntityEntry; import org.hibernate.engine.spi.EntityEntry;
import org.hibernate.engine.spi.EntityKey; import org.hibernate.engine.spi.EntityKey;
import org.hibernate.engine.spi.LoadQueryInfluencers; import org.hibernate.engine.spi.LoadQueryInfluencers;
@ -344,7 +345,13 @@ public class DynamicBatchingEntityLoaderBuilder extends BatchingEntityLoaderBuil
final int numberOfIds = ArrayHelper.countNonNull( batch ); final int numberOfIds = ArrayHelper.countNonNull( batch );
if ( numberOfIds <= 1 ) { if ( numberOfIds <= 1 ) {
return singleKeyLoader.load( id, optionalObject, session ); final Object result = singleKeyLoader.load( id, optionalObject, session );
if ( result == null ) {
// There was no entity with the specified ID. Make sure the EntityKey does not remain
// in the batch to avoid including it in future batches that get executed.
BatchFetchQueueHelper.removeBatchLoadableEntityKey( id, persister(), session );
}
return result;
} }
final Serializable[] idsToLoad = new Serializable[numberOfIds]; final Serializable[] idsToLoad = new Serializable[numberOfIds];
@ -356,6 +363,12 @@ public class DynamicBatchingEntityLoaderBuilder extends BatchingEntityLoaderBuil
QueryParameters qp = buildQueryParameters( id, idsToLoad, optionalObject, lockOptions ); QueryParameters qp = buildQueryParameters( id, idsToLoad, optionalObject, lockOptions );
List results = dynamicLoader.doEntityBatchFetch( session, qp, idsToLoad ); List results = dynamicLoader.doEntityBatchFetch( session, qp, idsToLoad );
// The EntityKey for any entity that is not found will remain in the batch.
// Explicitly remove the EntityKeys for entities that were not found to
// avoid including them in future batches that get executed.
BatchFetchQueueHelper.removeNotFoundBatchLoadableEntityKeys( idsToLoad, results, persister(), session );
return getObjectFromList( results, id, session ); return getObjectFromList( results, id, session );
} }
} }

View File

@ -11,6 +11,7 @@ import java.util.List;
import org.hibernate.LockMode; import org.hibernate.LockMode;
import org.hibernate.LockOptions; import org.hibernate.LockOptions;
import org.hibernate.engine.internal.BatchFetchQueueHelper;
import org.hibernate.engine.spi.LoadQueryInfluencers; import org.hibernate.engine.spi.LoadQueryInfluencers;
import org.hibernate.engine.spi.SessionFactoryImplementor; import org.hibernate.engine.spi.SessionFactoryImplementor;
import org.hibernate.engine.spi.SharedSessionContractImplementor; import org.hibernate.engine.spi.SharedSessionContractImplementor;
@ -100,10 +101,25 @@ public class LegacyBatchingEntityLoaderBuilder extends BatchingEntityLoaderBuild
persister(), persister(),
lockOptions lockOptions
); );
// The EntityKey for any entity that is not found will remain in the batch.
// Explicitly remove the EntityKeys for entities that were not found to
// avoid including them in future batches that get executed.
BatchFetchQueueHelper.removeNotFoundBatchLoadableEntityKeys(
smallBatch,
results,
persister(),
session
);
return getObjectFromList(results, id, session); //EARLY EXIT return getObjectFromList(results, id, session); //EARLY EXIT
} }
} }
return ( (UniqueEntityLoader) loaders[batchSizes.length-1] ).load(id, optionalObject, session); final Object result = ( (UniqueEntityLoader) loaders[batchSizes.length-1] ).load(id, optionalObject, session);
if ( result == null ) {
// There was no entity with the specified ID. Make sure the EntityKey does not remain
// in the batch to avoid including it in future batches that get executed.
BatchFetchQueueHelper.removeBatchLoadableEntityKey( id, persister(), session );
}
return result;
} }
} }

View File

@ -11,6 +11,7 @@ import java.io.Serializable;
import org.hibernate.HibernateException; import org.hibernate.HibernateException;
import org.hibernate.LockMode; import org.hibernate.LockMode;
import org.hibernate.LockOptions; import org.hibernate.LockOptions;
import org.hibernate.engine.internal.BatchFetchQueueHelper;
import org.hibernate.engine.spi.LoadQueryInfluencers; import org.hibernate.engine.spi.LoadQueryInfluencers;
import org.hibernate.engine.spi.SessionFactoryImplementor; import org.hibernate.engine.spi.SessionFactoryImplementor;
import org.hibernate.engine.spi.SharedSessionContractImplementor; import org.hibernate.engine.spi.SharedSessionContractImplementor;
@ -96,7 +97,13 @@ class PaddedBatchingEntityLoaderBuilder extends BatchingEntityLoaderBuilder {
final int numberOfIds = ArrayHelper.countNonNull( batch ); final int numberOfIds = ArrayHelper.countNonNull( batch );
if ( numberOfIds <= 1 ) { if ( numberOfIds <= 1 ) {
return ( (UniqueEntityLoader) loaders[batchSizes.length-1] ).load( id, optionalObject, session ); final Object result = ( (UniqueEntityLoader) loaders[batchSizes.length-1] ).load( id, optionalObject, session );
if ( result == null ) {
// There was no entity with the specified ID. Make sure the EntityKey does not remain
// in the batch to avoid including it in future batches that get executed.
BatchFetchQueueHelper.removeBatchLoadableEntityKey( id, persister(), session );
}
return result;
} }
// Uses the first batch-size bigger than the number of actual ids in the batch // Uses the first batch-size bigger than the number of actual ids in the batch

View File

@ -11,6 +11,7 @@ import java.util.List;
import org.hibernate.LockMode; import org.hibernate.LockMode;
import org.hibernate.LockOptions; import org.hibernate.LockOptions;
import org.hibernate.engine.internal.BatchFetchQueueHelper;
import org.hibernate.engine.spi.LoadQueryInfluencers; import org.hibernate.engine.spi.LoadQueryInfluencers;
import org.hibernate.engine.spi.SessionFactoryImplementor; import org.hibernate.engine.spi.SessionFactoryImplementor;
import org.hibernate.engine.spi.SharedSessionContractImplementor; import org.hibernate.engine.spi.SharedSessionContractImplementor;
@ -106,12 +107,27 @@ public class LegacyBatchingEntityLoaderBuilder extends AbstractBatchingEntityLoa
persister(), persister(),
lockOptions lockOptions
); );
// The EntityKey for any entity that is not found will remain in the batch.
// Explicitly remove the EntityKeys for entities that were not found to
// avoid including them in future batches that get executed.
BatchFetchQueueHelper.removeNotFoundBatchLoadableEntityKeys(
smallBatch,
results,
persister(),
session
);
//EARLY EXIT //EARLY EXIT
return getObjectFromList( results, id, session ); return getObjectFromList( results, id, session );
} }
} }
return ( loaders[batchSizes.length-1] ).load( id, optionalObject, session, lockOptions ); final Object result = ( loaders[batchSizes.length-1] ).load( id, optionalObject, session, lockOptions );
if ( result == null ) {
// There was no entity with the specified ID. Make sure the EntityKey does not remain
// in the batch to avoid including it in future batches that get executed.
BatchFetchQueueHelper.removeBatchLoadableEntityKey( id, persister(), session );
}
return result;
} }
} }
} }

View File

@ -0,0 +1,339 @@
/*
* 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.batchfetch;
import java.util.ArrayList;
import java.util.List;
import javax.persistence.ConstraintMode;
import javax.persistence.Entity;
import javax.persistence.ForeignKey;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.OneToOne;
import org.hibernate.Session;
import org.hibernate.annotations.BatchSize;
import org.hibernate.annotations.NotFound;
import org.hibernate.annotations.NotFoundAction;
import org.hibernate.cfg.Configuration;
import org.hibernate.cfg.Environment;
import org.hibernate.engine.spi.BatchFetchQueue;
import org.hibernate.engine.spi.EntityKey;
import org.hibernate.engine.spi.SessionImplementor;
import org.hibernate.persister.entity.EntityPersister;
import org.hibernate.resource.jdbc.spi.StatementInspector;
import org.hibernate.testing.junit4.BaseCoreFunctionalTestCase;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import static org.hibernate.testing.transaction.TransactionUtil.doInHibernate;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
/**
* @author Gail Badner
* @author Stephen Fikes
*/
public class BatchFetchNotFoundIgnoreDefaultStyleTest extends BaseCoreFunctionalTestCase {
private static final AStatementInspector statementInspector = new AStatementInspector();
private static final int NUMBER_OF_EMPLOYEES = 8;
private List<Task> tasks = new ArrayList<>();
@Override
protected Class<?>[] getAnnotatedClasses() {
return new Class[] { Employee.class, Task.class };
}
@Override
protected void configure(Configuration configuration) {
super.configure( configuration );
configuration.getProperties().put( Environment.STATEMENT_INSPECTOR, statementInspector );
}
@Before
public void createData() {
tasks.clear();
tasks = doInHibernate(
this::sessionFactory, session -> {
for (int i = 0 ; i < NUMBER_OF_EMPLOYEES ; i++) {
Task task = new Task();
task.id = i;
tasks.add( task );
session.persist( task );
Employee e = new Employee("employee0" + i);
e.task = task;
session.persist(e);
}
return tasks;
}
);
}
@After
public void deleteData() {
doInHibernate(
this::sessionFactory, session -> {
session.createQuery( "delete from Task" ).executeUpdate();
session.createQuery( "delete from Employee" ).executeUpdate();
}
);
}
@Test
public void testSeveralNotFoundFromQuery() {
doInHibernate(
this::sessionFactory, session -> {
// delete 2nd and 8th Task so that the non-found Task entities will be queried
// in 2 different batches.
session.delete( tasks.get( 1 ) );
session.delete( tasks.get( 7 ) );
}
);
statementInspector.clear();
final List<Employee> employees = doInHibernate(
this::sessionFactory, session -> {
List<Employee> results =
session.createQuery( "from Employee e order by e.id", Employee.class ).getResultList();
for ( int i = 0 ; i < tasks.size() ; i++ ) {
checkInBatchFetchQueue( tasks.get( i ).id, session, false );
}
return results;
}
);
final List<Integer> paramterCounts = statementInspector.parameterCounts;
// there should be 4 SQL statements executed
assertEquals( 4, paramterCounts.size() );
// query loading Employee entities shouldn't have any parameters
assertEquals( 0, paramterCounts.get( 0 ).intValue() );
// query specifically for Task with ID == 0 will result in 1st batch;
// query should have 5 parameters for [0,1,2,3,4];
// Task with ID == 1 won't be found; the rest will be found.
assertEquals( 5, paramterCounts.get( 1 ).intValue() );
// query specifically for Task with ID == 1 will result in 2nd batch;
// query should have 4 parameters [1,5,6,7];
// Task with IDs == [1,7] won't be found; the rest will be found.
assertEquals( 4, paramterCounts.get( 2 ).intValue() );
// no extra queries required to load entities with IDs [2,3,4] because they
// were already loaded from 1st batch
// no extra queries required to load entities with IDs [5,6] because they
// were already loaded from 2nd batch
// query specifically for Task with ID == 7 will result in just querying
// Task with ID == 7 (because the batch is empty).
// query should have 1 parameter [7];
// Task with ID == 7 won't be found.
assertEquals( 1, paramterCounts.get( 3 ).intValue() );
assertEquals( NUMBER_OF_EMPLOYEES, employees.size() );
for ( int i = 0 ; i < NUMBER_OF_EMPLOYEES ; i++ ) {
if ( i == 1 || i == 7 ) {
assertNull( employees.get( i ).task );
}
else {
assertEquals( tasks.get( i ).id, employees.get( i ).task.id );
}
}
}
@Test
public void testMostNotFoundFromQuery() {
doInHibernate(
this::sessionFactory, session -> {
// delete all but last Task entity
for ( int i = 0; i < 7; i++ ) {
session.delete( tasks.get( i ) );
}
}
);
statementInspector.clear();
final List<Employee> employees = doInHibernate(
this::sessionFactory, session -> {
List<Employee> results =
session.createQuery( "from Employee e order by e.id", Employee.class ).getResultList();
for ( int i = 0 ; i < tasks.size() ; i++ ) {
checkInBatchFetchQueue( tasks.get( i ).id, session, false );
}
return results;
}
);
final List<Integer> paramterCounts = statementInspector.parameterCounts;
// there should be 8 SQL statements executed
assertEquals( 8, paramterCounts.size() );
// query loading Employee entities shouldn't have any parameters
assertEquals( 0, paramterCounts.get( 0 ).intValue() );
// query specifically for Task with ID == 0 will result in 1st batch;
// query should have 5 parameters for [0,1,2,3,4];
// Task with IDs == [0,1,2,3,4] won't be found
assertEquals( 5, paramterCounts.get( 1 ).intValue() );
// query specifically for Task with ID == 1 will result in 2nd batch;
// query should have 4 parameters [1,5,6,7];
// Task with IDs == [1,5,6] won't be found; Task with ID == 7 will be found.
assertEquals( 4, paramterCounts.get( 2 ).intValue() );
// query specifically for Task with ID == 2 will result in just querying
// Task with ID == 2 (because the batch is empty).
// query should have 1 parameter [2];
// Task with ID == 2 won't be found.
assertEquals( 1, paramterCounts.get( 3 ).intValue() );
// query specifically for Task with ID == 3 will result in just querying
// Task with ID == 3 (because the batch is empty).
// query should have 1 parameter [3];
// Task with ID == 3 won't be found.
assertEquals( 1, paramterCounts.get( 4 ).intValue() );
// query specifically for Task with ID == 4 will result in just querying
// Task with ID == 4 (because the batch is empty).
// query should have 1 parameter [4];
// Task with ID == 4 won't be found.
assertEquals( 1, paramterCounts.get( 5 ).intValue() );
// query specifically for Task with ID == 5 will result in just querying
// Task with ID == 5 (because the batch is empty).
// query should have 1 parameter [5];
// Task with ID == 5 won't be found.
assertEquals( 1, paramterCounts.get( 6 ).intValue() );
// query specifically for Task with ID == 6 will result in just querying
// Task with ID == 6 (because the batch is empty).
// query should have 1 parameter [6];
// Task with ID == 6 won't be found.
assertEquals( 1, paramterCounts.get( 7 ).intValue() );
// no extra queries required to load entity with ID == 7 because it
// was already loaded from 2nd batch
assertEquals( NUMBER_OF_EMPLOYEES, employees.size() );
for ( int i = 0 ; i < NUMBER_OF_EMPLOYEES ; i++ ) {
if ( i == 7 ) {
assertEquals( tasks.get( i ).id, employees.get( i ).task.id );
}
else {
assertNull( employees.get( i ).task );
}
}
}
@Test
public void testNotFoundFromGet() {
doInHibernate(
this::sessionFactory, session -> {
// delete task so it is not found later when getting the Employee.
session.delete( tasks.get( 0 ) );
}
);
statementInspector.clear();
doInHibernate(
this::sessionFactory, session -> {
Employee employee = session.get( Employee.class, "employee00" );
checkInBatchFetchQueue( tasks.get( 0 ).id, session, false );
assertNotNull( employee );
assertNull( employee.task );
}
);
final List<Integer> paramterCounts = statementInspector.parameterCounts;
// there should be 2 SQL statements executed
// 1) query to load Employee entity by ID (associated Tasks is registered for batch loading)
// 2) batch will only contain the ID for the associated Task (which will not be found)
assertEquals( 2, paramterCounts.size() );
// query loading Employee entities shouldn't have any parameters
assertEquals( 1, paramterCounts.get( 0 ).intValue() );
// Will result in just querying a single Task (because the batch is empty).
// query should have 1 parameter;
// Task won't be found.
assertEquals( 1, paramterCounts.get( 1 ).intValue() );
}
private static void checkInBatchFetchQueue(long id, Session session, boolean expected) {
final SessionImplementor sessionImplementor = (SessionImplementor) session;
final EntityPersister persister =
sessionImplementor.getFactory().getMetamodel().entityPersister( Task.class );
final BatchFetchQueue batchFetchQueue =
sessionImplementor.getPersistenceContext().getBatchFetchQueue();
assertEquals( expected, batchFetchQueue.containsEntityKey( new EntityKey( id, persister ) ) );
}
@Entity(name = "Employee")
public static class Employee {
@Id
private String name;
@OneToOne(optional = true)
@JoinColumn(foreignKey = @ForeignKey(value = ConstraintMode.NO_CONSTRAINT))
@NotFound(action = NotFoundAction.IGNORE)
private Task task;
private Employee() {
}
private Employee(String name) {
this.name = name;
}
}
@Entity(name = "Task")
@BatchSize(size = 5)
public static class Task {
@Id
private long id;
public Task() {
}
}
public static class AStatementInspector implements StatementInspector {
private List<Integer> parameterCounts = new ArrayList<>();
public String inspect(String sql) {
parameterCounts.add( countParameters( sql ) );
return sql;
}
private void clear() {
parameterCounts.clear();
}
private int countParameters(String sql) {
int count = 0;
int parameterIndex = sql.indexOf( '?' );
while ( parameterIndex >= 0 ) {
count++;
parameterIndex = sql.indexOf( '?', parameterIndex + 1 );
}
return count;
}
}
}

View File

@ -0,0 +1,23 @@
/*
* 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.batchfetch;
import org.hibernate.cfg.Configuration;
import org.hibernate.cfg.Environment;
import org.hibernate.loader.BatchFetchStyle;
/**
* @author Gail Badner
*/
public class BatchFetchNotFoundIgnoreDynamicStyleTest extends BatchFetchNotFoundIgnoreDefaultStyleTest {
@Override
protected void configure(Configuration configuration) {
super.configure( configuration );
configuration.getProperties().put( Environment.BATCH_FETCH_STYLE, BatchFetchStyle.DYNAMIC );
}
}

View File

@ -0,0 +1,24 @@
/*
* 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.batchfetch;
import org.hibernate.cfg.Configuration;
import org.hibernate.cfg.Environment;
import org.hibernate.loader.BatchFetchStyle;
/**
* @author Gail Badner
*/
public class BatchFetchNotFoundIgnorePaddedStyleTest extends BatchFetchNotFoundIgnoreDefaultStyleTest {
@Override
protected void configure(Configuration configuration) {
super.configure( configuration );
configuration.getProperties().put( Environment.BATCH_FETCH_STYLE, BatchFetchStyle.PADDED );
}
}