HHH-18375 Reuse previous row state when result cardinality is duplicated
This commit is contained in:
parent
228bd7958f
commit
505092e4ea
|
@ -21,6 +21,10 @@ public interface AssemblerCreationState {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
default boolean containsMultipleCollectionFetches() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
int acquireInitializerId();
|
int acquireInitializerId();
|
||||||
|
|
||||||
Initializer<?> resolveInitializer(
|
Initializer<?> resolveInitializer(
|
||||||
|
|
|
@ -79,4 +79,20 @@ public interface FetchList extends Iterable<Fetch> {
|
||||||
return false;
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -95,6 +95,10 @@ public interface FetchParent extends DomainResultGraphNode {
|
||||||
|
|
||||||
boolean containsCollectionFetches();
|
boolean containsCollectionFetches();
|
||||||
|
|
||||||
|
default int getCollectionFetchesCount() {
|
||||||
|
return getFetches().getCollectionFetchesCount();
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
default void collectValueIndexesToCache(BitSet valueIndexes) {
|
default void collectValueIndexesToCache(BitSet valueIndexes) {
|
||||||
for ( Fetch fetch : getFetches() ) {
|
for ( Fetch fetch : getFetches() ) {
|
||||||
|
|
|
@ -86,7 +86,7 @@ public interface Initializer<Data extends InitializerData> {
|
||||||
// by default - nothing to do
|
// 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.
|
* row and then recurse to the sub-initializers.
|
||||||
*
|
*
|
||||||
* After this point, the initializer knows whether further processing is necessary
|
* After this point, the initializer knows whether further processing is necessary
|
||||||
|
@ -98,6 +98,20 @@ public interface Initializer<Data extends InitializerData> {
|
||||||
resolveKey( getData( rowProcessingState ) );
|
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
|
* Step 2.1 - Using the key resolved in {@link #resolveKey}, resolve the
|
||||||
* instance (of the thing initialized) to use for the current row.
|
* instance (of the thing initialized) to use for the current row.
|
||||||
|
|
|
@ -7,6 +7,7 @@
|
||||||
package org.hibernate.sql.results.graph.collection;
|
package org.hibernate.sql.results.graph.collection;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.function.BiConsumer;
|
||||||
import java.util.function.Consumer;
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
import org.hibernate.collection.spi.PersistentCollection;
|
import org.hibernate.collection.spi.PersistentCollection;
|
||||||
|
@ -44,6 +45,13 @@ public interface LoadingCollectionEntry {
|
||||||
*/
|
*/
|
||||||
void load(Consumer<List<Object>> loadingEntryConsumer);
|
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
|
* Complete the load
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -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) {
|
protected void setMissing(Data data) {
|
||||||
data.setState( State.MISSING );
|
data.setState( State.MISSING );
|
||||||
data.collectionKey = null;
|
data.collectionKey = null;
|
||||||
|
|
|
@ -12,7 +12,6 @@ import java.util.function.BiConsumer;
|
||||||
import org.hibernate.LockMode;
|
import org.hibernate.LockMode;
|
||||||
import org.hibernate.collection.spi.CollectionSemantics;
|
import org.hibernate.collection.spi.CollectionSemantics;
|
||||||
import org.hibernate.collection.spi.PersistentCollection;
|
import org.hibernate.collection.spi.PersistentCollection;
|
||||||
import org.hibernate.engine.spi.CollectionKey;
|
|
||||||
import org.hibernate.engine.spi.PersistenceContext;
|
import org.hibernate.engine.spi.PersistenceContext;
|
||||||
import org.hibernate.engine.spi.SharedSessionContractImplementor;
|
import org.hibernate.engine.spi.SharedSessionContractImplementor;
|
||||||
import org.hibernate.internal.log.LoggingHelper;
|
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.InitializerData;
|
||||||
import org.hibernate.sql.results.graph.InitializerParent;
|
import org.hibernate.sql.results.graph.InitializerParent;
|
||||||
import org.hibernate.sql.results.graph.collection.LoadingCollectionEntry;
|
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.internal.LoadingCollectionEntryImpl;
|
||||||
import org.hibernate.sql.results.jdbc.spi.RowProcessingState;
|
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.
|
* @implNote Mainly an intention contract wrt the immediacy of the fetch.
|
||||||
*/
|
*/
|
||||||
public abstract class AbstractImmediateCollectionInitializer<Data extends AbstractImmediateCollectionInitializer.ImmediateCollectionInitializerData>
|
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
|
* 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 {
|
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.
|
* Returns whether the collection value key is missing.
|
||||||
*/
|
*/
|
||||||
|
@ -185,7 +191,12 @@ public abstract class AbstractImmediateCollectionInitializer<Data extends Abstra
|
||||||
return;
|
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 ) {
|
if ( data.getState() != State.KEY_RESOLVED ) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -429,20 +440,17 @@ public abstract class AbstractImmediateCollectionInitializer<Data extends Abstra
|
||||||
}
|
}
|
||||||
data.setState( State.INITIALIZED );
|
data.setState( State.INITIALIZED );
|
||||||
|
|
||||||
final RowProcessingState initializerRowProcessingState = data.getRowProcessingState();
|
|
||||||
if ( data.collectionValueKey == null && collectionValueKeyResultAssembler != null ) {
|
if ( data.collectionValueKey == null && collectionValueKeyResultAssembler != null ) {
|
||||||
final Initializer<?> initializer = collectionValueKeyResultAssembler.getInitializer();
|
final Initializer<?> initializer = collectionValueKeyResultAssembler.getInitializer();
|
||||||
if ( initializer != null ) {
|
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
|
// the RHS key value of the association - determines if the row contains an element of the initializing collection
|
||||||
if ( collectionValueKeyResultAssembler == null || data.collectionValueKey != null ) {
|
if ( collectionValueKeyResultAssembler == null || data.collectionValueKey != null ) {
|
||||||
// the row contains an element in the collection...
|
// the row contains an element in the collection...
|
||||||
data.responsibility.load(
|
data.responsibility.load( data, this );
|
||||||
loadingState -> readCollectionRow( data.collectionKey, loadingState, initializerRowProcessingState )
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -453,10 +461,12 @@ public abstract class AbstractImmediateCollectionInitializer<Data extends Abstra
|
||||||
initializeSubInstancesFromParent( data );
|
initializeSubInstancesFromParent( data );
|
||||||
}
|
}
|
||||||
|
|
||||||
protected abstract void readCollectionRow(
|
@Override
|
||||||
CollectionKey collectionKey,
|
public void accept(Data data, List<Object> objects) {
|
||||||
List<Object> loadingState,
|
readCollectionRow( data, objects );
|
||||||
RowProcessingState rowProcessingState);
|
}
|
||||||
|
|
||||||
|
protected abstract void readCollectionRow(Data data, List<Object> loadingState);
|
||||||
|
|
||||||
protected abstract void initializeSubInstancesFromParent(Data data);
|
protected abstract void initializeSubInstancesFromParent(Data data);
|
||||||
|
|
||||||
|
|
|
@ -13,7 +13,6 @@ import java.util.function.BiConsumer;
|
||||||
import org.hibernate.HibernateException;
|
import org.hibernate.HibernateException;
|
||||||
import org.hibernate.LockMode;
|
import org.hibernate.LockMode;
|
||||||
import org.hibernate.collection.spi.PersistentArrayHolder;
|
import org.hibernate.collection.spi.PersistentArrayHolder;
|
||||||
import org.hibernate.engine.spi.CollectionKey;
|
|
||||||
import org.hibernate.internal.log.LoggingHelper;
|
import org.hibernate.internal.log.LoggingHelper;
|
||||||
import org.hibernate.metamodel.mapping.PluralAttributeMapping;
|
import org.hibernate.metamodel.mapping.PluralAttributeMapping;
|
||||||
import org.hibernate.spi.NavigablePath;
|
import org.hibernate.spi.NavigablePath;
|
||||||
|
@ -86,10 +85,8 @@ public class ArrayInitializer extends AbstractImmediateCollectionInitializer<Abs
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void readCollectionRow(
|
protected void readCollectionRow(ImmediateCollectionInitializerData data, List<Object> loadingState) {
|
||||||
CollectionKey collectionKey,
|
final RowProcessingState rowProcessingState = data.getRowProcessingState();
|
||||||
List<Object> loadingState,
|
|
||||||
RowProcessingState rowProcessingState) {
|
|
||||||
final Integer indexValue = listIndexAssembler.assemble( rowProcessingState );
|
final Integer indexValue = listIndexAssembler.assemble( rowProcessingState );
|
||||||
if ( indexValue == null ) {
|
if ( indexValue == null ) {
|
||||||
throw new HibernateException( "Illegal null value for array index encountered while reading: "
|
throw new HibernateException( "Illegal null value for array index encountered while reading: "
|
||||||
|
|
|
@ -13,7 +13,6 @@ import org.hibernate.LockMode;
|
||||||
import org.hibernate.collection.spi.PersistentBag;
|
import org.hibernate.collection.spi.PersistentBag;
|
||||||
import org.hibernate.collection.spi.PersistentCollection;
|
import org.hibernate.collection.spi.PersistentCollection;
|
||||||
import org.hibernate.collection.spi.PersistentIdentifierBag;
|
import org.hibernate.collection.spi.PersistentIdentifierBag;
|
||||||
import org.hibernate.engine.spi.CollectionKey;
|
|
||||||
import org.hibernate.internal.log.LoggingHelper;
|
import org.hibernate.internal.log.LoggingHelper;
|
||||||
import org.hibernate.metamodel.mapping.PluralAttributeMapping;
|
import org.hibernate.metamodel.mapping.PluralAttributeMapping;
|
||||||
import org.hibernate.spi.NavigablePath;
|
import org.hibernate.spi.NavigablePath;
|
||||||
|
@ -82,10 +81,8 @@ public class BagInitializer extends AbstractImmediateCollectionInitializer<Abstr
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void readCollectionRow(
|
protected void readCollectionRow(ImmediateCollectionInitializerData data, List<Object> loadingState) {
|
||||||
CollectionKey collectionKey,
|
final RowProcessingState rowProcessingState = data.getRowProcessingState();
|
||||||
List<Object> loadingState,
|
|
||||||
RowProcessingState rowProcessingState) {
|
|
||||||
if ( collectionIdAssembler != null ) {
|
if ( collectionIdAssembler != null ) {
|
||||||
final Object collectionId = collectionIdAssembler.assemble( rowProcessingState );
|
final Object collectionId = collectionIdAssembler.assemble( rowProcessingState );
|
||||||
if ( collectionId == null ) {
|
if ( collectionId == null ) {
|
||||||
|
|
|
@ -209,6 +209,11 @@ public class EagerCollectionFetch extends CollectionFetch {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getCollectionFetchesCount() {
|
||||||
|
return 1 + super.getCollectionFetchesCount();
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public JavaType<?> getResultJavaType() {
|
public JavaType<?> getResultJavaType() {
|
||||||
return getFetchedMapping().getJavaType();
|
return getFetchedMapping().getJavaType();
|
||||||
|
|
|
@ -12,7 +12,6 @@ import java.util.function.BiConsumer;
|
||||||
import org.hibernate.HibernateException;
|
import org.hibernate.HibernateException;
|
||||||
import org.hibernate.LockMode;
|
import org.hibernate.LockMode;
|
||||||
import org.hibernate.collection.spi.PersistentList;
|
import org.hibernate.collection.spi.PersistentList;
|
||||||
import org.hibernate.engine.spi.CollectionKey;
|
|
||||||
import org.hibernate.internal.log.LoggingHelper;
|
import org.hibernate.internal.log.LoggingHelper;
|
||||||
import org.hibernate.metamodel.mapping.PluralAttributeMapping;
|
import org.hibernate.metamodel.mapping.PluralAttributeMapping;
|
||||||
import org.hibernate.spi.NavigablePath;
|
import org.hibernate.spi.NavigablePath;
|
||||||
|
@ -87,10 +86,8 @@ public class ListInitializer extends AbstractImmediateCollectionInitializer<Abst
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void readCollectionRow(
|
protected void readCollectionRow(ImmediateCollectionInitializerData data, List<Object> loadingState) {
|
||||||
CollectionKey collectionKey,
|
final RowProcessingState rowProcessingState = data.getRowProcessingState();
|
||||||
List<Object> loadingState,
|
|
||||||
RowProcessingState rowProcessingState) {
|
|
||||||
final Integer indexValue = listIndexAssembler.assemble( rowProcessingState );
|
final Integer indexValue = listIndexAssembler.assemble( rowProcessingState );
|
||||||
if ( indexValue == null ) {
|
if ( indexValue == null ) {
|
||||||
throw new HibernateException( "Illegal null value for list index encountered while reading: "
|
throw new HibernateException( "Illegal null value for list index encountered while reading: "
|
||||||
|
|
|
@ -12,7 +12,6 @@ import java.util.function.BiConsumer;
|
||||||
|
|
||||||
import org.hibernate.LockMode;
|
import org.hibernate.LockMode;
|
||||||
import org.hibernate.collection.spi.PersistentMap;
|
import org.hibernate.collection.spi.PersistentMap;
|
||||||
import org.hibernate.engine.spi.CollectionKey;
|
|
||||||
import org.hibernate.internal.log.LoggingHelper;
|
import org.hibernate.internal.log.LoggingHelper;
|
||||||
import org.hibernate.metamodel.mapping.PluralAttributeMapping;
|
import org.hibernate.metamodel.mapping.PluralAttributeMapping;
|
||||||
import org.hibernate.spi.NavigablePath;
|
import org.hibernate.spi.NavigablePath;
|
||||||
|
@ -90,10 +89,8 @@ public class MapInitializer extends AbstractImmediateCollectionInitializer<Abstr
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void readCollectionRow(
|
protected void readCollectionRow(ImmediateCollectionInitializerData data, List<Object> loadingState) {
|
||||||
CollectionKey collectionKey,
|
final RowProcessingState rowProcessingState = data.getRowProcessingState();
|
||||||
List<Object> loadingState,
|
|
||||||
RowProcessingState rowProcessingState) {
|
|
||||||
final Object key = mapKeyAssembler.assemble( rowProcessingState );
|
final Object key = mapKeyAssembler.assemble( rowProcessingState );
|
||||||
if ( key == null ) {
|
if ( key == null ) {
|
||||||
// If element is null, then NotFoundAction must be IGNORE
|
// If element is null, then NotFoundAction must be IGNORE
|
||||||
|
|
|
@ -11,7 +11,6 @@ import java.util.function.BiConsumer;
|
||||||
|
|
||||||
import org.hibernate.LockMode;
|
import org.hibernate.LockMode;
|
||||||
import org.hibernate.collection.spi.PersistentSet;
|
import org.hibernate.collection.spi.PersistentSet;
|
||||||
import org.hibernate.engine.spi.CollectionKey;
|
|
||||||
import org.hibernate.internal.log.LoggingHelper;
|
import org.hibernate.internal.log.LoggingHelper;
|
||||||
import org.hibernate.metamodel.mapping.PluralAttributeMapping;
|
import org.hibernate.metamodel.mapping.PluralAttributeMapping;
|
||||||
import org.hibernate.spi.NavigablePath;
|
import org.hibernate.spi.NavigablePath;
|
||||||
|
@ -77,10 +76,8 @@ public class SetInitializer extends AbstractImmediateCollectionInitializer<Abstr
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void readCollectionRow(
|
protected void readCollectionRow(ImmediateCollectionInitializerData data, List<Object> loadingState) {
|
||||||
CollectionKey collectionKey,
|
final RowProcessingState rowProcessingState = data.getRowProcessingState();
|
||||||
List<Object> loadingState,
|
|
||||||
RowProcessingState rowProcessingState) {
|
|
||||||
final Object element = elementAssembler.assemble( rowProcessingState );
|
final Object element = elementAssembler.assemble( rowProcessingState );
|
||||||
if ( element == null ) {
|
if ( element == null ) {
|
||||||
// If element is null, then NotFoundAction must be IGNORE
|
// If element is null, then NotFoundAction must be IGNORE
|
||||||
|
|
|
@ -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
|
@Override
|
||||||
public void resolveInstance(EmbeddableInitializerData data) {
|
public void resolveInstance(EmbeddableInitializerData data) {
|
||||||
if ( data.getState() != State.KEY_RESOLVED ) {
|
if ( data.getState() != State.KEY_RESOLVED ) {
|
||||||
|
|
|
@ -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
|
@Override
|
||||||
public void resolveInstance(DiscriminatedEntityInitializerData data) {
|
public void resolveInstance(DiscriminatedEntityInitializerData data) {
|
||||||
if ( data.getState() != State.KEY_RESOLVED ) {
|
if ( data.getState() != State.KEY_RESOLVED ) {
|
||||||
|
|
|
@ -102,6 +102,23 @@ public class EntityDelayedFetchInitializer
|
||||||
return referencedModelPart;
|
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
|
@Override
|
||||||
public void resolveInstance(EntityDelayedFetchInitializerData data) {
|
public void resolveInstance(EntityDelayedFetchInitializerData data) {
|
||||||
if ( data.getState() != State.KEY_RESOLVED ) {
|
if ( data.getState() != State.KEY_RESOLVED ) {
|
||||||
|
|
|
@ -63,6 +63,7 @@ import org.hibernate.sql.results.graph.Initializer;
|
||||||
import org.hibernate.sql.results.graph.InitializerData;
|
import org.hibernate.sql.results.graph.InitializerData;
|
||||||
import org.hibernate.sql.results.graph.InitializerParent;
|
import org.hibernate.sql.results.graph.InitializerParent;
|
||||||
import org.hibernate.sql.results.graph.basic.BasicResultAssembler;
|
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.EntityInitializer;
|
||||||
import org.hibernate.sql.results.graph.entity.EntityResultGraphNode;
|
import org.hibernate.sql.results.graph.entity.EntityResultGraphNode;
|
||||||
import org.hibernate.sql.results.graph.internal.AbstractInitializer;
|
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 isPartOfKey;
|
||||||
private final boolean isResultInitializer;
|
private final boolean isResultInitializer;
|
||||||
private final boolean hasKeyManyToOne;
|
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<?> keyAssembler;
|
||||||
private final @Nullable DomainResultAssembler<?> identifierAssembler;
|
private final @Nullable DomainResultAssembler<?> identifierAssembler;
|
||||||
|
@ -163,6 +172,15 @@ public class EntityInitializerImpl extends AbstractInitializer<EntityInitializer
|
||||||
this.parent = parent;
|
this.parent = parent;
|
||||||
this.isResultInitializer = isResultInitializer;
|
this.isResultInitializer = isResultInitializer;
|
||||||
this.isPartOfKey = Initializer.isPartOfKey( navigablePath, parent );
|
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";
|
assert identifierFetch != null || isResultInitializer : "Identifier must be fetched, unless this is a result initializer";
|
||||||
if ( identifierFetch == null ) {
|
if ( identifierFetch == null ) {
|
||||||
|
@ -256,6 +274,21 @@ public class EntityInitializerImpl extends AbstractInitializer<EntityInitializer
|
||||||
this.affectedByFilter = affectedByFilter;
|
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
|
@Override
|
||||||
protected EntityInitializerData createInitializerData(RowProcessingState rowProcessingState) {
|
protected EntityInitializerData createInitializerData(RowProcessingState rowProcessingState) {
|
||||||
return new EntityInitializerData( rowProcessingState );
|
return new EntityInitializerData( rowProcessingState );
|
||||||
|
@ -299,6 +332,10 @@ public class EntityInitializerImpl extends AbstractInitializer<EntityInitializer
|
||||||
}
|
}
|
||||||
data.setState( State.KEY_RESOLVED );
|
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
|
// reset row state
|
||||||
data.concreteDescriptor = null;
|
data.concreteDescriptor = null;
|
||||||
data.entityKey = 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 );
|
resolveEntityKey( data, id );
|
||||||
if ( !entityKeyOnly ) {
|
if ( !entityKeyOnly ) {
|
||||||
// Resolve the entity instance early as we have no key many-to-one
|
// 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) {
|
private void resolveKeySubInitializers(EntityInitializerData data) {
|
||||||
final RowProcessingState rowProcessingState = data.getRowProcessingState();
|
final RowProcessingState rowProcessingState = data.getRowProcessingState();
|
||||||
for ( Initializer<?> initializer : subInitializers[data.concreteDescriptor.getSubclassId()] ) {
|
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
|
@Override
|
||||||
public void initializeInstanceFromParent(Object parentInstance, EntityInitializerData data) {
|
public void initializeInstanceFromParent(Object parentInstance, EntityInitializerData data) {
|
||||||
final AttributeMapping attributeMapping = getInitializedPart().asAttributeMapping();
|
final AttributeMapping attributeMapping = getInitializedPart().asAttributeMapping();
|
||||||
|
@ -1384,6 +1458,10 @@ public class EntityInitializerImpl extends AbstractInitializer<EntityInitializer
|
||||||
return isPartOfKey;
|
return isPartOfKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean isPreviousRowReuse() {
|
||||||
|
return previousRowReuse;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public EntityPersister getConcreteDescriptor(EntityInitializerData data) {
|
public EntityPersister getConcreteDescriptor(EntityInitializerData data) {
|
||||||
assert data.getState() != State.UNINITIALIZED;
|
assert data.getState() != State.UNINITIALIZED;
|
||||||
|
|
|
@ -104,6 +104,23 @@ public class EntitySelectFetchInitializer<Data extends EntitySelectFetchInitiali
|
||||||
return navigablePath;
|
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
|
@Override
|
||||||
public void resolveInstance(Data data) {
|
public void resolveInstance(Data data) {
|
||||||
if ( data.getState() != State.KEY_RESOLVED ) {
|
if ( data.getState() != State.KEY_RESOLVED ) {
|
||||||
|
|
|
@ -8,6 +8,7 @@ package org.hibernate.sql.results.internal;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.function.BiConsumer;
|
||||||
import java.util.function.Consumer;
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
import org.hibernate.collection.spi.PersistentCollection;
|
import org.hibernate.collection.spi.PersistentCollection;
|
||||||
|
@ -68,6 +69,11 @@ public class LoadingCollectionEntryImpl implements LoadingCollectionEntry {
|
||||||
loadingEntryConsumer.accept( loadingState );
|
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) {
|
@Override public void finishLoading(ExecutionContext executionContext) {
|
||||||
collectionInstance.injectLoadedState(
|
collectionInstance.injectLoadedState(
|
||||||
getCollectionDescriptor().getAttributeMapping(),
|
getCollectionDescriptor().getAttributeMapping(),
|
||||||
|
|
|
@ -109,6 +109,7 @@ public class StandardJdbcValuesMapping implements JdbcValuesMapping {
|
||||||
private int initializerId;
|
private int initializerId;
|
||||||
boolean hasCollectionInitializers;
|
boolean hasCollectionInitializers;
|
||||||
Boolean dynamicInstantiation;
|
Boolean dynamicInstantiation;
|
||||||
|
Boolean containsMultipleCollectionFetches;
|
||||||
|
|
||||||
public AssemblerCreationStateImpl(
|
public AssemblerCreationStateImpl(
|
||||||
JdbcValuesMapping jdbcValuesMapping,
|
JdbcValuesMapping jdbcValuesMapping,
|
||||||
|
@ -127,6 +128,20 @@ public class StandardJdbcValuesMapping implements JdbcValuesMapping {
|
||||||
return dynamicInstantiation;
|
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
|
@Override
|
||||||
public int acquireInitializerId() {
|
public int acquireInitializerId() {
|
||||||
return initializerId++;
|
return initializerId++;
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue