HHH-18375 Reuse previous row state when result cardinality is duplicated

This commit is contained in:
Christian Beikov 2024-07-11 13:19:06 +02:00
parent 228bd7958f
commit 505092e4ea
22 changed files with 939 additions and 40 deletions

View File

@ -21,6 +21,10 @@ public interface AssemblerCreationState {
return false;
}
default boolean containsMultipleCollectionFetches() {
return true;
}
int acquireInitializerId();
Initializer<?> resolveInitializer(

View File

@ -79,4 +79,20 @@ public interface FetchList extends Iterable<Fetch> {
return false;
}
default int getCollectionFetchesCount() {
int collectionFetchesCount = 0;
for ( Fetch fetch : this ) {
if ( fetch instanceof EagerCollectionFetch ) {
collectionFetchesCount++;
}
else {
final FetchParent fetchParent = fetch.asFetchParent();
if ( fetchParent != null ) {
collectionFetchesCount += fetchParent.getCollectionFetchesCount();
}
}
}
return collectionFetchesCount;
}
}

View File

@ -95,6 +95,10 @@ public interface FetchParent extends DomainResultGraphNode {
boolean containsCollectionFetches();
default int getCollectionFetchesCount() {
return getFetches().getCollectionFetchesCount();
}
@Override
default void collectValueIndexesToCache(BitSet valueIndexes) {
for ( Fetch fetch : getFetches() ) {

View File

@ -86,7 +86,7 @@ public interface Initializer<Data extends InitializerData> {
// by default - nothing to do
/**
* Step 1 - Resolve the key value for this initializer for the current
* Step 1.1 - Resolve the key value for this initializer for the current
* row and then recurse to the sub-initializers.
*
* After this point, the initializer knows whether further processing is necessary
@ -98,6 +98,20 @@ public interface Initializer<Data extends InitializerData> {
resolveKey( getData( rowProcessingState ) );
}
/**
* Step 1.2 - Special variant of {@link #resolveKey(InitializerData)} that allows the reuse of key value
* and instance value from the previous row.
*
* @implSpec Defaults to simply delegating to {@link #resolveKey(InitializerData)}.
*/
default void resolveFromPreviousRow(Data data) {
resolveKey( data );
}
default void resolveFromPreviousRow(RowProcessingState rowProcessingState) {
resolveFromPreviousRow( getData( rowProcessingState ) );
}
/**
* Step 2.1 - Using the key resolved in {@link #resolveKey}, resolve the
* instance (of the thing initialized) to use for the current row.

View File

@ -7,6 +7,7 @@
package org.hibernate.sql.results.graph.collection;
import java.util.List;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
import org.hibernate.collection.spi.PersistentCollection;
@ -44,6 +45,13 @@ public interface LoadingCollectionEntry {
*/
void load(Consumer<List<Object>> loadingEntryConsumer);
/**
* Callback for row loading. Allows delayed List creation
*/
default <T> void load(T arg1, BiConsumer<T, List<Object>> loadingEntryConsumer) {
load( list -> loadingEntryConsumer.accept( arg1, list ) );
}
/**
* Complete the load
*/

View File

@ -117,6 +117,24 @@ public abstract class AbstractCollectionInitializer<Data extends AbstractCollect
}
}
@Override
public void resolveFromPreviousRow(Data data) {
if ( data.getState() == State.UNINITIALIZED ) {
if ( data.collectionKey == null ) {
setMissing( data );
}
else {
if ( collectionKeyResultAssembler != null ) {
final Initializer<?> initializer = collectionKeyResultAssembler.getInitializer();
if ( initializer != null ) {
initializer.resolveFromPreviousRow( data.getRowProcessingState() );
}
}
data.setState( State.RESOLVED );
}
}
}
protected void setMissing(Data data) {
data.setState( State.MISSING );
data.collectionKey = null;

View File

@ -12,7 +12,6 @@ import java.util.function.BiConsumer;
import org.hibernate.LockMode;
import org.hibernate.collection.spi.CollectionSemantics;
import org.hibernate.collection.spi.PersistentCollection;
import org.hibernate.engine.spi.CollectionKey;
import org.hibernate.engine.spi.PersistenceContext;
import org.hibernate.engine.spi.SharedSessionContractImplementor;
import org.hibernate.internal.log.LoggingHelper;
@ -27,7 +26,6 @@ import org.hibernate.sql.results.graph.Initializer;
import org.hibernate.sql.results.graph.InitializerData;
import org.hibernate.sql.results.graph.InitializerParent;
import org.hibernate.sql.results.graph.collection.LoadingCollectionEntry;
import org.hibernate.sql.results.graph.entity.internal.EntityInitializerImpl;
import org.hibernate.sql.results.internal.LoadingCollectionEntryImpl;
import org.hibernate.sql.results.jdbc.spi.RowProcessingState;
@ -42,12 +40,12 @@ import org.checkerframework.checker.nullness.qual.Nullable;
* @implNote Mainly an intention contract wrt the immediacy of the fetch.
*/
public abstract class AbstractImmediateCollectionInitializer<Data extends AbstractImmediateCollectionInitializer.ImmediateCollectionInitializerData>
extends AbstractCollectionInitializer<Data> {
extends AbstractCollectionInitializer<Data> implements BiConsumer<Data, List<Object>> {
/**
* refers to the rows entry in the collection. null indicates that the collection is empty
*/
private final @Nullable DomainResultAssembler<?> collectionValueKeyResultAssembler;
protected final @Nullable DomainResultAssembler<?> collectionValueKeyResultAssembler;
public static class ImmediateCollectionInitializerData extends CollectionInitializerData {
@ -137,6 +135,14 @@ public abstract class AbstractImmediateCollectionInitializer<Data extends Abstra
}
}
@Override
public void resolveFromPreviousRow(Data data) {
super.resolveFromPreviousRow( data );
if ( data.getState() == State.RESOLVED ) {
resolveKeySubInitializers( data );
}
}
/**
* Returns whether the collection value key is missing.
*/
@ -185,7 +191,12 @@ public abstract class AbstractImmediateCollectionInitializer<Data extends Abstra
return;
}
resolveCollectionKey( data, true );
// Being a result initializer means that this collection initializer is for lazy loading,
// which has a very high chance that a collection resolved of the previous row is the same for the current row,
// so pass that flag as indicator whether to check previous row state.
// Note that we don't need to check previous rows in other cases,
// because the previous row checks are done by the owner of the collection initializer already.
resolveCollectionKey( data, isResultInitializer );
if ( data.getState() != State.KEY_RESOLVED ) {
return;
}
@ -429,20 +440,17 @@ public abstract class AbstractImmediateCollectionInitializer<Data extends Abstra
}
data.setState( State.INITIALIZED );
final RowProcessingState initializerRowProcessingState = data.getRowProcessingState();
if ( data.collectionValueKey == null && collectionValueKeyResultAssembler != null ) {
final Initializer<?> initializer = collectionValueKeyResultAssembler.getInitializer();
if ( initializer != null ) {
data.collectionValueKey = collectionValueKeyResultAssembler.assemble( initializerRowProcessingState );
data.collectionValueKey = collectionValueKeyResultAssembler.assemble( data.getRowProcessingState() );
}
}
// the RHS key value of the association - determines if the row contains an element of the initializing collection
if ( collectionValueKeyResultAssembler == null || data.collectionValueKey != null ) {
// the row contains an element in the collection...
data.responsibility.load(
loadingState -> readCollectionRow( data.collectionKey, loadingState, initializerRowProcessingState )
);
data.responsibility.load( data, this );
}
}
@ -453,10 +461,12 @@ public abstract class AbstractImmediateCollectionInitializer<Data extends Abstra
initializeSubInstancesFromParent( data );
}
protected abstract void readCollectionRow(
CollectionKey collectionKey,
List<Object> loadingState,
RowProcessingState rowProcessingState);
@Override
public void accept(Data data, List<Object> objects) {
readCollectionRow( data, objects );
}
protected abstract void readCollectionRow(Data data, List<Object> loadingState);
protected abstract void initializeSubInstancesFromParent(Data data);

View File

@ -13,7 +13,6 @@ import java.util.function.BiConsumer;
import org.hibernate.HibernateException;
import org.hibernate.LockMode;
import org.hibernate.collection.spi.PersistentArrayHolder;
import org.hibernate.engine.spi.CollectionKey;
import org.hibernate.internal.log.LoggingHelper;
import org.hibernate.metamodel.mapping.PluralAttributeMapping;
import org.hibernate.spi.NavigablePath;
@ -86,10 +85,8 @@ public class ArrayInitializer extends AbstractImmediateCollectionInitializer<Abs
}
@Override
protected void readCollectionRow(
CollectionKey collectionKey,
List<Object> loadingState,
RowProcessingState rowProcessingState) {
protected void readCollectionRow(ImmediateCollectionInitializerData data, List<Object> loadingState) {
final RowProcessingState rowProcessingState = data.getRowProcessingState();
final Integer indexValue = listIndexAssembler.assemble( rowProcessingState );
if ( indexValue == null ) {
throw new HibernateException( "Illegal null value for array index encountered while reading: "

View File

@ -13,7 +13,6 @@ import org.hibernate.LockMode;
import org.hibernate.collection.spi.PersistentBag;
import org.hibernate.collection.spi.PersistentCollection;
import org.hibernate.collection.spi.PersistentIdentifierBag;
import org.hibernate.engine.spi.CollectionKey;
import org.hibernate.internal.log.LoggingHelper;
import org.hibernate.metamodel.mapping.PluralAttributeMapping;
import org.hibernate.spi.NavigablePath;
@ -82,10 +81,8 @@ public class BagInitializer extends AbstractImmediateCollectionInitializer<Abstr
}
@Override
protected void readCollectionRow(
CollectionKey collectionKey,
List<Object> loadingState,
RowProcessingState rowProcessingState) {
protected void readCollectionRow(ImmediateCollectionInitializerData data, List<Object> loadingState) {
final RowProcessingState rowProcessingState = data.getRowProcessingState();
if ( collectionIdAssembler != null ) {
final Object collectionId = collectionIdAssembler.assemble( rowProcessingState );
if ( collectionId == null ) {

View File

@ -209,6 +209,11 @@ public class EagerCollectionFetch extends CollectionFetch {
return true;
}
@Override
public int getCollectionFetchesCount() {
return 1 + super.getCollectionFetchesCount();
}
@Override
public JavaType<?> getResultJavaType() {
return getFetchedMapping().getJavaType();

View File

@ -12,7 +12,6 @@ import java.util.function.BiConsumer;
import org.hibernate.HibernateException;
import org.hibernate.LockMode;
import org.hibernate.collection.spi.PersistentList;
import org.hibernate.engine.spi.CollectionKey;
import org.hibernate.internal.log.LoggingHelper;
import org.hibernate.metamodel.mapping.PluralAttributeMapping;
import org.hibernate.spi.NavigablePath;
@ -87,10 +86,8 @@ public class ListInitializer extends AbstractImmediateCollectionInitializer<Abst
}
@Override
protected void readCollectionRow(
CollectionKey collectionKey,
List<Object> loadingState,
RowProcessingState rowProcessingState) {
protected void readCollectionRow(ImmediateCollectionInitializerData data, List<Object> loadingState) {
final RowProcessingState rowProcessingState = data.getRowProcessingState();
final Integer indexValue = listIndexAssembler.assemble( rowProcessingState );
if ( indexValue == null ) {
throw new HibernateException( "Illegal null value for list index encountered while reading: "

View File

@ -12,7 +12,6 @@ import java.util.function.BiConsumer;
import org.hibernate.LockMode;
import org.hibernate.collection.spi.PersistentMap;
import org.hibernate.engine.spi.CollectionKey;
import org.hibernate.internal.log.LoggingHelper;
import org.hibernate.metamodel.mapping.PluralAttributeMapping;
import org.hibernate.spi.NavigablePath;
@ -90,10 +89,8 @@ public class MapInitializer extends AbstractImmediateCollectionInitializer<Abstr
}
@Override
protected void readCollectionRow(
CollectionKey collectionKey,
List<Object> loadingState,
RowProcessingState rowProcessingState) {
protected void readCollectionRow(ImmediateCollectionInitializerData data, List<Object> loadingState) {
final RowProcessingState rowProcessingState = data.getRowProcessingState();
final Object key = mapKeyAssembler.assemble( rowProcessingState );
if ( key == null ) {
// If element is null, then NotFoundAction must be IGNORE

View File

@ -11,7 +11,6 @@ import java.util.function.BiConsumer;
import org.hibernate.LockMode;
import org.hibernate.collection.spi.PersistentSet;
import org.hibernate.engine.spi.CollectionKey;
import org.hibernate.internal.log.LoggingHelper;
import org.hibernate.metamodel.mapping.PluralAttributeMapping;
import org.hibernate.spi.NavigablePath;
@ -77,10 +76,8 @@ public class SetInitializer extends AbstractImmediateCollectionInitializer<Abstr
}
@Override
protected void readCollectionRow(
CollectionKey collectionKey,
List<Object> loadingState,
RowProcessingState rowProcessingState) {
protected void readCollectionRow(ImmediateCollectionInitializerData data, List<Object> loadingState) {
final RowProcessingState rowProcessingState = data.getRowProcessingState();
final Object element = elementAssembler.assemble( rowProcessingState );
if ( element == null ) {
// If element is null, then NotFoundAction must be IGNORE

View File

@ -261,6 +261,22 @@ public class EmbeddableInitializerImpl extends AbstractInitializer<EmbeddableIni
}
}
@Override
public void resolveFromPreviousRow(EmbeddableInitializerData data) {
if ( data.getState() == State.UNINITIALIZED ) {
if ( data.getInstance() == null ) {
data.setState( State.MISSING );
}
else {
final RowProcessingState rowProcessingState = data.getRowProcessingState();
for ( Initializer<InitializerData> initializer : subInitializers[data.getSubclassId()] ) {
initializer.resolveFromPreviousRow( rowProcessingState );
}
data.setState( State.INITIALIZED );
}
}
}
@Override
public void resolveInstance(EmbeddableInitializerData data) {
if ( data.getState() != State.KEY_RESOLVED ) {

View File

@ -126,6 +126,23 @@ public class DiscriminatedEntityInitializer
}
}
@Override
public void resolveFromPreviousRow(DiscriminatedEntityInitializerData data) {
if ( data.getState() == State.UNINITIALIZED ) {
if ( data.entityIdentifier == null ) {
data.setState( State.MISSING );
data.setInstance( null );
}
else {
final Initializer<?> initializer = keyValueAssembler.getInitializer();
if ( initializer != null ) {
initializer.resolveFromPreviousRow( data.getRowProcessingState() );
}
data.setState( State.INITIALIZED );
}
}
}
@Override
public void resolveInstance(DiscriminatedEntityInitializerData data) {
if ( data.getState() != State.KEY_RESOLVED ) {

View File

@ -102,6 +102,23 @@ public class EntityDelayedFetchInitializer
return referencedModelPart;
}
@Override
public void resolveFromPreviousRow(EntityDelayedFetchInitializerData data) {
if ( data.getState() == State.UNINITIALIZED ) {
if ( data.entityIdentifier == null ) {
data.setState( State.MISSING );
data.setInstance( null );
}
else {
final Initializer<?> initializer = identifierAssembler.getInitializer();
if ( initializer != null ) {
initializer.resolveFromPreviousRow( data.getRowProcessingState() );
}
data.setState( State.RESOLVED );
}
}
}
@Override
public void resolveInstance(EntityDelayedFetchInitializerData data) {
if ( data.getState() != State.KEY_RESOLVED ) {

View File

@ -63,6 +63,7 @@ import org.hibernate.sql.results.graph.Initializer;
import org.hibernate.sql.results.graph.InitializerData;
import org.hibernate.sql.results.graph.InitializerParent;
import org.hibernate.sql.results.graph.basic.BasicResultAssembler;
import org.hibernate.sql.results.graph.collection.internal.AbstractImmediateCollectionInitializer;
import org.hibernate.sql.results.graph.entity.EntityInitializer;
import org.hibernate.sql.results.graph.entity.EntityResultGraphNode;
import org.hibernate.sql.results.graph.internal.AbstractInitializer;
@ -106,6 +107,14 @@ public class EntityInitializerImpl extends AbstractInitializer<EntityInitializer
private final boolean isPartOfKey;
private final boolean isResultInitializer;
private final boolean hasKeyManyToOne;
/**
* Indicates whether there is a high chance of the previous row to have the same entity key as the current row
* and hence enable a check in the {@link #resolveKey(RowProcessingState)} phase which compare the previously read
* identifier with the current row identifier. If it matches, the state from the previous row processing can be reused.
* In addition to that, all direct sub-initializers can be informed about the reuse by calling {@link Initializer#resolveFromPreviousRow(RowProcessingState)},
* so that these initializers can avoid unnecessary processing as well.
*/
private final boolean previousRowReuse;
private final @Nullable DomainResultAssembler<?> keyAssembler;
private final @Nullable DomainResultAssembler<?> identifierAssembler;
@ -163,6 +172,15 @@ public class EntityInitializerImpl extends AbstractInitializer<EntityInitializer
this.parent = parent;
this.isResultInitializer = isResultInitializer;
this.isPartOfKey = Initializer.isPartOfKey( navigablePath, parent );
// If the parent already has previous row reuse enabled, we can skip that here
this.previousRowReuse = !isPreviousRowReuse( parent ) && (
// If this entity domain result contains a collection join fetch, this usually means that the entity data is
// duplicate in the result data for every collection element. Since collections usually have more than one element,
// optimizing the resolving of the entity data is very beneficial.
resultDescriptor.containsCollectionFetches()
// Result duplication generally also happens if more than one collection is join fetched,
|| creationState.containsMultipleCollectionFetches()
);
assert identifierFetch != null || isResultInitializer : "Identifier must be fetched, unless this is a result initializer";
if ( identifierFetch == null ) {
@ -256,6 +274,21 @@ public class EntityInitializerImpl extends AbstractInitializer<EntityInitializer
this.affectedByFilter = affectedByFilter;
}
private static boolean isPreviousRowReuse(@Nullable InitializerParent<?> parent) {
// Traverse up the parents to find out if one of our parents has row reuse enabled
while ( parent != null ) {
if ( parent instanceof EntityInitializerImpl ) {
return ( (EntityInitializerImpl) parent ).isPreviousRowReuse();
}
// Immediate collections don't reuse previous rows for elements, so we can safely assume false
if ( parent instanceof AbstractImmediateCollectionInitializer<?> ) {
return false;
}
parent = parent.getParent();
}
return false;
}
@Override
protected EntityInitializerData createInitializerData(RowProcessingState rowProcessingState) {
return new EntityInitializerData( rowProcessingState );
@ -299,6 +332,10 @@ public class EntityInitializerImpl extends AbstractInitializer<EntityInitializer
}
data.setState( State.KEY_RESOLVED );
final EntityKey oldEntityKey = data.entityKey;
final Object oldEntityInstance = data.getInstance();
final Object oldEntityInstanceForNotify = data.entityInstanceForNotify;
final EntityHolder oldEntityHolder = data.entityHolder;
// reset row state
data.concreteDescriptor = null;
data.entityKey = null;
@ -344,6 +381,20 @@ public class EntityInitializerImpl extends AbstractInitializer<EntityInitializer
}
}
if ( oldEntityKey != null && previousRowReuse && oldEntityInstance != null
&& entityDescriptor.getIdentifierType().isEqual( oldEntityKey.getIdentifier(), id ) ) {
// The row we read previously referred to this entity already, so we can safely assume it's initialized.
// Unfortunately we can't set the state to INITIALIZED though, as that has other implications,
// but RESOLVED is fine, since the EntityEntry is marked as initialized which skips instance initialization
data.setState( State.RESOLVED );
data.entityKey = oldEntityKey;
data.setInstance( oldEntityInstance );
data.entityInstanceForNotify = oldEntityInstanceForNotify;
data.concreteDescriptor = oldEntityKey.getPersister();
data.entityHolder = oldEntityHolder;
notifySubInitializersToReusePreviousRowInstance( data );
return;
}
resolveEntityKey( data, id );
if ( !entityKeyOnly ) {
// Resolve the entity instance early as we have no key many-to-one
@ -413,6 +464,15 @@ public class EntityInitializerImpl extends AbstractInitializer<EntityInitializer
}
}
private void notifySubInitializersToReusePreviousRowInstance(EntityInitializerData data) {
final RowProcessingState rowProcessingState = data.getRowProcessingState();
for ( Initializer<?> initializer : subInitializers[data.concreteDescriptor.getSubclassId()] ) {
if ( initializer != null ) {
initializer.resolveFromPreviousRow( rowProcessingState );
}
}
}
private void resolveKeySubInitializers(EntityInitializerData data) {
final RowProcessingState rowProcessingState = data.getRowProcessingState();
for ( Initializer<?> initializer : subInitializers[data.concreteDescriptor.getSubclassId()] ) {
@ -467,6 +527,20 @@ public class EntityInitializerImpl extends AbstractInitializer<EntityInitializer
}
}
@Override
public void resolveFromPreviousRow(EntityInitializerData data) {
if ( data.getState() == State.UNINITIALIZED ) {
final EntityKey entityKey = data.entityKey;
if ( entityKey == null ) {
setMissing( data );
}
else {
data.setState( State.RESOLVED );
notifySubInitializersToReusePreviousRowInstance( data );
}
}
}
@Override
public void initializeInstanceFromParent(Object parentInstance, EntityInitializerData data) {
final AttributeMapping attributeMapping = getInitializedPart().asAttributeMapping();
@ -1384,6 +1458,10 @@ public class EntityInitializerImpl extends AbstractInitializer<EntityInitializer
return isPartOfKey;
}
public boolean isPreviousRowReuse() {
return previousRowReuse;
}
@Override
public EntityPersister getConcreteDescriptor(EntityInitializerData data) {
assert data.getState() != State.UNINITIALIZED;

View File

@ -104,6 +104,23 @@ public class EntitySelectFetchInitializer<Data extends EntitySelectFetchInitiali
return navigablePath;
}
@Override
public void resolveFromPreviousRow(Data data) {
if ( data.getState() == State.UNINITIALIZED ) {
if ( data.entityIdentifier == null ) {
data.setState( State.MISSING );
data.setInstance( null );
}
else {
final Initializer<?> initializer = keyAssembler.getInitializer();
if ( initializer != null ) {
initializer.resolveFromPreviousRow( data.getRowProcessingState() );
}
data.setState( State.INITIALIZED );
}
}
}
@Override
public void resolveInstance(Data data) {
if ( data.getState() != State.KEY_RESOLVED ) {

View File

@ -8,6 +8,7 @@ package org.hibernate.sql.results.internal;
import java.util.ArrayList;
import java.util.List;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
import org.hibernate.collection.spi.PersistentCollection;
@ -68,6 +69,11 @@ public class LoadingCollectionEntryImpl implements LoadingCollectionEntry {
loadingEntryConsumer.accept( loadingState );
}
@Override
public <T> void load(T arg1, BiConsumer<T, List<Object>> loadingEntryConsumer) {
loadingEntryConsumer.accept( arg1, loadingState );
}
@Override public void finishLoading(ExecutionContext executionContext) {
collectionInstance.injectLoadedState(
getCollectionDescriptor().getAttributeMapping(),

View File

@ -109,6 +109,7 @@ public class StandardJdbcValuesMapping implements JdbcValuesMapping {
private int initializerId;
boolean hasCollectionInitializers;
Boolean dynamicInstantiation;
Boolean containsMultipleCollectionFetches;
public AssemblerCreationStateImpl(
JdbcValuesMapping jdbcValuesMapping,
@ -127,6 +128,20 @@ public class StandardJdbcValuesMapping implements JdbcValuesMapping {
return dynamicInstantiation;
}
@Override
public boolean containsMultipleCollectionFetches() {
if ( containsMultipleCollectionFetches == null ) {
int collectionFetchesCount = 0;
for ( DomainResult<?> domainResult : jdbcValuesMapping.getDomainResults() ) {
if ( domainResult instanceof FetchParent ) {
collectionFetchesCount += ( (FetchParent) domainResult ).getCollectionFetchesCount();
}
}
containsMultipleCollectionFetches = collectionFetchesCount > 1;
}
return containsMultipleCollectionFetches;
}
@Override
public int acquireInitializerId() {
return initializerId++;

View File

@ -0,0 +1,328 @@
/*
* 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.mapping.collections;
import java.util.AbstractMap;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.UUID;
import org.hibernate.cfg.AvailableSettings;
import org.hibernate.jpa.AvailableHints;
import org.hibernate.testing.orm.junit.EntityManagerFactoryScope;
import org.hibernate.testing.orm.junit.Jira;
import org.hibernate.testing.orm.junit.Jpa;
import org.hibernate.testing.orm.junit.Setting;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import jakarta.persistence.CascadeType;
import jakarta.persistence.ElementCollection;
import jakarta.persistence.Embeddable;
import jakarta.persistence.Embedded;
import jakarta.persistence.Entity;
import jakarta.persistence.EntityManager;
import jakarta.persistence.FetchType;
import jakarta.persistence.Id;
import jakarta.persistence.ManyToOne;
import static org.assertj.core.api.Assertions.assertThat;
@Jpa(
annotatedClasses = {
ElementCollectionCachePerfTest.Element.class,
ElementCollectionCachePerfTest.KeyValue.class,
ElementCollectionCachePerfTest.Association.class,
},
properties = {
@Setting(name = AvailableSettings.USE_QUERY_CACHE, value = "true"),
@Setting(name = AvailableSettings.USE_SECOND_LEVEL_CACHE, value = "true"),
@Setting(name = AvailableSettings.QUERY_CACHE_LAYOUT, value = "auto")
}
)
@Jira("https://hibernate.atlassian.net/browse/HHH-18375")
public class ElementCollectionCachePerfTest {
@BeforeAll
public void setUp(EntityManagerFactoryScope scope) {
scope.inTransaction( entityManager -> {
for ( int i = 0; i < 100; i++ ) {
final String id = UUID.randomUUID().toString();
final Element element = new Element( id );
element.setKeyValueEmbeddable( new KeyValue( "embeddable", "_" + id ) );
element.setAssociation1( new Association( (long) i, "assoc_" + id ) );
final Set<KeyValue> key1Values = new HashSet<>();
key1Values.add( new KeyValue( "key1_1", "_" + id ) );
key1Values.add( new KeyValue( "key1_2", "_" + id ) );
key1Values.add( new KeyValue( "key1_3", "_" + id ) );
element.setKeyValues1( key1Values );
final Set<KeyValue> key2Values = new HashSet<>();
key2Values.add( new KeyValue( "key2_1", "_" + id ) );
key2Values.add( new KeyValue( "key2_2", "_" + id ) );
element.setKeyValues2( key2Values );
final Map<String, KeyValue> map = new HashMap<>();
map.put( "k1", new KeyValue( "k1", "_" + id ) );
map.put( "k2", new KeyValue( "k2", "_" + id ) );
element.setMap( map );
entityManager.persist( element );
}
} );
}
@Test
public void testSelect1(EntityManagerFactoryScope scope) {
scope.inTransaction(
entityManager -> {
assertQuery(
entityManager,
"select e from Element e join fetch e.association1 join fetch e.keyValues1 join fetch e.keyValues2 join fetch e.map"
);
} );
}
@Test
public void testSelect2(EntityManagerFactoryScope scope) {
scope.inTransaction(
entityManager -> {
assertQuery(
entityManager,
"select e from Element e join fetch e.association1 join fetch e.map join fetch e.keyValues1 join fetch e.keyValues2"
);
} );
}
private static void assertQuery(EntityManager entityManager, String query) {
final List<Element> firstResult = entityManager.createQuery( query, Element.class )
.setHint( AvailableHints.HINT_CACHEABLE, true )
.getResultList();
assertResults( firstResult );
final List<Element> secondResult = entityManager.createQuery( query, Element.class )
.setHint( AvailableHints.HINT_CACHEABLE, true )
.getResultList();
assertResults( secondResult );
}
private static void assertResults(List<Element> result) {
for ( Element element : result ) {
final String id = element.getId();
assertThat( element.getAssociation1().getName() ).isEqualTo( "assoc_" + id );
assertThat( element.getKeyValueEmbeddable().getK() ).isEqualTo( "embeddable" );
assertThat( element.getKeyValueEmbeddable().getV() ).isEqualTo( "_" + id );
assertThat( element.getKeyValues1().size() ).isEqualTo( 3 );
assertThat( element.getKeyValues2().size() ).isEqualTo( 2 );
assertThat( element.getMap().size() ).isEqualTo( 2 );
assertThat( element.getKeyValues1() ).containsExactlyInAnyOrder(
new KeyValue( "key1_1", "_" + id ),
new KeyValue( "key1_2", "_" + id ),
new KeyValue( "key1_3", "_" + id )
);
assertThat( element.getKeyValues2() ).containsExactlyInAnyOrder(
new KeyValue( "key2_1", "_" + id ),
new KeyValue( "key2_2", "_" + id )
);
assertThat( element.getMap() ).containsExactly(
new AbstractMap.SimpleEntry<>( "k1", new KeyValue( "k1", "_" + id ) ),
new AbstractMap.SimpleEntry<>( "k2", new KeyValue( "k2", "_" + id ) )
);
}
}
@Entity(name = "Element")
public static class Element {
@Id
private String id;
@ManyToOne(fetch = FetchType.LAZY, cascade = CascadeType.ALL)
private Association association1;
@Embedded
KeyValue keyValueEmbeddable;
@ElementCollection
private Set<KeyValue> keyValues1;
@ElementCollection
private Set<KeyValue> keyValues2;
@ElementCollection
private Map<String, KeyValue> map;
protected Element() {
}
public Element(String id) {
this.id = id;
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public Association getAssociation1() {
return association1;
}
public void setAssociation1(Association association1) {
this.association1 = association1;
}
public KeyValue getKeyValueEmbeddable() {
return keyValueEmbeddable;
}
public void setKeyValueEmbeddable(KeyValue keyValueEmbeddable) {
this.keyValueEmbeddable = keyValueEmbeddable;
}
public Set<KeyValue> getKeyValues1() {
return keyValues1;
}
public void setKeyValues1(Set<KeyValue> keyValues) {
this.keyValues1 = keyValues;
}
public Set<KeyValue> getKeyValues2() {
return keyValues2;
}
public void setKeyValues2(Set<KeyValue> keyValues2) {
this.keyValues2 = keyValues2;
}
public Map<String, KeyValue> getMap() {
return map;
}
public void setMap(Map<String, KeyValue> map) {
this.map = map;
}
@Override
public boolean equals(Object o) {
if ( this == o ) {
return true;
}
if ( o == null || getClass() != o.getClass() ) {
return false;
}
Element element = (Element) o;
return Objects.equals( id, element.id );
}
@Override
public int hashCode() {
return Objects.hash( id );
}
@Override
public String toString() {
return "Element{" +
"id=" + id +
", keyValues=" + keyValues1 +
'}';
}
}
@Embeddable
public static class KeyValue {
private String k;
private String v;
public KeyValue() {
}
public KeyValue(String k, String v) {
this.k = k;
this.v = v;
}
public String getK() {
return k;
}
public void setK(String key) {
this.k = key;
}
public String getV() {
return v;
}
public void setV(String value) {
this.v = value;
}
@Override
public boolean equals(Object o) {
if ( this == o ) {
return true;
}
if ( o == null || getClass() != o.getClass() ) {
return false;
}
KeyValue keyValue = (KeyValue) o;
return Objects.equals( k, keyValue.k ) && Objects.equals( v, keyValue.v );
}
@Override
public int hashCode() {
return Objects.hash( k, v );
}
@Override
public String toString() {
return "KeyValue{" +
"key='" + k + '\'' +
", value='" + v + '\'' +
'}';
}
}
@Entity(name = "Association")
public static class Association {
@Id
private Long id;
private String name;
public Association() {
}
public Association(Long id, String name) {
this.id = id;
this.name = name;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
}

View File

@ -0,0 +1,341 @@
/*
* 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.mapping.collections;
import java.util.AbstractMap;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.UUID;
import org.hibernate.testing.orm.junit.Jira;
import org.hibernate.testing.orm.junit.Jpa;
import org.hibernate.testing.orm.junit.EntityManagerFactoryScope;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import jakarta.persistence.CascadeType;
import jakarta.persistence.ElementCollection;
import jakarta.persistence.Embeddable;
import jakarta.persistence.Embedded;
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.Id;
import jakarta.persistence.ManyToOne;
import static org.assertj.core.api.Assertions.assertThat;
@Jpa(annotatedClasses = {
ElementCollectionPerfTest.Element.class,
ElementCollectionPerfTest.KeyValue.class,
ElementCollectionPerfTest.Association.class,
})
@Jira("https://hibernate.atlassian.net/browse/HHH-18375")
public class ElementCollectionPerfTest {
@BeforeAll
public void setUp(EntityManagerFactoryScope scope) {
scope.inTransaction( entityManager -> {
for ( int i = 0; i < 100; i++ ) {
final String id = UUID.randomUUID().toString();
final Element element = new Element( id );
element.setKeyValueEmbeddable( new KeyValue( "embeddable", "_" + id ) );
element.setAssociation1( new Association( (long) i, "assoc_" + id ) );
final Set<KeyValue> key1Values = new HashSet<>();
key1Values.add( new KeyValue( "key1_1", "_" + id ) );
key1Values.add( new KeyValue( "key1_2", "_" + id ) );
key1Values.add( new KeyValue( "key1_3", "_" + id ) );
element.setKeyValues1( key1Values );
element.association1.keyValues1 = new HashSet<>( key1Values);
final Set<KeyValue> key2Values = new HashSet<>();
key2Values.add( new KeyValue( "key2_1", "_" + id ) );
key2Values.add( new KeyValue( "key2_2", "_" + id ) );
element.setKeyValues2( key2Values );
final Map<String, KeyValue> map = new HashMap<>();
map.put( "k1", new KeyValue( "k1", "_" + id ) );
map.put( "k2", new KeyValue( "k2", "_" + id ) );
element.setMap( map );
entityManager.persist( element );
}
} );
}
@Test
public void testSelect0(EntityManagerFactoryScope scope) {
scope.inTransaction(
entityManager -> {
List<Element> result = entityManager.createQuery(
"select e from Element e join fetch e.association1 a join fetch a.keyValues1 join fetch e.keyValues1 join fetch e.keyValues2 join fetch e.map",
Element.class
).getResultList();
assertResults( result );
} );
}
@Test
public void testSelect1(EntityManagerFactoryScope scope) {
scope.inTransaction(
entityManager -> {
List<Element> result = entityManager.createQuery(
"select e from Element e join fetch e.association1 join fetch e.keyValues1 join fetch e.keyValues2 join fetch e.map",
Element.class
).getResultList();
assertResults( result );
} );
}
@Test
public void testSelect2(EntityManagerFactoryScope scope) {
scope.inTransaction(
entityManager -> {
List<Element> result = entityManager.createQuery(
"select e from Element e join fetch e.association1 join fetch e.map join fetch e.keyValues1 join fetch e.keyValues2",
Element.class
).getResultList();
assertResults( result );
} );
}
private static void assertResults(List<Element> result) {
for ( Element element : result ) {
final String id = element.getId();
assertThat( element.getAssociation1().getName() ).isEqualTo( "assoc_" + id );
assertThat( element.getAssociation1().getKeyValues1().size() ).isEqualTo( 3 );
assertThat( element.getKeyValueEmbeddable().getK() ).isEqualTo( "embeddable" );
assertThat( element.getKeyValueEmbeddable().getV() ).isEqualTo( "_" + id );
assertThat( element.getKeyValues1().size() ).isEqualTo( 3 );
assertThat( element.getKeyValues2().size() ).isEqualTo( 2 );
assertThat( element.getMap().size() ).isEqualTo( 2 );
assertThat( element.getAssociation1().getKeyValues1() ).containsExactlyInAnyOrder(
new KeyValue( "key1_1", "_" + id ),
new KeyValue( "key1_2", "_" + id ),
new KeyValue( "key1_3", "_" + id )
);
assertThat( element.getKeyValues1() ).containsExactlyInAnyOrder(
new KeyValue( "key1_1", "_" + id ),
new KeyValue( "key1_2", "_" + id ),
new KeyValue( "key1_3", "_" + id )
);
assertThat( element.getKeyValues2() ).containsExactlyInAnyOrder(
new KeyValue( "key2_1", "_" + id ),
new KeyValue( "key2_2", "_" + id )
);
assertThat( element.getMap() ).containsExactly(
new AbstractMap.SimpleEntry<>( "k1", new KeyValue( "k1", "_" + id ) ),
new AbstractMap.SimpleEntry<>( "k2", new KeyValue( "k2", "_" + id ) )
);
}
}
@Entity(name = "Element")
public static class Element {
@Id
private String id;
@ManyToOne(fetch = FetchType.LAZY, cascade = CascadeType.ALL)
private Association association1;
@Embedded
KeyValue keyValueEmbeddable;
@ElementCollection
private Set<KeyValue> keyValues1;
@ElementCollection
private Set<KeyValue> keyValues2;
@ElementCollection
private Map<String, KeyValue> map;
protected Element() {
}
public Element(String id) {
this.id = id;
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public Association getAssociation1() {
return association1;
}
public void setAssociation1(Association association1) {
this.association1 = association1;
}
public KeyValue getKeyValueEmbeddable() {
return keyValueEmbeddable;
}
public void setKeyValueEmbeddable(KeyValue keyValueEmbeddable) {
this.keyValueEmbeddable = keyValueEmbeddable;
}
public Set<KeyValue> getKeyValues1() {
return keyValues1;
}
public void setKeyValues1(Set<KeyValue> keyValues) {
this.keyValues1 = keyValues;
}
public Set<KeyValue> getKeyValues2() {
return keyValues2;
}
public void setKeyValues2(Set<KeyValue> keyValues2) {
this.keyValues2 = keyValues2;
}
public Map<String, KeyValue> getMap() {
return map;
}
public void setMap(Map<String, KeyValue> map) {
this.map = map;
}
@Override
public boolean equals(Object o) {
if ( this == o ) {
return true;
}
if ( o == null || getClass() != o.getClass() ) {
return false;
}
Element element = (Element) o;
return Objects.equals( id, element.id );
}
@Override
public int hashCode() {
return Objects.hash( id );
}
@Override
public String toString() {
return "Element{" +
"id=" + id +
", keyValues=" + keyValues1 +
'}';
}
}
@Embeddable
public static class KeyValue {
private String k;
private String v;
public KeyValue() {
}
public KeyValue(String k, String v) {
this.k = k;
this.v = v;
}
public String getK() {
return k;
}
public void setK(String key) {
this.k = key;
}
public String getV() {
return v;
}
public void setV(String value) {
this.v = value;
}
@Override
public boolean equals(Object o) {
if ( this == o ) {
return true;
}
if ( o == null || getClass() != o.getClass() ) {
return false;
}
KeyValue keyValue = (KeyValue) o;
return Objects.equals( k, keyValue.k ) && Objects.equals( v, keyValue.v );
}
@Override
public int hashCode() {
return Objects.hash( k, v );
}
@Override
public String toString() {
return "KeyValue{" +
"key='" + k + '\'' +
", value='" + v + '\'' +
'}';
}
}
@Entity(name = "Association")
public static class Association {
@Id
private Long id;
private String name;
@ElementCollection
private Set<KeyValue> keyValues1;
public Association() {
}
public Association(Long id, String name) {
this.id = id;
this.name = name;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Set<KeyValue> getKeyValues1() {
return keyValues1;
}
public void setKeyValues1(Set<KeyValue> keyValues1) {
this.keyValues1 = keyValues1;
}
}
}