From 0ec5af29855f3483e4fdac346188c20bfd665e62 Mon Sep 17 00:00:00 2001 From: Steve Ebersole Date: Sat, 23 Nov 2019 13:39:48 -0600 Subject: [PATCH] HHH-13746 - Implement load-by-multiple-ids using SQL AST --- .../DynamicBatchingEntityLoaderBuilder.java | 3 + .../MultiIdEntityLoaderStandardImpl.java | 410 +++++++++++++++++- .../entity/AbstractEntityPersister.java | 6 +- .../test/loading/MultiIdEntityLoadTests.java | 225 ++++++++++ .../orm/junit/DomainModelExtension.java | 6 + .../orm/junit/SessionFactoryExtension.java | 6 + .../SessionFactoryFunctionalTesting.java | 16 +- 7 files changed, 653 insertions(+), 19 deletions(-) create mode 100644 hibernate-core/src/test/java/org/hibernate/orm/test/loading/MultiIdEntityLoadTests.java 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 b8a61eea06..98d71459a0 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 @@ -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 ); } diff --git a/hibernate-core/src/main/java/org/hibernate/loader/internal/MultiIdEntityLoaderStandardImpl.java b/hibernate-core/src/main/java/org/hibernate/loader/internal/MultiIdEntityLoaderStandardImpl.java index c486ece8b2..7802474c6b 100644 --- a/hibernate-core/src/main/java/org/hibernate/loader/internal/MultiIdEntityLoaderStandardImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/loader/internal/MultiIdEntityLoaderStandardImpl.java @@ -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 implements MultiIdEntityLoader { - 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 implements MultiIdEntityLoader 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 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 idsInBatch = new ArrayList<>(); + final List 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 loadEntitiesById( + List 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 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 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 performUnorderedMultiLoad( + Object[] ids, + SharedSessionContractImplementor session, + MultiLoadOptions loadOptions) { + assert !loadOptions.isOrderReturnEnabled(); + assert ids != null; + + log.tracef( "#performUnorderedMultiLoad(`%s`, ..)", entityDescriptor.getEntityName() ); + + final List 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 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; + } } diff --git a/hibernate-core/src/main/java/org/hibernate/persister/entity/AbstractEntityPersister.java b/hibernate-core/src/main/java/org/hibernate/persister/entity/AbstractEntityPersister.java index 4bb585c16b..994845a8d7 100644 --- a/hibernate-core/src/main/java/org/hibernate/persister/entity/AbstractEntityPersister.java +++ b/hibernate-core/src/main/java/org/hibernate/persister/entity/AbstractEntityPersister.java @@ -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 ) diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/loading/MultiIdEntityLoadTests.java b/hibernate-core/src/test/java/org/hibernate/orm/test/loading/MultiIdEntityLoadTests.java new file mode 100644 index 0000000000..6abc3ef2b6 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/loading/MultiIdEntityLoadTests.java @@ -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 results = session.byMultipleIds( BasicEntity.class ).multiLoad( 1, 3 ); + assertThat( results.size(), is( 2 ) ); + } + ); + } + + @Test + public void testBasicEntityOrderedLoad(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + final List 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 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 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 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 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> extends BaseMatcher { + public static final HasNullElementsMatcher INSTANCE = new HasNullElementsMatcher( false ); + public static final HasNullElementsMatcher NEGATED_INSTANCE = new HasNullElementsMatcher( true ); + + public static > HasNullElementsMatcher hasNullElements() { + //noinspection unchecked + return INSTANCE; + } + + public static > HasNullElementsMatcher 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" ); + } + } +} diff --git a/hibernate-testing/src/main/java/org/hibernate/testing/orm/junit/DomainModelExtension.java b/hibernate-testing/src/main/java/org/hibernate/testing/orm/junit/DomainModelExtension.java index ce197a7b8d..9bf99d3523 100644 --- a/hibernate-testing/src/main/java/org/hibernate/testing/orm/junit/DomainModelExtension.java +++ b/hibernate-testing/src/main/java/org/hibernate/testing/orm/junit/DomainModelExtension.java @@ -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 diff --git a/hibernate-testing/src/main/java/org/hibernate/testing/orm/junit/SessionFactoryExtension.java b/hibernate-testing/src/main/java/org/hibernate/testing/orm/junit/SessionFactoryExtension.java index 3bbb9c4e5a..3e79e6ac02 100644 --- a/hibernate-testing/src/main/java/org/hibernate/testing/orm/junit/SessionFactoryExtension.java +++ b/hibernate-testing/src/main/java/org/hibernate/testing/orm/junit/SessionFactoryExtension.java @@ -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 diff --git a/hibernate-testing/src/main/java/org/hibernate/testing/orm/junit/SessionFactoryFunctionalTesting.java b/hibernate-testing/src/main/java/org/hibernate/testing/orm/junit/SessionFactoryFunctionalTesting.java index d29580da53..07f476ee98 100644 --- a/hibernate-testing/src/main/java/org/hibernate/testing/orm/junit/SessionFactoryFunctionalTesting.java +++ b/hibernate-testing/src/main/java/org/hibernate/testing/orm/junit/SessionFactoryFunctionalTesting.java @@ -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 { }