continued work on circular fetch detection. still need to work through the cases involving embeddables "in between"

This commit is contained in:
Steve Ebersole 2020-01-24 08:49:50 -06:00 committed by Andrea Boriero
parent 4f750cdc55
commit 00b5a700eb
18 changed files with 476 additions and 196 deletions

View File

@ -0,0 +1,134 @@
= Circular fetching
[IMPORTANT]
====
We need to make sure that any association is only join-fetched once. E.g.
```
from LineItem l
join fetch l.order o
join fetch l.order o2
```
This should be illegal. It is possible to handle it specially, but that would be very complicated.
`FromClauseIndex#findJoinFetch(NavigablePath parentPath, String fetchableName)` - if this does not return null we
know that the association is already join fetched and we should throw and exception
====
== one-to-one
```
@Entity
class Root {
...
@OneToOne(mappedBy="root1")
Leaf leaf1;
@OneToOne(mappedBy="root2")
Leaf leaf2;
}
@Entity
class Leaf {
...
@OneToOne
@JoinColumn(name="root_id_1")
Root root1;
@OneToOne
@JoinColumn(name="root_id_2")
Root root2;
}
```
Given this model, we have the following mappings from modelPart to "identifying columns":
* `Root#leaf1` -> `leaf.root_id_1`
* `Root#leaf2` -> `leaf.root_id_2`
* `Leaf#root1` -> `leaf.root_id_1`
* `Leaf#root2` -> `leaf.root_id_2`
So given a query like:
```
from Root r
join fetch r.leaf1 l
join fetch l.root1
join fetch l.root2
```
`l.root1` is circular whereas `l.root2` is not. We'd know this by looking at the "identifying columns".
Specifically, `l.root1` is considered circular **not** because it refers back to `Root(r)` but because it maps to the
same column(s) as its parent: `leaf.root_id_1`
// we need to be able to ultimately be able to resolve the "identifying columns" for a given path. E.g.
```
interface DomainResultCreationState {
...
Fetchable resolveFetchable(NavigablePath navigablePath) {
// the path passed in here would be `pathToParent` (i.e. `Root(r).leaf1(l)`)
// we'd used that to determine the lhs's identifying columns via
// `Fetchable#getIdentifyingColumnExpressions` and check them against the
// identifying columns for the Fetchable we are processing
}
}
```
== many-to-one
```
@Entity
@Table(name="orders")
class Order {
...
@OneToMany(mappedBy="order")
List<LineItem> lineItems;
}
@Entity
@Table(name="lines")
class LineItem {
...
@ManyToOne
@JoinColumn(name="order_id")
Order order;
}
```
Given this model, we have the following mappings from modelPart to "identifying columns":
* `LineItem#order` -> `lines.order_id`
* `Order#lineItems#{element}` -> `lines.order_id`
Hibernate needs to handle circularity in a fetch-graph. E.g.:
```
select o
from Order o
join fetch o.lineItems l
join fetch l.order
```
Here, the join fetch of `l.order` is circular, meaning we do not want to render a join in the SQL for it
because it is already part of the from-clause via `Order o`.
Recognizing circularity needs to happen in a number of mapping scenarios and I believe the conditions vary
depending on the type of mapping involved (one-to-one, many-to-one, many-to-many). Ideally we can find commonality
and handle these conditions uniformly.

View File

