HHH-13746 - Implement load-by-multiple-ids using SQL AST
This commit is contained in:
parent
e9920f5489
commit
0ec5af2985
|
@ -148,6 +148,7 @@ public class DynamicBatchingEntityLoaderBuilder extends BatchingEntityLoaderBuil
|
|||
idsInBatch.add( ids[i] );
|
||||
|
||||
if ( idsInBatch.size() >= maxBatchSize ) {
|
||||
// we've hit the allotted max-batch-size, perform an "intermediate load"
|
||||
performOrderedBatchLoad( idsInBatch, lockOptions, persister, session );
|
||||
}
|
||||
|
||||
|
@ -157,6 +158,8 @@ public class DynamicBatchingEntityLoaderBuilder extends BatchingEntityLoaderBuil
|
|||
}
|
||||
|
||||
if ( !idsInBatch.isEmpty() ) {
|
||||
// we still have ids to load from the processing above since the last max-batch-size trigger,
|
||||
// perform a load for them
|
||||
performOrderedBatchLoad( idsInBatch, lockOptions, persister, session );
|
||||
}
|
||||
|
||||
|
|
|
@ -6,24 +6,63 @@
|
|||
*/
|
||||
package org.hibernate.loader.internal;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.lang.reflect.Array;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
|
||||
import org.hibernate.LockMode;
|
||||
import org.hibernate.LockOptions;
|
||||
import org.hibernate.dialect.Dialect;
|
||||
import org.hibernate.engine.jdbc.env.spi.JdbcEnvironment;
|
||||
import org.hibernate.engine.jdbc.spi.JdbcServices;
|
||||
import org.hibernate.engine.spi.EntityEntry;
|
||||
import org.hibernate.engine.spi.EntityKey;
|
||||
import org.hibernate.engine.spi.PersistenceContext;
|
||||
import org.hibernate.engine.spi.SessionFactoryImplementor;
|
||||
import org.hibernate.engine.spi.SharedSessionContractImplementor;
|
||||
import org.hibernate.loader.entity.DynamicBatchingEntityLoaderBuilder;
|
||||
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.collections.CollectionHelper;
|
||||
import org.hibernate.loader.entity.CacheEntityLoaderHelper;
|
||||
import org.hibernate.loader.spi.MultiIdEntityLoader;
|
||||
import org.hibernate.metamodel.mapping.JdbcMapping;
|
||||
import org.hibernate.persister.entity.EntityPersister;
|
||||
import org.hibernate.persister.entity.MultiLoadOptions;
|
||||
import org.hibernate.persister.entity.OuterJoinLoadable;
|
||||
import org.hibernate.query.spi.QueryOptions;
|
||||
import org.hibernate.query.spi.QueryParameterBindings;
|
||||
import org.hibernate.sql.ast.Clause;
|
||||
import org.hibernate.sql.ast.SqlAstTranslatorFactory;
|
||||
import org.hibernate.sql.ast.tree.select.SelectStatement;
|
||||
import org.hibernate.sql.exec.internal.JdbcParameterBindingsImpl;
|
||||
import org.hibernate.sql.exec.internal.JdbcSelectExecutorStandardImpl;
|
||||
import org.hibernate.sql.exec.spi.Callback;
|
||||
import org.hibernate.sql.exec.spi.ExecutionContext;
|
||||
import org.hibernate.sql.exec.spi.JdbcParameter;
|
||||
import org.hibernate.sql.exec.spi.JdbcParameterBinding;
|
||||
import org.hibernate.sql.exec.spi.JdbcParameterBindings;
|
||||
import org.hibernate.sql.exec.spi.JdbcSelect;
|
||||
import org.hibernate.sql.results.internal.RowTransformerPassThruImpl;
|
||||
|
||||
import org.jboss.logging.Logger;
|
||||
|
||||
/**
|
||||
* @author Steve Ebersole
|
||||
*/
|
||||
public class MultiIdEntityLoaderStandardImpl<T> implements MultiIdEntityLoader<T> {
|
||||
private final EntityPersister entityDescriptor;
|
||||
private static final Logger log = Logger.getLogger( MultiIdEntityLoaderStandardImpl.class );
|
||||
|
||||
public MultiIdEntityLoaderStandardImpl(EntityPersister entityDescriptor) {
|
||||
private final EntityPersister entityDescriptor;
|
||||
private final SessionFactoryImplementor sessionFactory;
|
||||
|
||||
private int idJdbcTypeCount = -1;
|
||||
|
||||
public MultiIdEntityLoaderStandardImpl(EntityPersister entityDescriptor, SessionFactoryImplementor sessionFactory) {
|
||||
this.entityDescriptor = entityDescriptor;
|
||||
this.sessionFactory = sessionFactory;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -33,13 +72,364 @@ public class MultiIdEntityLoaderStandardImpl<T> implements MultiIdEntityLoader<T
|
|||
|
||||
@Override
|
||||
public List<T> load(Object[] ids, MultiLoadOptions loadOptions, SharedSessionContractImplementor session) {
|
||||
// todo (6.0) : account for all of the `loadOptions`...
|
||||
// for now just do a simple load
|
||||
|
||||
assert ids != null;
|
||||
|
||||
if ( idJdbcTypeCount < 0 ) {
|
||||
// can't do this in the ctor because of chicken-egg between this ctor and the persisters
|
||||
idJdbcTypeCount = entityDescriptor.getIdentifierMapping().getJdbcTypeCount( sessionFactory.getTypeConfiguration() );
|
||||
}
|
||||
|
||||
if ( loadOptions.isOrderReturnEnabled() ) {
|
||||
return performOrderedMultiLoad( ids, session, loadOptions );
|
||||
}
|
||||
else {
|
||||
return performUnorderedMultiLoad( ids, session, loadOptions );
|
||||
}
|
||||
}
|
||||
|
||||
private List<T> performOrderedMultiLoad(
|
||||
Object[] ids,
|
||||
SharedSessionContractImplementor session,
|
||||
MultiLoadOptions loadOptions) {
|
||||
log.tracef( "#performOrderedMultiLoad(`%s`, ..)", entityDescriptor.getEntityName() );
|
||||
|
||||
assert loadOptions.isOrderReturnEnabled();
|
||||
|
||||
final JdbcEnvironment jdbcEnvironment = sessionFactory.getJdbcServices().getJdbcEnvironment();
|
||||
final Dialect dialect = jdbcEnvironment.getDialect();
|
||||
|
||||
final List result = CollectionHelper.arrayList( ids.length );
|
||||
|
||||
final LockOptions lockOptions = (loadOptions.getLockOptions() == null)
|
||||
? new LockOptions( LockMode.NONE )
|
||||
: loadOptions.getLockOptions();
|
||||
|
||||
final int maxBatchSize;
|
||||
if ( loadOptions.getBatchSize() != null && loadOptions.getBatchSize() > 0 ) {
|
||||
maxBatchSize = loadOptions.getBatchSize();
|
||||
}
|
||||
else {
|
||||
maxBatchSize = dialect.getDefaultBatchLoadSizingStrategy().determineOptimalBatchLoadSize(
|
||||
idJdbcTypeCount,
|
||||
ids.length
|
||||
);
|
||||
}
|
||||
|
||||
final List<Object> idsInBatch = new ArrayList<>();
|
||||
final List<Integer> elementPositionsLoadedByBatch = new ArrayList<>();
|
||||
|
||||
for ( int i = 0; i < ids.length; i++ ) {
|
||||
final Object id = ids[i];
|
||||
final EntityKey entityKey = new EntityKey( id, entityDescriptor );
|
||||
|
||||
if ( loadOptions.isSessionCheckingEnabled() || loadOptions.isSecondLevelCacheCheckingEnabled() ) {
|
||||
LoadEvent loadEvent = new LoadEvent(
|
||||
id,
|
||||
entityDescriptor.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
|
||||
//noinspection unchecked
|
||||
result.add( i, null );
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if ( managedEntity == null && loadOptions.isSecondLevelCacheCheckingEnabled() ) {
|
||||
// look for it in the SessionFactory
|
||||
managedEntity = CacheEntityLoaderHelper.INSTANCE.loadFromSecondLevelCache(
|
||||
loadEvent,
|
||||
entityDescriptor,
|
||||
entityKey
|
||||
);
|
||||
}
|
||||
|
||||
if ( managedEntity != null ) {
|
||||
//noinspection unchecked
|
||||
result.add( i, managedEntity );
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// if we did not hit any of the continues above, then we need to batch
|
||||
// load the entity state.
|
||||
idsInBatch.add( ids[i] );
|
||||
|
||||
if ( idsInBatch.size() >= maxBatchSize ) {
|
||||
// we've hit the allotted max-batch-size, perform an "intermediate load"
|
||||
loadEntitiesById( idsInBatch, lockOptions, session );
|
||||
}
|
||||
|
||||
// Save the EntityKey instance for use later!
|
||||
// todo (6.0) : see below wrt why `elementPositionsLoadedByBatch` probably isn't needed
|
||||
result.add( i, entityKey );
|
||||
elementPositionsLoadedByBatch.add( i );
|
||||
}
|
||||
|
||||
if ( !idsInBatch.isEmpty() ) {
|
||||
// we still have ids to load from the processing above since the last max-batch-size trigger,
|
||||
// perform a load for them
|
||||
loadEntitiesById( idsInBatch, lockOptions, session );
|
||||
}
|
||||
|
||||
// todo (6.0) : can't we just walk all elements of the results looking for EntityKey and replacing here?
|
||||
// can't imagine
|
||||
final PersistenceContext persistenceContext = session.getPersistenceContextInternal();
|
||||
for ( Integer position : elementPositionsLoadedByBatch ) {
|
||||
// the element value at this position in the result List should be
|
||||
// the EntityKey for that entity; reuse it!
|
||||
final EntityKey entityKey = (EntityKey) result.get( position );
|
||||
Object entity = persistenceContext.getEntity( entityKey );
|
||||
if ( entity != null && !loadOptions.isReturnOfDeletedEntitiesEnabled() ) {
|
||||
// make sure it is not DELETED
|
||||
final EntityEntry entry = persistenceContext.getEntry( entity );
|
||||
if ( entry.getStatus() == Status.DELETED || entry.getStatus() == Status.GONE ) {
|
||||
// the entity is locally deleted, and the options ask that we not return such entities...
|
||||
entity = null;
|
||||
}
|
||||
}
|
||||
//noinspection unchecked
|
||||
result.set( position, entity );
|
||||
}
|
||||
|
||||
//noinspection unchecked
|
||||
return DynamicBatchingEntityLoaderBuilder.INSTANCE.multiLoad(
|
||||
(OuterJoinLoadable) entityDescriptor,
|
||||
(Serializable[]) ids,
|
||||
session,
|
||||
loadOptions
|
||||
return (List) result;
|
||||
}
|
||||
|
||||
private List<T> loadEntitiesById(
|
||||
List<Object> idsInBatch,
|
||||
LockOptions lockOptions,
|
||||
SharedSessionContractImplementor session) {
|
||||
assert idsInBatch != null;
|
||||
assert ! idsInBatch.isEmpty();
|
||||
|
||||
assert idJdbcTypeCount > 0;
|
||||
|
||||
final int numberOfIdsInBatch = idsInBatch.size();
|
||||
|
||||
log.tracef( "#loadEntitiesById(`%s`, `%s`, ..)", entityDescriptor.getEntityName(), numberOfIdsInBatch );
|
||||
|
||||
final List<JdbcParameter> jdbcParameters = new ArrayList<>( numberOfIdsInBatch * idJdbcTypeCount);
|
||||
|
||||
final SelectStatement sqlAst = MetamodelSelectBuilderProcess.createSelect(
|
||||
getLoadable(),
|
||||
// null here means to select everything
|
||||
null,
|
||||
getLoadable().getIdentifierMapping(),
|
||||
null,
|
||||
numberOfIdsInBatch,
|
||||
session.getLoadQueryInfluencers(),
|
||||
lockOptions,
|
||||
jdbcParameters::add,
|
||||
sessionFactory
|
||||
);
|
||||
|
||||
final JdbcServices jdbcServices = sessionFactory.getJdbcServices();
|
||||
final JdbcEnvironment jdbcEnvironment = jdbcServices.getJdbcEnvironment();
|
||||
final SqlAstTranslatorFactory sqlAstTranslatorFactory = jdbcEnvironment.getSqlAstTranslatorFactory();
|
||||
|
||||
final JdbcSelect jdbcSelect = sqlAstTranslatorFactory.buildSelectTranslator( sessionFactory ).translate( sqlAst );
|
||||
|
||||
final JdbcParameterBindings jdbcParameterBindings = new JdbcParameterBindingsImpl( jdbcParameters.size() );
|
||||
final Iterator<JdbcParameter> paramItr = jdbcParameters.iterator();
|
||||
|
||||
for ( int i = 0; i < numberOfIdsInBatch; i++ ) {
|
||||
final Object id = idsInBatch.get( i );
|
||||
|
||||
entityDescriptor.getIdentifierMapping().visitJdbcValues(
|
||||
id,
|
||||
Clause.WHERE,
|
||||
(value, type) -> {
|
||||
assert paramItr.hasNext();
|
||||
final JdbcParameter parameter = paramItr.next();
|
||||
jdbcParameterBindings.addBinding(
|
||||
parameter,
|
||||
new JdbcParameterBinding() {
|
||||
@Override
|
||||
public JdbcMapping getBindType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getBindValue() {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
);
|
||||
},
|
||||
session
|
||||
);
|
||||
}
|
||||
|
||||
// we should have used all of the JbdcParameter references (created bindings for all)
|
||||
assert !paramItr.hasNext();
|
||||
|
||||
return JdbcSelectExecutorStandardImpl.INSTANCE.list(
|
||||
jdbcSelect,
|
||||
jdbcParameterBindings,
|
||||
new ExecutionContext() {
|
||||
@Override
|
||||
public SharedSessionContractImplementor getSession() {
|
||||
return session;
|
||||
}
|
||||
|
||||
@Override
|
||||
public QueryOptions getQueryOptions() {
|
||||
return QueryOptions.NONE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public QueryParameterBindings getQueryParameterBindings() {
|
||||
return QueryParameterBindings.NO_PARAM_BINDINGS;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Callback getCallback() {
|
||||
return null;
|
||||
}
|
||||
},
|
||||
RowTransformerPassThruImpl.instance()
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
private List<T> performUnorderedMultiLoad(
|
||||
Object[] ids,
|
||||
SharedSessionContractImplementor session,
|
||||
MultiLoadOptions loadOptions) {
|
||||
assert !loadOptions.isOrderReturnEnabled();
|
||||
assert ids != null;
|
||||
|
||||
log.tracef( "#performUnorderedMultiLoad(`%s`, ..)", entityDescriptor.getEntityName() );
|
||||
|
||||
final List<T> result = CollectionHelper.arrayList( ids.length );
|
||||
|
||||
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
|
||||
// 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<Object> nonManagedIds = new ArrayList<>();
|
||||
|
||||
for ( int i = 0; i < ids.length; i++ ) {
|
||||
final Object id = ids[ i ];
|
||||
final EntityKey entityKey = new EntityKey( id, entityDescriptor );
|
||||
|
||||
LoadEvent loadEvent = new LoadEvent(
|
||||
id,
|
||||
entityDescriptor.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,
|
||||
entityDescriptor,
|
||||
entityKey
|
||||
);
|
||||
}
|
||||
|
||||
if ( managedEntity != null ) {
|
||||
foundAnyManagedEntities = true;
|
||||
//noinspection unchecked
|
||||
result.add( (T) 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(
|
||||
(Object[]) Array.newInstance(
|
||||
ids.getClass().getComponentType(),
|
||||
nonManagedIds.size()
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int numberOfIdsLeft = ids.length;
|
||||
final int maxBatchSize;
|
||||
if ( loadOptions.getBatchSize() != null && loadOptions.getBatchSize() > 0 ) {
|
||||
maxBatchSize = loadOptions.getBatchSize();
|
||||
}
|
||||
else {
|
||||
maxBatchSize = session.getJdbcServices().getJdbcEnvironment().getDialect().getDefaultBatchLoadSizingStrategy().determineOptimalBatchLoadSize(
|
||||
entityDescriptor.getIdentifierType().getColumnSpan( session.getFactory() ),
|
||||
numberOfIdsLeft
|
||||
);
|
||||
}
|
||||
|
||||
int idPosition = 0;
|
||||
while ( numberOfIdsLeft > 0 ) {
|
||||
final int batchSize = Math.min( numberOfIdsLeft, maxBatchSize );
|
||||
|
||||
final Object[] idsInBatch = new Object[ batchSize ];
|
||||
System.arraycopy( ids, idPosition, idsInBatch, 0, batchSize );
|
||||
|
||||
result.addAll(
|
||||
loadEntitiesById( Arrays.asList( idsInBatch ), lockOptions, session )
|
||||
);
|
||||
|
||||
numberOfIdsLeft = numberOfIdsLeft - batchSize;
|
||||
idPosition += batchSize;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -705,7 +705,11 @@ public abstract class AbstractEntityPersister
|
|||
singleIdEntityLoader = new SingleIdEntityLoaderStandardImpl( this, factory );
|
||||
}
|
||||
|
||||
multiIdEntityLoader = new MultiIdEntityLoaderStandardImpl( this );
|
||||
// todo (6.0) : allow a "max entities" to be passed (or determine based on Dialect?) indicating how many entities
|
||||
// to load at once. i.e. it limits the number of the generated IN-list JDBC-parameters in a given
|
||||
// PreparedStatement, opting to split the load into multiple JDBC operations to work around database
|
||||
// limits on number of parameters, number of IN-list values, etc
|
||||
multiIdEntityLoader = new MultiIdEntityLoaderStandardImpl( this, factory );
|
||||
|
||||
naturalIdLoader = bootDescriptor.hasNaturalId()
|
||||
? new NaturalIdLoaderStandardImpl( this )
|
||||
|
|
|
@ -0,0 +1,225 @@
|
|||
/*
|
||||
* 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.orm.test.loading;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
|
||||
import org.hibernate.testing.orm.domain.StandardDomainModel;
|
||||
import org.hibernate.testing.orm.domain.gambit.BasicEntity;
|
||||
import org.hibernate.testing.orm.junit.DomainModel;
|
||||
import org.hibernate.testing.orm.junit.SessionFactory;
|
||||
import org.hibernate.testing.orm.junit.SessionFactoryFunctionalTesting;
|
||||
import org.hibernate.testing.orm.junit.SessionFactoryScope;
|
||||
import org.junit.jupiter.api.AfterAll;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.hamcrest.BaseMatcher;
|
||||
import org.hamcrest.CoreMatchers;
|
||||
import org.hamcrest.Description;
|
||||
|
||||
import static org.hamcrest.CoreMatchers.*;
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
|
||||
/**
|
||||
* @author Steve Ebersole
|
||||
*/
|
||||
@DomainModel(
|
||||
standardModels = StandardDomainModel.GAMBIT
|
||||
)
|
||||
@SessionFactory
|
||||
@SessionFactoryFunctionalTesting
|
||||
@SuppressWarnings("WeakerAccess")
|
||||
public class MultiIdEntityLoadTests {
|
||||
@Test
|
||||
public void testBasicEntitySimpleLoad(SessionFactoryScope scope) {
|
||||
scope.inTransaction(
|
||||
session -> {
|
||||
final List<BasicEntity> results = session.byMultipleIds( BasicEntity.class ).multiLoad( 1, 3 );
|
||||
assertThat( results.size(), is( 2 ) );
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testBasicEntityOrderedLoad(SessionFactoryScope scope) {
|
||||
scope.inTransaction(
|
||||
session -> {
|
||||
final List<BasicEntity> results = session.byMultipleIds( BasicEntity.class )
|
||||
.enableOrderedReturn( true )
|
||||
.multiLoad( 3, 1 );
|
||||
assertThat( results.size(), is( 2 ) );
|
||||
// the problem with asserting this is that its in-determinate based on the order we get them back from the database
|
||||
assertThat( results.get( 0 ).getId(), is( 3 ) );
|
||||
assertThat( results.get( 1 ).getId(), is( 1 ) );
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testBasicEntityOrderedDeleteCheckLoad(SessionFactoryScope scope) {
|
||||
// using ordered results
|
||||
scope.inTransaction(
|
||||
session -> {
|
||||
session.delete( session.load( BasicEntity.class, 2 ) );
|
||||
|
||||
// test control - no delete-checking
|
||||
{
|
||||
final List<BasicEntity> results = session.byMultipleIds( BasicEntity.class )
|
||||
.enableOrderedReturn( true )
|
||||
.enableReturnOfDeletedEntities( true )
|
||||
.multiLoad( 3, 2, 1 );
|
||||
assertThat( results.size(), is( 3 ) );
|
||||
assertThat( results.get( 0 ).getId(), is( 3 ) );
|
||||
assertThat( results.get( 1 ).getId(), is( 2 ) );
|
||||
assertThat( results.get( 2 ).getId(), is( 1 ) );
|
||||
}
|
||||
|
||||
// now apply delete-checking
|
||||
{
|
||||
final List<BasicEntity> results = session.byMultipleIds( BasicEntity.class )
|
||||
.enableOrderedReturn( true )
|
||||
.enableReturnOfDeletedEntities( false )
|
||||
.multiLoad( 3, 2, 1 );
|
||||
// we should still get 3 results
|
||||
assertThat( results.size(), is( 3 ) );
|
||||
assertThat( results.get( 0 ).getId(), is( 3 ) );
|
||||
// however, the second one should now be null
|
||||
assertThat( results.get( 1 ), nullValue() );
|
||||
assertThat( results.get( 2 ).getId(), is( 1 ) );
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
// todo (6.0) : consider a bunch of collection-based hamcrest Matchers asserting "is initialized", for example:
|
||||
// `assertThat( results, CollectionMatchers.isInitialized() )`
|
||||
//
|
||||
// todo (6.0) : ^^ another useful one would be a composite matcher checking the elements, e.g.:
|
||||
// ````
|
||||
// assertThat(
|
||||
// results,
|
||||
// CollectionMatchers.all( e -> notNullValue( e ) )
|
||||
// .and( e -> isInitialized() )
|
||||
// ....
|
||||
// );
|
||||
// ````
|
||||
|
||||
@Test
|
||||
public void testBasicEntityUnOrderedDeleteCheckLoad(SessionFactoryScope scope) {
|
||||
|
||||
// using un-ordered results
|
||||
scope.inTransaction(
|
||||
session -> {
|
||||
session.delete( session.load( BasicEntity.class, 2 ) );
|
||||
|
||||
// test control - no delete-checking
|
||||
{
|
||||
final List<BasicEntity> results = session.byMultipleIds( BasicEntity.class )
|
||||
.enableReturnOfDeletedEntities( true )
|
||||
.multiLoad( 3, 2, 1 );
|
||||
|
||||
assertThat( results.size(), is( 3 ) );
|
||||
|
||||
assertThat(
|
||||
results,
|
||||
HasNullElementsMatcher.hasNoNullElements()
|
||||
);
|
||||
}
|
||||
|
||||
// now apply delete-checking
|
||||
{
|
||||
final List<BasicEntity> results = session.byMultipleIds( BasicEntity.class )
|
||||
.enableReturnOfDeletedEntities( false )
|
||||
.multiLoad( 3, 2, 1 );
|
||||
// we should still get 3 results
|
||||
assertThat( results.size(), is( 3 ) );
|
||||
|
||||
// however, we should now have a null element for the deleted entity
|
||||
assertThat(
|
||||
results,
|
||||
HasNullElementsMatcher.hasNullElements()
|
||||
);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@BeforeEach
|
||||
public void prepareTestData(SessionFactoryScope scope) {
|
||||
scope.inTransaction(
|
||||
session -> {
|
||||
final BasicEntity first = new BasicEntity( 1, "first" );
|
||||
final BasicEntity second = new BasicEntity( 2, "second" );
|
||||
final BasicEntity third = new BasicEntity( 3, "third" );
|
||||
session.save( first );
|
||||
session.save( second );
|
||||
session.save( third );
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
public void dropTestData(SessionFactoryScope scope) {
|
||||
scope.inTransaction(
|
||||
session -> session.createQuery( "delete BasicEntity" ).executeUpdate()
|
||||
);
|
||||
}
|
||||
|
||||
private static class HasNullElementsMatcher<C extends Collection<?>> extends BaseMatcher<C> {
|
||||
public static final HasNullElementsMatcher INSTANCE = new HasNullElementsMatcher( false );
|
||||
public static final HasNullElementsMatcher NEGATED_INSTANCE = new HasNullElementsMatcher( true );
|
||||
|
||||
public static <X extends Collection<?>> HasNullElementsMatcher<X> hasNullElements() {
|
||||
//noinspection unchecked
|
||||
return INSTANCE;
|
||||
}
|
||||
|
||||
public static <X extends Collection<?>> HasNullElementsMatcher<X> hasNoNullElements() {
|
||||
//noinspection unchecked
|
||||
return NEGATED_INSTANCE;
|
||||
}
|
||||
|
||||
private final boolean negated;
|
||||
|
||||
public HasNullElementsMatcher(boolean negated) {
|
||||
this.negated = negated;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean matches(Object item) {
|
||||
assertThat( item, instanceOf( Collection.class ) );
|
||||
|
||||
//noinspection unchecked
|
||||
C collection = (C) item;
|
||||
|
||||
if ( negated ) {
|
||||
// check no-null-elements - if any is null, this check fails
|
||||
collection.forEach( e -> assertThat( e, notNullValue() ) );
|
||||
return true;
|
||||
}
|
||||
|
||||
boolean foundOne = false;
|
||||
for ( Object e : collection ) {
|
||||
if ( e == null ) {
|
||||
foundOne = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return foundOne;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void describeTo(Description description) {
|
||||
description.appendText( "had null elements" );
|
||||
}
|
||||
}
|
||||
}
|
|
@ -21,6 +21,12 @@ import org.junit.jupiter.api.extension.TestInstancePostProcessor;
|
|||
import org.junit.platform.commons.support.AnnotationSupport;
|
||||
|
||||
/**
|
||||
* hibernate-testing implementation of a few JUnit5 contracts to support SessionFactory-based testing,
|
||||
* including argument injection (or see {@link DomainModelScopeAware})
|
||||
*
|
||||
* @see ServiceRegistryScope
|
||||
* @see DomainModelExtension
|
||||
*
|
||||
* @author Steve Ebersole
|
||||
*/
|
||||
public class DomainModelExtension
|
||||
|
|
|
@ -36,6 +36,12 @@ import org.junit.platform.commons.support.AnnotationSupport;
|
|||
import org.jboss.logging.Logger;
|
||||
|
||||
/**
|
||||
* hibernate-testing implementation of a few JUnit5 contracts to support SessionFactory-based testing,
|
||||
* including argument injection (or see {@link SessionFactoryScopeAware})
|
||||
*
|
||||
* @see SessionFactoryScope
|
||||
* @see DomainModelExtension
|
||||
*
|
||||
* @author Steve Ebersole
|
||||
*/
|
||||
public class SessionFactoryExtension
|
||||
|
|
|
@ -16,14 +16,14 @@ import org.junit.jupiter.api.TestInstance;
|
|||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
|
||||
/**
|
||||
* Composite annotation for functional tests that
|
||||
* require a functioning SessionFactory.
|
||||
* Composite annotation for functional tests that require a functioning SessionFactory.
|
||||
*
|
||||
* @apiNote Logically this should also include
|
||||
* `@TestInstance( TestInstance.Lifecycle.PER_CLASS )`
|
||||
* but that annotation is not conveyed (is that the
|
||||
* right word? its not applied to the thing using this annotation).
|
||||
* Test classes should apply that themselves.
|
||||
* @apiNote Applies support for SessionFactory-based testing. Up to the test to define
|
||||
* configuration (via {@link ServiceRegistry}), mappings (via {@link DomainModel}) and/or
|
||||
* SessionFactory-options (via {@link SessionFactory}). Rather than using these other
|
||||
* annotations, tests could just implement building those individual pieces via
|
||||
* {@link ServiceRegistryProducer}, {@link DomainModelProducer} and/or {@link SessionFactoryProducer}
|
||||
* instead.
|
||||
*
|
||||
* @see SessionFactoryExtension
|
||||
* @see DialectFilterExtension
|
||||
|
@ -39,6 +39,6 @@ import org.junit.jupiter.api.extension.ExtendWith;
|
|||
|
||||
@DomainModelFunctionalTesting
|
||||
|
||||
@ExtendWith(SessionFactoryExtension.class )
|
||||
@ExtendWith( SessionFactoryExtension.class )
|
||||
public @interface SessionFactoryFunctionalTesting {
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue