HHH-13746 - Implement load-by-multiple-ids using SQL AST

This commit is contained in:
Steve Ebersole 2019-11-23 13:39:48 -06:00
parent e9920f5489
commit 0ec5af2985
7 changed files with 653 additions and 19 deletions

View File

@ -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 );
}

View File

@ -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;
}
}

View File

@ -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 )

View File

@ -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" );
}
}
}

View File

@ -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

View File

@ -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

View File

@ -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 {
}