Do not alter the join type anymore for non-real table groups that have table reference joins. Handle nullable key tables for to-ones properly and implement bidirectional one-to-one fetching optimization
This commit is contained in:
parent
45c891a75a
commit
24c758c2e9
|
@ -198,6 +198,7 @@ public class OneToOneSecondPass implements SecondPass {
|
||||||
manyToOne.setLazy( value.isLazy() );
|
manyToOne.setLazy( value.isLazy() );
|
||||||
manyToOne.setReferencedEntityName( value.getReferencedEntityName() );
|
manyToOne.setReferencedEntityName( value.getReferencedEntityName() );
|
||||||
manyToOne.setUnwrapProxy( value.isUnwrapProxy() );
|
manyToOne.setUnwrapProxy( value.isUnwrapProxy() );
|
||||||
|
manyToOne.markAsLogicalOneToOne();
|
||||||
prop.setValue( manyToOne );
|
prop.setValue( manyToOne );
|
||||||
Iterator otherSideJoinKeyColumns = otherSideJoin.getKey().getColumnIterator();
|
Iterator otherSideJoinKeyColumns = otherSideJoin.getKey().getColumnIterator();
|
||||||
while ( otherSideJoinKeyColumns.hasNext() ) {
|
while ( otherSideJoinKeyColumns.hasNext() ) {
|
||||||
|
|
|
@ -35,7 +35,19 @@ public interface Queryable extends ModelPart {
|
||||||
ModelPart findSubPart(String name, EntityMappingType treatTargetType);
|
ModelPart findSubPart(String name, EntityMappingType treatTargetType);
|
||||||
|
|
||||||
default ModelPart resolveSubPart(DotIdentifierSequence path) {
|
default ModelPart resolveSubPart(DotIdentifierSequence path) {
|
||||||
return path.resolve( (ModelPart) this, (part, name) -> ( (Queryable) part ).findSubPart( name, null ) );
|
return path.resolve(
|
||||||
|
(ModelPart) this,
|
||||||
|
(part, name) -> {
|
||||||
|
final String fullPath = part.getNavigableRole().getFullPath();
|
||||||
|
if ( fullPath.equals( name ) ) {
|
||||||
|
return part;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return ( (Queryable) part ).findSubPart( name.substring( fullPath.length() + 1 ), null );
|
||||||
|
}
|
||||||
|
},
|
||||||
|
(part, name) -> ( (Queryable) part ).findSubPart( name, null )
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -16,14 +16,18 @@ import java.util.function.Consumer;
|
||||||
import org.hibernate.LockMode;
|
import org.hibernate.LockMode;
|
||||||
import org.hibernate.engine.FetchStyle;
|
import org.hibernate.engine.FetchStyle;
|
||||||
import org.hibernate.engine.FetchTiming;
|
import org.hibernate.engine.FetchTiming;
|
||||||
|
import org.hibernate.engine.jdbc.spi.JdbcServices;
|
||||||
import org.hibernate.engine.spi.SharedSessionContractImplementor;
|
import org.hibernate.engine.spi.SharedSessionContractImplementor;
|
||||||
import org.hibernate.internal.util.StringHelper;
|
import org.hibernate.internal.util.StringHelper;
|
||||||
|
import org.hibernate.internal.util.collections.ArrayHelper;
|
||||||
import org.hibernate.mapping.Collection;
|
import org.hibernate.mapping.Collection;
|
||||||
import org.hibernate.mapping.IndexedConsumer;
|
import org.hibernate.mapping.IndexedConsumer;
|
||||||
|
import org.hibernate.mapping.Join;
|
||||||
import org.hibernate.mapping.ManyToOne;
|
import org.hibernate.mapping.ManyToOne;
|
||||||
import org.hibernate.mapping.OneToOne;
|
import org.hibernate.mapping.OneToOne;
|
||||||
import org.hibernate.mapping.PersistentClass;
|
import org.hibernate.mapping.PersistentClass;
|
||||||
import org.hibernate.mapping.Property;
|
import org.hibernate.mapping.Property;
|
||||||
|
import org.hibernate.mapping.Selectable;
|
||||||
import org.hibernate.mapping.ToOne;
|
import org.hibernate.mapping.ToOne;
|
||||||
import org.hibernate.metamodel.mapping.AssociationKey;
|
import org.hibernate.metamodel.mapping.AssociationKey;
|
||||||
import org.hibernate.metamodel.mapping.CollectionPart;
|
import org.hibernate.metamodel.mapping.CollectionPart;
|
||||||
|
@ -38,6 +42,8 @@ import org.hibernate.metamodel.mapping.PluralAttributeMapping;
|
||||||
import org.hibernate.metamodel.mapping.SelectableConsumer;
|
import org.hibernate.metamodel.mapping.SelectableConsumer;
|
||||||
import org.hibernate.metamodel.mapping.StateArrayContributorMetadataAccess;
|
import org.hibernate.metamodel.mapping.StateArrayContributorMetadataAccess;
|
||||||
import org.hibernate.metamodel.model.domain.NavigableRole;
|
import org.hibernate.metamodel.model.domain.NavigableRole;
|
||||||
|
import org.hibernate.persister.collection.QueryableCollection;
|
||||||
|
import org.hibernate.persister.entity.AbstractEntityPersister;
|
||||||
import org.hibernate.persister.entity.EntityPersister;
|
import org.hibernate.persister.entity.EntityPersister;
|
||||||
import org.hibernate.property.access.spi.PropertyAccess;
|
import org.hibernate.property.access.spi.PropertyAccess;
|
||||||
import org.hibernate.query.EntityIdentifierNavigablePath;
|
import org.hibernate.query.EntityIdentifierNavigablePath;
|
||||||
|
@ -99,6 +105,7 @@ public class ToOneAttributeMapping
|
||||||
|
|
||||||
private final String sqlAliasStem;
|
private final String sqlAliasStem;
|
||||||
private final boolean isNullable;
|
private final boolean isNullable;
|
||||||
|
private final boolean isKeyTableNullable;
|
||||||
private final boolean isConstrained;
|
private final boolean isConstrained;
|
||||||
private final boolean isIgnoreNotFound;
|
private final boolean isIgnoreNotFound;
|
||||||
private final boolean unwrapProxy;
|
private final boolean unwrapProxy;
|
||||||
|
@ -185,13 +192,41 @@ public class ToOneAttributeMapping
|
||||||
}
|
}
|
||||||
if ( referencedPropertyName == null ) {
|
if ( referencedPropertyName == null ) {
|
||||||
String bidirectionalAttributeName = null;
|
String bidirectionalAttributeName = null;
|
||||||
final PersistentClass entityBinding = manyToOne.getMetadata().getEntityBinding( manyToOne.getReferencedEntityName() );
|
final PersistentClass entityBinding = manyToOne.getMetadata()
|
||||||
final Iterator<Property> propertyClosureIterator = entityBinding.getPropertyClosureIterator();
|
.getEntityBinding( manyToOne.getReferencedEntityName() );
|
||||||
while (propertyClosureIterator.hasNext()) {
|
if ( cardinality == Cardinality.LOGICAL_ONE_TO_ONE ) {
|
||||||
final Property property = propertyClosureIterator.next();
|
// Handle join table cases
|
||||||
if ( property.getValue() instanceof Collection && name.equals( ( (Collection) property.getValue() ).getMappedByProperty() ) ) {
|
final Iterator<Join> joinClosureIterator = entityBinding.getJoinClosureIterator();
|
||||||
bidirectionalAttributeName = property.getName();
|
while ( joinClosureIterator.hasNext() ) {
|
||||||
break;
|
final Join join = joinClosureIterator.next();
|
||||||
|
if ( join.getPersistentClass().getEntityName().equals( entityBinding.getEntityName() )
|
||||||
|
&& join.getPropertySpan() == 1
|
||||||
|
&& join.getTable() == manyToOne.getTable()
|
||||||
|
&& equal( join.getKey().getColumnIterator(), manyToOne.getColumnIterator() ) ) {
|
||||||
|
bidirectionalAttributeName = join.getPropertyIterator().next().getName();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Simple one-to-one mapped by cases
|
||||||
|
if ( bidirectionalAttributeName == null ) {
|
||||||
|
final Iterator<Property> propertyClosureIterator = entityBinding.getPropertyClosureIterator();
|
||||||
|
while ( propertyClosureIterator.hasNext() ) {
|
||||||
|
final Property property = propertyClosureIterator.next();
|
||||||
|
if ( property.getValue() instanceof OneToOne && name.equals( ( (OneToOne) property.getValue() ).getMappedByProperty() ) ) {
|
||||||
|
bidirectionalAttributeName = property.getName();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
final Iterator<Property> propertyClosureIterator = entityBinding.getPropertyClosureIterator();
|
||||||
|
while ( propertyClosureIterator.hasNext() ) {
|
||||||
|
final Property property = propertyClosureIterator.next();
|
||||||
|
if ( property.getValue() instanceof Collection && name.equals( ( (Collection) property.getValue() ).getMappedByProperty() ) ) {
|
||||||
|
bidirectionalAttributeName = property.getName();
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.bidirectionalAttributeName = bidirectionalAttributeName;
|
this.bidirectionalAttributeName = bidirectionalAttributeName;
|
||||||
|
@ -199,6 +234,33 @@ public class ToOneAttributeMapping
|
||||||
else {
|
else {
|
||||||
this.bidirectionalAttributeName = referencedPropertyName;
|
this.bidirectionalAttributeName = referencedPropertyName;
|
||||||
}
|
}
|
||||||
|
if ( bootValue.isNullable() ) {
|
||||||
|
isKeyTableNullable = true;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
final JdbcServices jdbcServices = declaringEntityPersister.getFactory().getJdbcServices();
|
||||||
|
final String targetTableName = jdbcServices.getJdbcEnvironment()
|
||||||
|
.getQualifiedObjectNameFormatter()
|
||||||
|
.format(
|
||||||
|
manyToOne.getTable().getQualifiedTableName(),
|
||||||
|
jdbcServices.getDialect()
|
||||||
|
);
|
||||||
|
if ( CollectionPart.Nature.fromNameExact( navigableRole.getParent().getLocalName() ) != null ) {
|
||||||
|
final PluralAttributeMapping pluralAttribute = (PluralAttributeMapping) declaringEntityPersister.resolveSubPart(
|
||||||
|
navigableRole.getParent().getParent()
|
||||||
|
);
|
||||||
|
final QueryableCollection persister = (QueryableCollection) pluralAttribute.getCollectionDescriptor();
|
||||||
|
isKeyTableNullable = !persister.getTableName().equals( targetTableName );
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
final AbstractEntityPersister persister = (AbstractEntityPersister) declaringEntityPersister;
|
||||||
|
final int tableIndex = ArrayHelper.indexOf(
|
||||||
|
persister.getTableNames(),
|
||||||
|
targetTableName
|
||||||
|
);
|
||||||
|
isKeyTableNullable = persister.isNullableTable( tableIndex );
|
||||||
|
}
|
||||||
|
}
|
||||||
isOptional = ( (ManyToOne) bootValue ).isIgnoreNotFound();
|
isOptional = ( (ManyToOne) bootValue ).isIgnoreNotFound();
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
|
@ -265,6 +327,7 @@ public class ToOneAttributeMapping
|
||||||
this.bidirectionalAttributeName = bidirectionalAttributeName;
|
this.bidirectionalAttributeName = bidirectionalAttributeName;
|
||||||
}
|
}
|
||||||
isIgnoreNotFound = isNullable();
|
isIgnoreNotFound = isNullable();
|
||||||
|
isKeyTableNullable = isNullable();
|
||||||
isOptional = ! bootValue.isConstrained();
|
isOptional = ! bootValue.isConstrained();
|
||||||
}
|
}
|
||||||
isConstrained = bootValue.isConstrained();
|
isConstrained = bootValue.isConstrained();
|
||||||
|
@ -344,6 +407,7 @@ public class ToOneAttributeMapping
|
||||||
this.navigableRole = original.navigableRole;
|
this.navigableRole = original.navigableRole;
|
||||||
this.sqlAliasStem = original.sqlAliasStem;
|
this.sqlAliasStem = original.sqlAliasStem;
|
||||||
this.isNullable = original.isNullable;
|
this.isNullable = original.isNullable;
|
||||||
|
this.isKeyTableNullable = original.isKeyTableNullable;
|
||||||
this.isOptional = original.isOptional;
|
this.isOptional = original.isOptional;
|
||||||
this.isIgnoreNotFound = original.isIgnoreNotFound;
|
this.isIgnoreNotFound = original.isIgnoreNotFound;
|
||||||
this.unwrapProxy = original.unwrapProxy;
|
this.unwrapProxy = original.unwrapProxy;
|
||||||
|
@ -357,6 +421,23 @@ public class ToOneAttributeMapping
|
||||||
this.isConstrained = original.isConstrained;
|
this.isConstrained = original.isConstrained;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private boolean equal(Iterator<Selectable> lhsColumns, Iterator<Selectable> rhsColumns) {
|
||||||
|
boolean hasNext;
|
||||||
|
do {
|
||||||
|
final Selectable lhs = lhsColumns.next();
|
||||||
|
final Selectable rhs = rhsColumns.next();
|
||||||
|
if ( !lhs.getText().equals( rhs.getText() ) ) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
hasNext = lhsColumns.hasNext();
|
||||||
|
if ( hasNext != rhsColumns.hasNext() ) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} while ( hasNext );
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
private static void addPrefixedPropertyNames(
|
private static void addPrefixedPropertyNames(
|
||||||
Set<String> targetKeyPropertyNames,
|
Set<String> targetKeyPropertyNames,
|
||||||
String prefix,
|
String prefix,
|
||||||
|
@ -747,6 +828,16 @@ public class ToOneAttributeMapping
|
||||||
}
|
}
|
||||||
|
|
||||||
creationState.registerVisitedAssociationKey( foreignKeyDescriptor.getAssociationKey() );
|
creationState.registerVisitedAssociationKey( foreignKeyDescriptor.getAssociationKey() );
|
||||||
|
if ( cardinality == Cardinality.LOGICAL_ONE_TO_ONE && bidirectionalAttributeName != null ) {
|
||||||
|
final ModelPart bidirectionalModelPart = entityMappingType.findSubPart( bidirectionalAttributeName );
|
||||||
|
// Add the inverse association key side as well to be able to resolve to a CircularFetch
|
||||||
|
if ( bidirectionalModelPart instanceof ToOneAttributeMapping ) {
|
||||||
|
final ToOneAttributeMapping bidirectionalAttribute = (ToOneAttributeMapping) bidirectionalModelPart;
|
||||||
|
creationState.registerVisitedAssociationKey(
|
||||||
|
bidirectionalAttribute.getForeignKeyDescriptor().getAssociationKey()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
return new EntityFetchJoinedImpl(
|
return new EntityFetchJoinedImpl(
|
||||||
fetchParent,
|
fetchParent,
|
||||||
this,
|
this,
|
||||||
|
@ -875,7 +966,7 @@ public class ToOneAttributeMapping
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public SqlAstJoinType getDefaultSqlAstJoinType(TableGroup parentTableGroup) {
|
public SqlAstJoinType getDefaultSqlAstJoinType(TableGroup parentTableGroup) {
|
||||||
if ( isNullable ) {
|
if ( isKeyTableNullable || isNullable ) {
|
||||||
return SqlAstJoinType.LEFT;
|
return SqlAstJoinType.LEFT;
|
||||||
}
|
}
|
||||||
else if ( parentTableGroup.getModelPart() instanceof CollectionPart ) {
|
else if ( parentTableGroup.getModelPart() instanceof CollectionPart ) {
|
||||||
|
|
|
@ -24,13 +24,13 @@ public interface DotIdentifierSequence {
|
||||||
return getParent() == null;
|
return getParent() == null;
|
||||||
}
|
}
|
||||||
|
|
||||||
default <T> T resolve(T base, BiFunction<T, String, T> resolver) {
|
default <T> T resolve(T base, BiFunction<T, String, T> baseResolver, BiFunction<T, String, T> resolver) {
|
||||||
final T result;
|
final T result;
|
||||||
if ( getParent() == null ) {
|
if ( getParent() == null ) {
|
||||||
result = base;
|
result = baseResolver.apply( base, getLocalName() );
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
result = resolver.apply( getParent().resolve( base, resolver ), getLocalName() );
|
result = resolver.apply( getParent().resolve( base, baseResolver, resolver ), getLocalName() );
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
|
@ -3534,17 +3534,7 @@ public abstract class AbstractSqlAstTranslator<T extends JdbcOperation> implemen
|
||||||
}
|
}
|
||||||
else if ( !( joinedGroup instanceof LazyTableGroup ) || ( (LazyTableGroup) joinedGroup ).getUnderlyingTableGroup() != null ) {
|
else if ( !( joinedGroup instanceof LazyTableGroup ) || ( (LazyTableGroup) joinedGroup ).getUnderlyingTableGroup() != null ) {
|
||||||
appendSql( WHITESPACE );
|
appendSql( WHITESPACE );
|
||||||
SqlAstJoinType joinType = tableGroupJoin.getJoinType();
|
renderJoinType( tableGroupJoin.getJoinType() );
|
||||||
// todo (6.0): no exactly sure why this is necessary and IMO this should ideally go away.
|
|
||||||
// I realized that inverse one-to-one associations with join tables are considered "non-nullable" in the boot model.
|
|
||||||
// Due to that, we use an inner join for the table group join of that association which the following "works around"
|
|
||||||
// IMO such an association should be considered nullable. It's also odd that the association
|
|
||||||
// has the FK Side KEY, although it is clearly TARGET
|
|
||||||
// See org.hibernate.orm.test.annotations.manytoone.ManyToOneJoinTest.testOneToOneJoinTable2
|
|
||||||
if ( !joinedGroup.isRealTableGroup() && joinType == SqlAstJoinType.INNER && !joinedGroup.getTableReferenceJoins().isEmpty() ) {
|
|
||||||
joinType = SqlAstJoinType.LEFT;
|
|
||||||
}
|
|
||||||
renderJoinType( joinType );
|
|
||||||
|
|
||||||
if ( tableGroupJoin.getPredicate() != null && !tableGroupJoin.getPredicate().isEmpty() ) {
|
if ( tableGroupJoin.getPredicate() != null && !tableGroupJoin.getPredicate().isEmpty() ) {
|
||||||
renderTableGroup( joinedGroup, tableGroupJoin.getPredicate() );
|
renderTableGroup( joinedGroup, tableGroupJoin.getPredicate() );
|
||||||
|
|
|
@ -84,7 +84,7 @@ public class EntityWithBidirectionalAssociationsOneOfWhichIsAJoinTableTest {
|
||||||
session -> {
|
session -> {
|
||||||
final Parent parent = session.get( Parent.class, 1 );
|
final Parent parent = session.get( Parent.class, 1 );
|
||||||
statementInspector.assertExecutedCount( 1 );
|
statementInspector.assertExecutedCount( 1 );
|
||||||
statementInspector.assertNumberOfOccurrenceInQuery( 0, "join", 6 );
|
statementInspector.assertNumberOfOccurrenceInQuery( 0, "join", 3 );
|
||||||
Male son = parent.getSon();
|
Male son = parent.getSon();
|
||||||
assertThat( son, CoreMatchers.notNullValue() );
|
assertThat( son, CoreMatchers.notNullValue() );
|
||||||
assertTrue(
|
assertTrue(
|
||||||
|
@ -113,7 +113,7 @@ public class EntityWithBidirectionalAssociationsOneOfWhichIsAJoinTableTest {
|
||||||
scope.inTransaction( session -> {
|
scope.inTransaction( session -> {
|
||||||
final Male son = session.get( Male.class, 2 );
|
final Male son = session.get( Male.class, 2 );
|
||||||
statementInspector.assertExecutedCount( 1 );
|
statementInspector.assertExecutedCount( 1 );
|
||||||
statementInspector.assertNumberOfOccurrenceInQuery( 0, "join", 6 );
|
statementInspector.assertNumberOfOccurrenceInQuery( 0, "join", 3 );
|
||||||
Parent parent = son.getParent();
|
Parent parent = son.getParent();
|
||||||
assertThat( parent, CoreMatchers.notNullValue() );
|
assertThat( parent, CoreMatchers.notNullValue() );
|
||||||
assertTrue(
|
assertTrue(
|
||||||
|
@ -155,7 +155,7 @@ public class EntityWithBidirectionalAssociationsOneOfWhichIsAJoinTableTest {
|
||||||
// The join to the target table PARENT for Male#parent is avoided,
|
// The join to the target table PARENT for Male#parent is avoided,
|
||||||
// because the FK in the collection table is not-null and data from the target table is not needed
|
// because the FK in the collection table is not-null and data from the target table is not needed
|
||||||
statementInspector.assertNumberOfOccurrenceInQuery( 0, "join", 1 );
|
statementInspector.assertNumberOfOccurrenceInQuery( 0, "join", 1 );
|
||||||
statementInspector.assertNumberOfOccurrenceInQuery( 1, "join", 6 );
|
statementInspector.assertNumberOfOccurrenceInQuery( 1, "join", 3 );
|
||||||
assertThat( son.getParent(), CoreMatchers.notNullValue() );
|
assertThat( son.getParent(), CoreMatchers.notNullValue() );
|
||||||
|
|
||||||
String description = son.getParent().getDescription();
|
String description = son.getParent().getDescription();
|
||||||
|
@ -178,7 +178,7 @@ public class EntityWithBidirectionalAssociationsOneOfWhichIsAJoinTableTest {
|
||||||
.getSingleResult();
|
.getSingleResult();
|
||||||
statementInspector.assertExecutedCount( 2 );
|
statementInspector.assertExecutedCount( 2 );
|
||||||
statementInspector.assertNumberOfOccurrenceInQuery( 0, "join", 2 );
|
statementInspector.assertNumberOfOccurrenceInQuery( 0, "join", 2 );
|
||||||
statementInspector.assertNumberOfOccurrenceInQuery( 1, "join", 6 );
|
statementInspector.assertNumberOfOccurrenceInQuery( 1, "join", 3 );
|
||||||
Male son = parent.getSon();
|
Male son = parent.getSon();
|
||||||
assertThat( son, CoreMatchers.notNullValue() );
|
assertThat( son, CoreMatchers.notNullValue() );
|
||||||
assertTrue(
|
assertTrue(
|
||||||
|
|
Loading…
Reference in New Issue