@ -6,6 +6,8 @@
*/
package org.hibernate;
import java.util.Collection;
/**
* @author Steve Ebersole
*/
@ -36,7 +38,7 @@ public class PropertySetterAccessException extends PropertyAccessException {
propertyName,
expectedType.getName(),
target,
value
loggablePropertyValueString( value )
),
true,
persistentClass,
@ -44,6 +46,13 @@ public class PropertySetterAccessException extends PropertyAccessException {
);
}
public static String loggablePropertyValueString(Object value) {
if ( value instanceof Collection ) {
return value.getClass().getSimpleName();
}
return value.toString();
}
@Override
public String toString() {
return super.originalMessage();

View File

@ -6,6 +6,8 @@
*/
package org.hibernate.internal.log;
import java.util.Collection;
import org.hibernate.collection.spi.PersistentCollection;
import org.hibernate.engine.spi.CollectionKey;
import org.hibernate.engine.spi.EntityKey;

View File

@ -55,7 +55,6 @@ import org.hibernate.sql.results.graph.Fetchable;
import org.hibernate.sql.results.graph.FetchableContainer;
import org.hibernate.sql.results.graph.collection.internal.CollectionDomainResult;
import org.hibernate.sql.results.internal.SqlSelectionImpl;
import org.hibernate.sql.results.spi.CircularFetchDetector;
import org.jboss.logging.Logger;
@ -376,7 +375,6 @@ public class LoaderSelectBuilder {
orderByFragments.put( orderByFragment, tableGroup );
}
private final CircularFetchDetector circularFetchDetector = new CircularFetchDetector();
private int fetchDepth = 0;
private List<Fetch> visitFetches(FetchParent fetchParent, QuerySpec querySpec, LoaderSqlAstCreationState creationState) {
@ -387,9 +385,9 @@ public class LoaderSelectBuilder {
final Consumer<Fetchable> processor = fetchable -> {
final NavigablePath fetchablePath = fetchParent.getNavigablePath().append( fetchable.getFetchableName() );
final Fetch biDirectionalFetch = circularFetchDetector.findBiDirectionalFetch(
final Fetch biDirectionalFetch = fetchable.resolveCircularFetch(
fetchablePath,
fetchParent,
fetchable,
creationState
);
@ -426,7 +424,7 @@ public class LoaderSelectBuilder {
try {
if ( ! (fetchable instanceof BasicValuedModelPart) ) {
fetchDepth--;
fetchDepth++;
}
Fetch fetch = fetchable.generateFetch(
fetchParent,

View File

@ -37,6 +37,8 @@ public interface EntityValuedModelPart extends FetchableContainer {
getEntityMappingType().visitSubParts( consumer, targetType );
}
String[] getIdentifyingColumnExpressions();
@Override
default <T> DomainResult<T> createDomainResult(
NavigablePath navigablePath,

View File

@ -163,14 +163,15 @@ public class EmbeddedAttributeMapping
return navigableRole;
}
@Override
public boolean isCircular(FetchParent fetchParent, SqlAstProcessingState creationState) {
public Fetch resolveCircularFetch(
NavigablePath fetchablePath,
FetchParent fetchParent,
SqlAstProcessingState creationState) {
// an embeddable can never be circular
return false;
return null;
}
@Override
public Fetch generateFetch(
FetchParent fetchParent,
@ -279,4 +280,9 @@ public class EmbeddedAttributeMapping
public int getNumberOfFetchables() {
return getEmbeddableTypeDescriptor().getNumberOfAttributeMappings();
}
@Override
public String toString() {
return "EmbeddedAttributeMapping {" + navigableRole + "}";
}
}

View File

@ -20,9 +20,11 @@ import org.hibernate.metamodel.mapping.ModelPart;
import org.hibernate.metamodel.model.domain.NavigableRole;
import org.hibernate.persister.collection.CollectionPersister;
import org.hibernate.query.NavigablePath;
import org.hibernate.sql.ast.spi.SqlAstProcessingState;
import org.hibernate.sql.ast.tree.from.TableGroup;
import org.hibernate.sql.results.graph.DomainResult;
import org.hibernate.sql.results.graph.DomainResultCreationState;
import org.hibernate.sql.results.graph.Fetch;
import org.hibernate.sql.results.graph.FetchParent;
import org.hibernate.sql.results.graph.collection.internal.EntityCollectionPartTableGroup;
import org.hibernate.sql.results.graph.entity.EntityFetch;
@ -115,6 +117,14 @@ public class EntityCollectionPart implements CollectionPart, EntityAssociationMa
return FetchStrategy.IMMEDIATE_JOIN;
}
@Override
public Fetch resolveCircularFetch(
NavigablePath fetchablePath,
FetchParent fetchParent,
SqlAstProcessingState creationState) {
return null;
}
@Override
public EntityFetch generateFetch(
FetchParent fetchParent,
@ -173,4 +183,13 @@ public class EntityCollectionPart implements CollectionPart, EntityAssociationMa
public int getNumberOfFetchables() {
return entityMappingType.getNumberOfFetchables();
}
public String getMappedBy() {
return collectionDescriptor.getMappedByProperty();
}
@Override
public String toString() {
return "EntityCollectionPart {" + navigableRole + "}";
}
}

View File

@ -7,11 +7,16 @@
package org.hibernate.metamodel.mapping.internal;
import org.hibernate.LockMode;
import org.hibernate.NotYetImplementedFor6Exception;
import org.hibernate.engine.FetchStrategy;
import org.hibernate.engine.FetchTiming;
import org.hibernate.engine.internal.JoinHelper;
import org.hibernate.mapping.ManyToOne;
import org.hibernate.mapping.OneToOne;
import org.hibernate.mapping.ToOne;
import org.hibernate.metamodel.mapping.EntityAssociationMapping;
import org.hibernate.metamodel.mapping.EntityMappingType;
import org.hibernate.metamodel.mapping.EntityValuedModelPart;
import org.hibernate.metamodel.mapping.ForeignKeyDescriptor;
import org.hibernate.metamodel.mapping.ManagedMappingType;
import org.hibernate.metamodel.mapping.ModelPart;
@ -37,26 +42,37 @@ import org.hibernate.sql.ast.tree.from.TableReference;
import org.hibernate.sql.ast.tree.predicate.Predicate;
import org.hibernate.sql.results.graph.DomainResult;
import org.hibernate.sql.results.graph.DomainResultCreationState;
import org.hibernate.sql.results.graph.Fetch;
import org.hibernate.sql.results.graph.FetchParent;
import org.hibernate.sql.results.graph.entity.EntityFetch;
import org.hibernate.sql.results.graph.entity.EntityValuedFetchable;
import org.hibernate.sql.results.graph.entity.internal.EntityFetchDelayedImpl;
import org.hibernate.sql.results.graph.entity.internal.EntityFetchJoinedImpl;
import org.hibernate.sql.results.graph.entity.internal.EntityFetchSelectImpl;
import org.hibernate.sql.results.internal.domain.BiDirectionalFetchImpl;
/**
* @author Steve Ebersole
*/
public class SingularAssociationAttributeMapping extends AbstractSingularAttributeMapping
implements EntityValuedFetchable, EntityAssociationMapping, TableGroupJoinProducer {
public enum Cardinality {
ONE_TO_ONE,
MANY_TO_ONE,
LOGICAL_ONE_TO_ONE
}
private final NavigableRole navigableRole;
private final String subRole;
private final String sqlAliasStem;
private final boolean isNullable;
private final boolean referringPrimaryKey;
private final boolean unwrapProxy;
private final String referencedPropertyName;
private final boolean referringPrimaryKey;
private final Cardinality cardinality;
private ForeignKeyDescriptor foreignKeyDescriptor;
@ -64,7 +80,7 @@ public class SingularAssociationAttributeMapping extends AbstractSingularAttribu
public SingularAssociationAttributeMapping(
String name,
int stateArrayPosition,
ToOne value,
ToOne bootValue,
StateArrayContributorMetadataAccess attributeMetadataAccess,
FetchStrategy mappedFetchStrategy,
EntityMappingType type,
@ -80,19 +96,33 @@ public class SingularAssociationAttributeMapping extends AbstractSingularAttribu
propertyAccess
);
this.sqlAliasStem = SqlAliasStemHelper.INSTANCE.generateStemFromAttributeName( name );
this.isNullable = value.isNullable();
referencedPropertyName = value.getReferencedPropertyName();
referringPrimaryKey = value.isReferenceToPrimaryKey();
unwrapProxy = value.isUnwrapProxy();
this.isNullable = bootValue.isNullable();
this.referencedPropertyName = bootValue.getReferencedPropertyName();
this.referringPrimaryKey = bootValue.isReferenceToPrimaryKey();
this.unwrapProxy = bootValue.isUnwrapProxy();
this.navigableRole = declaringType.getNavigableRole().appendContainer( name );
final int containerMarkerPosition = navigableRole.getFullPath().lastIndexOf( '#' );
if ( containerMarkerPosition < 0 ) {
subRole = name;
if ( referringPrimaryKey ) {
assert referencedPropertyName == null;
}
else {
subRole = navigableRole.getFullPath().substring( containerMarkerPosition + 1 );
assert referencedPropertyName != null;
}
if ( bootValue instanceof ManyToOne ) {
final ManyToOne manyToOne = (ManyToOne) bootValue;
if ( manyToOne.isLogicalOneToOne() ) {
cardinality = Cardinality.LOGICAL_ONE_TO_ONE;
}
else {
cardinality = Cardinality.MANY_TO_ONE;
}
}
else {
assert bootValue instanceof OneToOne;
cardinality = Cardinality.ONE_TO_ONE;
}
this.navigableRole = declaringType.getNavigableRole().appendContainer( name );
}
public void setForeignKeyDescriptor(ForeignKeyDescriptor foreignKeyDescriptor) {
@ -123,12 +153,33 @@ public class SingularAssociationAttributeMapping extends AbstractSingularAttribu
}
@Override
public boolean isCircular(FetchParent fetchParent, SqlAstProcessingState creationState) {
// E.g. say we have a query like:
// select p
// from Person p
// join fetch p.address a
// join fetch a.owner
public Fetch resolveCircularFetch(
NavigablePath fetchablePath,
FetchParent fetchParent,
SqlAstProcessingState creationState) {
// given a typical Order/LineItem model and a query like:
// select o
// from Order o
// join fetch o.lineItems l
// join fetch l.order
//
// - note : Order has a collection of LineItems which is "mapped by" LineItem#order
//
// the join-fetch for `l.order` ought point back to `o`.
//
// `o` -> Order(o)
// `l` -> Order(o).lineItems(l).{element}
// `l.order` -> Order(o).lineItems(l).{element}.order
//
// both `Order(o)` and `Order(o).lineItems(l).order` have the same identifying columns, so we know
// they are circular. So how do we resolve the columns? ...
//
// see `org.hibernate.loader.JoinWalker.isDuplicateAssociation(java.lang.String, java.lang.String[], org.hibernate.type.AssociationType)` in
// previous versions of Hibernate
//
// For `l.order` we are in SingularAssociationAttributeMapping as the Fetchable, so we have access to the FK descriptor.
// For `o` (the potential circular target reference) we need to locate the
//
//
// where `owner` is the "owner" (in the mapped-by sense) of the association. In other words it is a
// bi-directional mapping.
@ -137,39 +188,90 @@ public class SingularAssociationAttributeMapping extends AbstractSingularAttribu
// What we need to determine is whether owner is the same as address's container. This might include
// multiple parent-paths which we need to walk up to find the container (an entity of collection)
final NavigablePath parentNavigablePath = fetchParent.getNavigablePath();
NavigablePath pathToParentParent = parentNavigablePath.getParent();
final NavigablePath pathToParent = fetchParent.getNavigablePath();
final NavigablePath pathToParentParent = pathToParent.getParent();
// pathToParent : org.hibernate.orm.test.annotations.embedded.EmbeddedCircularFetchTests$RootEntity(r).intermediateComponent.leaves.{element}
// pathToParentParent : org.hibernate.orm.test.annotations.embedded.EmbeddedCircularFetchTests$RootEntity(r).intermediateComponent.leaves
// attributeName : rootEntity
// referencedPropertyName : null
if ( pathToParentParent == null ) {
return false;
return null;
}
for ( int i = 0; i < numberOfPathElementsToContainer; i++ ) {
pathToParentParent = pathToParentParent.getParent();
}
assert pathToParentParent != null;
final ModelPartContainer modelPart = creationState.getSqlAstCreationState()
final TableGroup parentParentTableGroup = creationState.getSqlAstCreationState()
.getFromClauseAccess()
.findTableGroup( pathToParentParent )
.getModelPart();
.findTableGroup( pathToParentParent );
parentParentTableGroup.getModelPart().findContainingEntityMapping()
final ModelPartContainer parentParentPart = parentParentTableGroup.getModelPart();
final ModelPart parentPart = parentParentPart.findSubPart( pathToParent.getLocalName(), null );
if ( ! parentPart.equals( fetchParent.getReferencedModePart() ) ) {
throw new AssertionError( );
}
final ModelPart subPart = modelPart.findSubPart( parentNavigablePath.getLocalName(), null );
if ( subPart instanceof EntityAssociationMapping ) {
final EntityAssociationMapping part = (EntityAssociationMapping) subPart;
if ( parentNavigablePath.getLocalName().equals( referencedPropertyName )
&& part.getFetchableName().equals( referencedPropertyName ) ) {
return true;
final EntityMappingType containingEntityMapping = findContainingEntityMapping();
// find the key-columns for the `parentParentTableGroup` and see if they match the fk-target
switch ( cardinality ) {
case ONE_TO_ONE:
case LOGICAL_ONE_TO_ONE: {
if ( ! EntityValuedModelPart.class.isInstance( parentPart ) ) {
throw new IllegalStateException(
"Parent part [" + pathToParent + "] did not refer to a `EntityValuedModelPart` - " + parentPart
);
}
final EntityValuedModelPart entityValuedParentPart = (EntityValuedModelPart) parentPart;
throw new NotYetImplementedFor6Exception( getClass() );
}
else if ( part.getKeyTargetMatchPart() != null
&& part.getKeyTargetMatchPart().getPartName().equals( getAttributeName() ) ) {
return true;
case MANY_TO_ONE: {
}
default: {
throw new UnsupportedOperationException( "Unknown to-one singular attribute cardinality - " + cardinality.name() );
}
}
if ( parentPart instanceof EntityCollectionPart ) {
final EntityCollectionPart entityCollectionPart = (EntityCollectionPart) parentPart;
final String mappedBy = entityCollectionPart.getMappedBy();
if ( mappedBy.equals( getAttributeName() ) ) {
return new BiDirectionalFetchImpl(
FetchTiming.IMMEDIATE,
fetchablePath,
fetchParent,
this,
fetchParent.getNavigablePath().getParent()
);
}
}
else if ( parentPart instanceof EntityAssociationMapping ) {
final EntityAssociationMapping entitySubPart = (EntityAssociationMapping) parentPart;
final boolean condition1 = pathToParent.getLocalName().equals( referencedPropertyName )
&& entitySubPart.getFetchableName().equals( referencedPropertyName );
final boolean condition2 = entitySubPart.getKeyTargetMatchPart() != null
&& entitySubPart.getKeyTargetMatchPart().getPartName().equals( getAttributeName() );
if ( condition1 || condition2 ) {
return new BiDirectionalFetchImpl(
FetchTiming.IMMEDIATE,
fetchablePath,
fetchParent,
this,
fetchParent.getNavigablePath().getParent()
);
}
}
return false;
return null;
}
@Override
@ -339,4 +441,9 @@ public class SingularAssociationAttributeMapping extends AbstractSingularAttribu
public ModelPart getKeyTargetMatchPart() {
return foreignKeyDescriptor;
}
@Override
public String toString() {
return "SingularAssociationAttributeMapping {" + navigableRole + "}";
}
}

View File

@ -68,7 +68,6 @@ import org.hibernate.sql.results.graph.FetchParent;
import org.hibernate.sql.results.graph.Fetchable;
import org.hibernate.sql.results.graph.entity.EntityResultGraphNode;
import org.hibernate.sql.results.graph.instantiation.internal.DynamicInstantiation;
import org.hibernate.sql.results.spi.CircularFetchDetector;
import org.hibernate.type.descriptor.java.JavaTypeDescriptor;
/**
@ -82,7 +81,6 @@ public class StandardSqmSelectTranslator
extends BaseSqmToSqlAstConverter
implements DomainResultCreationState, SqmSelectTranslator {
private final LoadQueryInfluencers fetchInfluencers;
private final CircularFetchDetector circularFetchDetector = new CircularFetchDetector();
// prepare for 10 root selections to avoid list growth in most cases
private final List<DomainResult> domainResults = CollectionHelper.arrayList( 10 );
@ -233,9 +231,11 @@ public class StandardSqmSelectTranslator
final Consumer<Fetchable> fetchableConsumer = new Consumer<Fetchable>() {
@Override
public void accept(Fetchable fetchable) {
final Fetch biDirectionalFetch = circularFetchDetector.findBiDirectionalFetch(
final NavigablePath fetchablePath = fetchParent.getNavigablePath().append( fetchable.getFetchableName() );
final Fetch biDirectionalFetch = fetchable.resolveCircularFetch(
fetchablePath,
fetchParent,
fetchable,
getSqlAstCreationState().getCurrentProcessingState()
);
@ -246,7 +246,7 @@ public class StandardSqmSelectTranslator
try {
fetchDepth++;
final Fetch fetch = buildFetch( fetchParent, fetchable );
final Fetch fetch = buildFetch( fetchablePath, fetchParent, fetchable );
if ( fetch != null ) {
fetches.add( fetch );
@ -267,15 +267,13 @@ public class StandardSqmSelectTranslator
return fetches;
}
private Fetch buildFetch(FetchParent fetchParent, Fetchable fetchable) {
private Fetch buildFetch(NavigablePath fetchablePath, FetchParent fetchParent, Fetchable fetchable) {
// fetch has access to its parent in addition to the parent having its fetches.
//
// we could sever the parent -> fetch link ... it would not be "seen" while walking
// but it would still have access to its parent info - and be able to access its
// "initializing" state as part of AfterLoadAction
final NavigablePath fetchablePath = fetchParent.getNavigablePath().append( fetchable.getFetchableName() );
final GraphImplementor<?> previousGraphNode = currentJpaGraphNode;
final String alias;

View File

@ -8,6 +8,7 @@ package org.hibernate.sql.results.graph;
import java.util.List;
import org.hibernate.metamodel.mapping.ModelPart;
import org.hibernate.query.NavigablePath;
/**
@ -16,13 +17,29 @@ import org.hibernate.query.NavigablePath;
* @author Steve Ebersole
*/
public interface FetchParent extends DomainResultGraphNode {
/**
* This parent's mapping type
*/
FetchableContainer getReferencedMappingContainer();
/**
* This parent's type
* This parent's mapping type
*/
FetchableContainer getReferencedMappingType();
/**
* Whereas {@link #getReferencedMappingContainer} and {@link #getReferencedMappingType} return the
* referenced container type, this method returns the referenced part.
*
* E.g. for a many-to-one this methods returns the
* {@link org.hibernate.metamodel.mapping.internal.SingularAssociationAttributeMapping} while
* {@link #getReferencedMappingContainer} and {@link #getReferencedMappingType} return the referenced
* {@link org.hibernate.metamodel.mapping.EntityMappingType}.
*/
default ModelPart getReferencedModePart() {
return getReferencedMappingContainer();
}
/**
* Get the property path to this parent
*/

View File

@ -9,6 +9,7 @@ package org.hibernate.sql.results.graph;
import org.hibernate.LockMode;
import org.hibernate.engine.FetchStrategy;
import org.hibernate.engine.FetchTiming;
import org.hibernate.metamodel.mapping.EntityValuedModelPart;
import org.hibernate.metamodel.mapping.ModelPart;
import org.hibernate.query.NavigablePath;
import org.hibernate.sql.ast.spi.SqlAstProcessingState;
@ -26,6 +27,19 @@ public interface Fetchable extends ModelPart {
// per Fetch generation is performance drain. Would be better to
// simply pass these 2 pieces of information
/**
* For an association, this would return the foreign-key's "referring columns". Would target
* the columns defined by {@link EntityValuedModelPart#getIdentifyingColumnExpressions}
*/
String[] getIdentifyingColumnExpressions();
default Fetch resolveCircularFetch(
NavigablePath fetchablePath,
FetchParent fetchParent,
SqlAstProcessingState creationState) {
return null;
}
Fetch generateFetch(
FetchParent fetchParent,
NavigablePath fetchablePath,
@ -34,9 +48,4 @@ public interface Fetchable extends ModelPart {
LockMode lockMode,
String resultVariable,
DomainResultCreationState creationState);
default boolean isCircular(FetchParent fetchParent, SqlAstProcessingState creationState){
return false;
}
}

View File

@ -8,6 +8,7 @@ package org.hibernate.sql.results.graph.entity.internal;
import java.util.function.Consumer;
import org.hibernate.metamodel.mapping.ModelPart;
import org.hibernate.sql.results.graph.FetchableContainer;
import org.hibernate.sql.results.graph.entity.AbstractEntityResultGraphNode;
import org.hibernate.sql.results.graph.entity.EntityResult;
@ -57,6 +58,11 @@ public class EntityResultImpl extends AbstractEntityResultGraphNode implements E
return getReferencedMappingContainer();
}
@Override
public EntityValuedModelPart getReferencedModePart() {
return getEntityValuedModelPart();
}
@Override
public String getResultVariable() {
return resultVariable;
@ -82,4 +88,8 @@ public class EntityResultImpl extends AbstractEntityResultGraphNode implements E
return new EntityAssembler( getResultJavaTypeDescriptor(), initializer );
}
@Override
public String toString() {
return "EntityResultImpl {" + getNavigablePath() + "}";
}
}

View File

@ -56,10 +56,7 @@ public class Helper {
};
}
else {
return initializer -> {
// noinspection Convert2MethodRef
initializers.add( initializer );
};
return initializers::add;
}
}

View File

@ -14,6 +14,8 @@ import org.hibernate.engine.FetchStrategy;
import org.hibernate.engine.FetchTiming;
import org.hibernate.metamodel.mapping.EntityMappingType;
import org.hibernate.metamodel.mapping.MappingType;
import org.hibernate.metamodel.mapping.ModelPart;
import org.hibernate.metamodel.mapping.PluralAttributeMapping;
import org.hibernate.metamodel.model.domain.NavigableRole;
import org.hibernate.query.NavigablePath;
import org.hibernate.sql.results.graph.AssemblerCreationState;
@ -25,6 +27,9 @@ import org.hibernate.sql.results.graph.FetchParent;
import org.hibernate.sql.results.graph.FetchParentAccess;
import org.hibernate.sql.results.graph.Fetchable;
import org.hibernate.sql.results.graph.Initializer;
import org.hibernate.sql.results.graph.embeddable.EmbeddableInitializer;
import org.hibernate.sql.results.graph.embeddable.EmbeddableValuedFetchable;
import org.hibernate.sql.results.graph.entity.EntityInitializer;
import org.hibernate.sql.results.jdbc.spi.JdbcValuesSourceProcessingOptions;
import org.hibernate.sql.results.jdbc.spi.RowProcessingState;
import org.hibernate.type.descriptor.java.JavaTypeDescriptor;
@ -164,7 +169,7 @@ public class BiDirectionalFetchImpl implements BiDirectionalFetch, Fetchable {
@Override
public Object assemble(RowProcessingState rowProcessingState, JdbcValuesSourceProcessingOptions options) {
Initializer initializer = rowProcessingState.resolveInitializer( circularPath );
final EntityInitializer initializer = resolveCircularInitializer( rowProcessingState );
if ( initializer.getInitializedInstance() == null ) {
initializer.resolveKey( rowProcessingState );
initializer.resolveInstance( rowProcessingState );
@ -173,6 +178,24 @@ public class BiDirectionalFetchImpl implements BiDirectionalFetch, Fetchable {
return initializer.getInitializedInstance();
}
private EntityInitializer resolveCircularInitializer(RowProcessingState rowProcessingState) {
final Initializer initializer = rowProcessingState.resolveInitializer( circularPath );
final ModelPart initializedPart = initializer.getInitializedPart();
if ( initializedPart instanceof EntityInitializer ) {
return (EntityInitializer) initializedPart;
}
NavigablePath path = circularPath.getParent();
Initializer parentInitializer = rowProcessingState.resolveInitializer( path );
while ( ! ( parentInitializer instanceof EntityInitializer ) ) {
path = path.getParent();
parentInitializer = rowProcessingState.resolveInitializer( path );
}
return (EntityInitializer) parentInitializer;
}
@Override
public JavaTypeDescriptor getAssembledJavaTypeDescriptor() {
return javaTypeDescriptor;

View File

@ -11,6 +11,7 @@ import org.hibernate.query.NavigablePath;
import org.hibernate.sql.ast.spi.SqlSelection;
import org.hibernate.sql.exec.spi.ExecutionContext;
import org.hibernate.sql.results.graph.entity.EntityFetch;
import org.hibernate.sql.results.graph.entity.EntityInitializer;
/**
* State pertaining to the processing of a single "row" of a JdbcValuesSource

View File

@ -1,56 +0,0 @@
/*
* 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.sql.results.spi;
import org.hibernate.engine.FetchTiming;
import org.hibernate.sql.results.graph.Fetch;
import org.hibernate.sql.results.graph.FetchParent;
import org.hibernate.sql.results.graph.Fetchable;
import org.hibernate.query.NavigablePath;
import org.hibernate.sql.ast.spi.SqlAstProcessingState;
import org.hibernate.sql.results.internal.domain.BiDirectionalFetchImpl;
/**
* Maintains state while processing a Fetch graph to be able to detect
* and handle circular bi-directional references
*
* @author Steve Ebersole
*/
public class CircularFetchDetector {
public Fetch findBiDirectionalFetch(
FetchParent fetchParent,
Fetchable fetchable,
SqlAstProcessingState creationState) {
if ( !fetchable.isCircular( fetchParent, creationState ) ) {
return null;
}
final NavigablePath navigablePath = fetchParent.getNavigablePath();
if ( navigablePath.getParent().getParent() == null ) {
return new BiDirectionalFetchImpl(
FetchTiming.IMMEDIATE,
navigablePath,
fetchParent,
fetchable,
fetchParent.getNavigablePath().getParent()
);
}
else {
return new BiDirectionalFetchImpl(
// todo (6.0) : needs to be the parent-parent's fetch timing
// atm we do not have the ability to get this
null,
navigablePath.append( fetchable.getFetchableName() ),
fetchParent,
fetchable,
navigablePath.getParent()
);
}
}
}

View File

@ -7,7 +7,6 @@
package org.hibernate.orm.test.annotations.embedded;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import javax.persistence.Embeddable;
import javax.persistence.Embedded;
@ -28,11 +27,8 @@ import org.hibernate.testing.orm.junit.SessionFactory;
import org.hibernate.testing.orm.junit.SessionFactoryScope;
import org.junit.jupiter.api.Test;
import org.hamcrest.CoreMatchers;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
/**
@ -50,6 +46,12 @@ import static org.junit.Assert.assertTrue;
@ServiceRegistry.Setting(name = AvailableSettings.GENERATE_STATISTICS, value = "true")
})
public class EmbeddedCircularFetchTests {
// todo (6.0 : this (along with the `org.hibernate.orm.test.sql.exec.onetoone.bidirectional` package)
// probably makes better sense in a dedicated `org.hibernate.orm.test.fetch.circular` package:
// - `org.hibernate.orm.test.fetch.circular.embedded`
// - `org.hibernate.orm.test.fetch.circular.onetoone`
// - `org.hibernate.orm.test.fetch.circular.manytoone`
// @Test
// @TestForIssue(jiraKey = "HHH-9642")
// public void testEmbeddedAndOneToManyHql(SessionFactoryScope scope) {
@ -156,9 +158,9 @@ public class EmbeddedCircularFetchTests {
RootEntity.class
).uniqueResult();
assertTrue( Hibernate.isInitialized( result.getIntermediateComponent().getLeaves() ) );
assertThat( result.getIntermediateComponent().getLeaves().size(), is( 1 ) );
assertThat( result.getIntermediateComponent().getLeaves().size(), is( 2 ) );
assertThat( session.getSessionFactory().getStatistics().getPrepareStatementCount(), is( 1 ) );
assertThat( session.getSessionFactory().getStatistics().getPrepareStatementCount(), is( 1L ) );
}
);

View File

@ -66,24 +66,24 @@ public class EntityWithBidirectionalOneToOneTest extends SessionFactoryBasedFunc
public void testGetParent() {
inTransaction( session -> {
final Parent parent = session.get( Parent.class, 1 );
Child child = parent.getChild();
Child child = parent.getOwnedBidirectionalChild();
assertThat( child, notNullValue() );
assertTrue(
Hibernate.isInitialized( child ),
"The child eager OneToOne association is not initialized"
);
assertThat( child.getName(), notNullValue() );
assertThat( child.getParent(), notNullValue() );
assertThat( child.getParent(), notNullValue() );
assertThat( child.getParentMappedByChild(), notNullValue() );
assertThat( child.getParentMappedByChild(), notNullValue() );
Child2 child2 = parent.getChild2();
Child2 child2 = parent.getChildMappedByParent1();
assertThat( child2, notNullValue() );
assertTrue(
Hibernate.isInitialized( child2 ),
"The child2 eager OneToOne association is not initialized"
);
assertThat( child2.getName(), equalTo( "Fab" ) );
assertThat( child2.getParent1(), notNullValue() );
assertThat( child2.getOwnedBidirectionalParent(), notNullValue() );
} );
}
@ -98,7 +98,7 @@ public class EntityWithBidirectionalOneToOneTest extends SessionFactoryBasedFunc
Child2 child2 = new Child2( 6, parent );
child2.setName( "Fab2" );
child2.setParent2( parent );
child2.setUnidirectionalParent( parent );
session.save( parent );
session.save( child );
@ -107,29 +107,29 @@ public class EntityWithBidirectionalOneToOneTest extends SessionFactoryBasedFunc
inTransaction( session -> {
final Parent parent = session.get( Parent.class, 4 );
Child child = parent.getChild();
Child child = parent.getOwnedBidirectionalChild();
assertThat( child, notNullValue() );
assertTrue(
Hibernate.isInitialized( child ),
"The child eager OneToOne association is not initialized"
);
assertThat( child.getName(), notNullValue() );
assertThat( child.getParent(), notNullValue() );
assertThat( child.getParentMappedByChild(), notNullValue() );
Child2 child2 = parent.getChild2();
Child2 child2 = parent.getChildMappedByParent1();
assertThat( child2, notNullValue() );
assertTrue(
Hibernate.isInitialized( child2 ),
"The child2 eager OneToOne association is not initialized"
);
assertThat( child2.getName(), equalTo( "Fab2" ) );
assertThat( child2.getParent1(), notNullValue() );
assertThat( child2.getParent1().getDescription(), equalTo( "Hibernate OGM" ) );
assertThat( child2.getOwnedBidirectionalParent(), notNullValue() );
assertThat( child2.getOwnedBidirectionalParent().getDescription(), equalTo( "Hibernate OGM" ) );
Parent parent2 = child2.getParent2();
Parent parent2 = child2.getUnidirectionalParent();
assertThat( parent2, notNullValue() );
assertThat( parent2.getDescription(), equalTo( "Hibernate OGM" ) );
assertThat( parent2.getChild(), notNullValue() );
assertThat( parent2.getOwnedBidirectionalChild(), notNullValue() );
} );
}
@ -145,7 +145,7 @@ public class EntityWithBidirectionalOneToOneTest extends SessionFactoryBasedFunc
child2.setName( "Fab2" );
Parent parent2 = new Parent( 6, "Hibernate OGM" );
child2.setParent2( parent2 );
child2.setUnidirectionalParent( parent2 );
Child child1 = new Child( 8, parent2 );
@ -160,29 +160,29 @@ public class EntityWithBidirectionalOneToOneTest extends SessionFactoryBasedFunc
final Parent parent = session.get( Parent.class, 4 );
assertThat( parent.getDescription(), equalTo( "Hibernate Search" ) );
Child child = parent.getChild();
Child child = parent.getOwnedBidirectionalChild();
assertThat( child, notNullValue() );
assertTrue(
Hibernate.isInitialized( child ),
"The child eager OneToOne association is not initialized"
);
assertThat( child.getName(), notNullValue() );
assertThat( child.getParent(), notNullValue() );
assertThat( child.getParentMappedByChild(), notNullValue() );
Child2 child2 = parent.getChild2();
Child2 child2 = parent.getChildMappedByParent1();
assertThat( child2, notNullValue() );
assertTrue(
Hibernate.isInitialized( child2 ),
"The child2 eager OneToOne association is not initialized"
);
assertThat( child2.getName(), equalTo( "Fab2" ) );
assertThat( child2.getParent1(), notNullValue() );
assertThat( child2.getParent1().getDescription(), equalTo( "Hibernate Search" ) );
assertThat( child2.getOwnedBidirectionalParent(), notNullValue() );
assertThat( child2.getOwnedBidirectionalParent().getDescription(), equalTo( "Hibernate Search" ) );
Parent parent2 = child2.getParent2();
Parent parent2 = child2.getUnidirectionalParent();
assertThat( parent2, notNullValue() );
assertThat( parent2.getDescription(), equalTo( "Hibernate OGM" ) );
assertThat( parent2.getChild(), notNullValue() );
assertThat( parent2.getOwnedBidirectionalChild(), notNullValue() );
} );
}
@ -191,27 +191,27 @@ public class EntityWithBidirectionalOneToOneTest extends SessionFactoryBasedFunc
public void testGetChild() {
inTransaction( session -> {
final Child child = session.get( Child.class, 2 );
Parent parent = child.getParent();
Parent parent = child.getParentMappedByChild();
assertTrue(
Hibernate.isInitialized( parent ),
"The parent eager OneToOne association is not initialized"
);
assertThat( parent, notNullValue() );
assertThat( parent.getDescription(), notNullValue() );
Child child1 = parent.getChild();
Child child1 = parent.getOwnedBidirectionalChild();
assertThat( child1, notNullValue() );
assertTrue(
Hibernate.isInitialized( child1 ),
"The child eager OneToOne association is not initialized"
);
Child2 child2 = parent.getChild2();
Child2 child2 = parent.getChildMappedByParent1();
assertThat( child2, notNullValue() );
assertTrue(
Hibernate.isInitialized( child2 ),
"The child2 eager OneToOne association is not initialized"
);
assertThat( child2.getParent1(), notNullValue() );
assertThat( child2.getParent2(), nullValue() );
assertThat( child2.getOwnedBidirectionalParent(), notNullValue() );
assertThat( child2.getUnidirectionalParent(), nullValue() );
} );
}
@ -220,14 +220,14 @@ public class EntityWithBidirectionalOneToOneTest extends SessionFactoryBasedFunc
inTransaction(
session -> {
final Parent parent = session.createQuery(
"SELECT p FROM Parent p JOIN p.child WHERE p.id = :id",
"SELECT p FROM Parent p JOIN p.ownedBidirectionalChild WHERE p.id = :id",
Parent.class
)
.setParameter( "id", 1 )
.getSingleResult();
assertThat( parent.getChild(), notNullValue() );
String name = parent.getChild().getName();
assertThat( parent.getOwnedBidirectionalChild(), notNullValue() );
String name = parent.getOwnedBidirectionalChild().getName();
assertThat( name, notNullValue() );
}
);
@ -238,14 +238,14 @@ public class EntityWithBidirectionalOneToOneTest extends SessionFactoryBasedFunc
public void testHqlSelectChild() {
inTransaction(
session -> {
final String queryString = "SELECT c FROM Child c JOIN c.parent d WHERE d.id = :id";
final String queryString = "SELECT c FROM Child c JOIN c.parentMappedByChild d WHERE d.id = :id";
final Child child = session.createQuery( queryString, Child.class )
.setParameter( "id", 1 )
.getSingleResult();
assertThat( child.getParent(), notNullValue() );
assertThat( child.getParentMappedByChild(), notNullValue() );
String description = child.getParent().getDescription();
String description = child.getParentMappedByChild().getDescription();
assertThat( description, notNullValue() );
}
);
@ -256,10 +256,12 @@ public class EntityWithBidirectionalOneToOneTest extends SessionFactoryBasedFunc
@Id
private Integer id;
private String description;
@OneToOne
private Child child;
@OneToOne(mappedBy = "parent1")
private Child2 child2;
private Child ownedBidirectionalChild;
@OneToOne(mappedBy = "ownedBidirectionalParent")
private Child2 childMappedByParent1;
Parent() {
}
@ -289,20 +291,20 @@ public class EntityWithBidirectionalOneToOneTest extends SessionFactoryBasedFunc
this.description = description;
}
public Child getChild() {
return child;
public Child getOwnedBidirectionalChild() {
return ownedBidirectionalChild;
}
public void setChild(Child child) {
this.child = child;
public void setOwnedBidirectionalChild(Child ownedBidirectionalChild) {
this.ownedBidirectionalChild = ownedBidirectionalChild;
}
public Child2 getChild2() {
return child2;
public Child2 getChildMappedByParent1() {
return childMappedByParent1;
}
public void setChild2(Child2 child2) {
this.child2 = child2;
public void setChildMappedByParent1(Child2 childMappedByParent1) {
this.childMappedByParent1 = childMappedByParent1;
}
}
@ -313,17 +315,17 @@ public class EntityWithBidirectionalOneToOneTest extends SessionFactoryBasedFunc
private Integer id;
private String name;
@OneToOne(mappedBy = "child")
private Parent parent;
@OneToOne(mappedBy = "ownedBidirectionalChild")
private Parent parentMappedByChild;
Child() {
}
Child(Integer id, Parent parent) {
Child(Integer id, Parent parentMappedByChild) {
this.id = id;
this.parent = parent;
this.parent.setChild( this );
this.parentMappedByChild = parentMappedByChild;
this.parentMappedByChild.setOwnedBidirectionalChild( this );
}
public Integer getId() {
@ -342,12 +344,12 @@ public class EntityWithBidirectionalOneToOneTest extends SessionFactoryBasedFunc
this.name = name;
}
public Parent getParent() {
return parent;
public Parent getParentMappedByChild() {
return parentMappedByChild;
}
public void setParent(Parent parent) {
this.parent = parent;
public void setParentMappedByChild(Parent parentMappedByChild) {
this.parentMappedByChild = parentMappedByChild;
}
}
@ -360,18 +362,18 @@ public class EntityWithBidirectionalOneToOneTest extends SessionFactoryBasedFunc
private String name;
@OneToOne
private Parent parent1;
private Parent ownedBidirectionalParent;
@OneToOne
private Parent parent2;
private Parent unidirectionalParent;
Child2() {
}
Child2(Integer id, Parent parent1) {
Child2(Integer id, Parent ownedBidirectionalParent) {
this.id = id;
this.parent1 = parent1;
this.parent1.setChild2( this );
this.ownedBidirectionalParent = ownedBidirectionalParent;
this.ownedBidirectionalParent.setChildMappedByParent1( this );
}
@ -391,20 +393,20 @@ public class EntityWithBidirectionalOneToOneTest extends SessionFactoryBasedFunc
this.name = name;
}
public Parent getParent1() {
return parent1;
public Parent getOwnedBidirectionalParent() {
return ownedBidirectionalParent;
}
public void setParent1(Parent parent1) {
this.parent1 = parent1;
public void setOwnedBidirectionalParent(Parent ownedBidirectionalParent) {
this.ownedBidirectionalParent = ownedBidirectionalParent;
}
public Parent getParent2() {
return parent2;
public Parent getUnidirectionalParent() {
return unidirectionalParent;
}
public void setParent2(Parent parent) {
this.parent2 = parent;
public void setUnidirectionalParent(Parent parent) {
this.unidirectionalParent = parent;
}
}
}