HHH-16833 Assertion Error when inserting two entities linked with a OneToOne relation

This commit is contained in:
Andrea Boriero 2023-07-11 16:37:09 +02:00 committed by Andrea Boriero
parent 5d85db9211
commit 188529da36
6 changed files with 30 additions and 231 deletions

View File

@ -133,14 +133,15 @@ class DatabaseSnapshotExecutor {
entityDescriptor.forEachAttributeMapping( entityDescriptor.forEachAttributeMapping(
attributeMapping -> { attributeMapping -> {
final NavigablePath navigablePath = rootPath.append( attributeMapping.getAttributeName() ); final NavigablePath navigablePath = rootPath.append( attributeMapping.getAttributeName() );
domainResults.add( final DomainResult<Object> snapshotDomainResult = attributeMapping.createSnapshotDomainResult(
attributeMapping.createSnapshotDomainResult( navigablePath,
navigablePath, rootTableGroup,
rootTableGroup, null,
null, state
state
)
); );
if ( snapshotDomainResult != null ) {
domainResults.add( snapshotDomainResult );
}
} }
); );

View File

@ -88,12 +88,9 @@ import org.hibernate.sql.results.graph.embeddable.EmbeddableValuedFetchable;
import org.hibernate.sql.results.graph.entity.EntityFetch; import org.hibernate.sql.results.graph.entity.EntityFetch;
import org.hibernate.sql.results.graph.entity.EntityValuedFetchable; import org.hibernate.sql.results.graph.entity.EntityValuedFetchable;
import org.hibernate.sql.results.graph.entity.internal.EntityDelayedFetchImpl; import org.hibernate.sql.results.graph.entity.internal.EntityDelayedFetchImpl;
import org.hibernate.sql.results.graph.entity.internal.EntityDelayedResultImpl;
import org.hibernate.sql.results.graph.entity.internal.EntityFetchJoinedImpl; import org.hibernate.sql.results.graph.entity.internal.EntityFetchJoinedImpl;
import org.hibernate.sql.results.graph.entity.internal.EntityFetchSelectImpl; import org.hibernate.sql.results.graph.entity.internal.EntityFetchSelectImpl;
import org.hibernate.sql.results.graph.entity.internal.EntityResultImpl;
import org.hibernate.sql.results.graph.entity.internal.EntityResultJoinedSubclassImpl; import org.hibernate.sql.results.graph.entity.internal.EntityResultJoinedSubclassImpl;
import org.hibernate.sql.results.graph.entity.internal.NotFoundSnapshotResult;
import org.hibernate.sql.results.internal.domain.CircularBiDirectionalFetchImpl; import org.hibernate.sql.results.internal.domain.CircularBiDirectionalFetchImpl;
import org.hibernate.sql.results.internal.domain.CircularFetchImpl; import org.hibernate.sql.results.internal.domain.CircularFetchImpl;
import org.hibernate.type.ComponentType; import org.hibernate.type.ComponentType;
@ -1709,73 +1706,17 @@ public class ToOneAttributeMapping
TableGroup parentTableGroup, TableGroup parentTableGroup,
String resultVariable, String resultVariable,
DomainResultCreationState creationState) { DomainResultCreationState creationState) {
// We need a join if either // it's a Snapshot then we just need the value of the FK when it belongs to the parentTableGroup
// - the association is mapped with `@NotFound` if ( sideNature == ForeignKeyDescriptor.Nature.KEY ) {
// - the key is on the referring side i.e. this is an inverse to-one return foreignKeyDescriptor.getKeyPart().createDomainResult(
// and if the FK refers to a non-PK
final boolean forceJoin = hasNotFoundAction()
|| sideNature == ForeignKeyDescriptor.Nature.TARGET
|| referencedPropertyName != null;
final TableGroup tableGroupToUse;
if ( forceJoin ) {
tableGroupToUse = creationState.getSqlAstCreationState().getFromClauseAccess().resolveTableGroup(
navigablePath,
np -> {
final TableGroupJoin tableGroupJoin = createTableGroupJoin(
navigablePath,
parentTableGroup,
null,
null,
getDefaultSqlAstJoinType( parentTableGroup ),
true,
false,
creationState.getSqlAstCreationState()
);
parentTableGroup.addTableGroupJoin( tableGroupJoin );
return tableGroupJoin.getJoinedGroup();
}
);
}
else {
tableGroupToUse = createTableGroupForDelayedFetch(
navigablePath, navigablePath,
parentTableGroup, parentTableGroup,
resultVariable, resultVariable,
creationState creationState
); );
} }
if ( hasNotFoundAction() ) {
assert tableGroupToUse != parentTableGroup;
//noinspection unchecked
return new NotFoundSnapshotResult(
navigablePath,
this,
parentTableGroup,
tableGroupToUse,
creationState
);
}
if ( referencedPropertyName == null ) {
//noinspection unchecked
return new EntityDelayedResultImpl(
navigablePath.append( EntityIdentifierMapping.ID_ROLE_NAME ),
this,
tableGroupToUse,
creationState
);
}
else { else {
// We don't support proxies based on a non-PK yet, so we must fetch the whole entity return null;
final EntityResultImpl entityResult = new EntityResultImpl(
navigablePath,
this,
tableGroupToUse,
null
);
entityResult.afterInitialize( entityResult, creationState );
//noinspection unchecked
return entityResult;
} }
} }
@ -2360,7 +2301,20 @@ public class ToOneAttributeMapping
@Override @Override
public void addToCacheKey(MutableCacheKeyBuilder cacheKey, Object value, SharedSessionContractImplementor session) { public void addToCacheKey(MutableCacheKeyBuilder cacheKey, Object value, SharedSessionContractImplementor session) {
foreignKeyDescriptor.addToCacheKey( cacheKey, foreignKeyDescriptor.getAssociationKeyFromSide( value, sideNature.inverse(), session ), session ); final Object cacheValue;
// the value may come from a database snapshot, in this case it corresponds to the value of the key and can be
// added to the cache key
if ( value != null && foreignKeyDescriptor.getJavaType().getJavaTypeClass() == value.getClass() ) {
cacheValue = value;
}
else {
cacheValue = foreignKeyDescriptor.getAssociationKeyFromSide(
value,
sideNature.inverse(),
session
);
}
foreignKeyDescriptor.addToCacheKey( cacheKey, cacheValue, session );
} }
@Override @Override

View File

@ -1,69 +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.graph.entity.internal;
import org.hibernate.FetchNotFoundException;
import org.hibernate.annotations.NotFoundAction;
import org.hibernate.metamodel.mapping.internal.ToOneAttributeMapping;
import org.hibernate.spi.NavigablePath;
import org.hibernate.sql.results.graph.DomainResultAssembler;
import org.hibernate.sql.results.jdbc.spi.JdbcValuesSourceProcessingOptions;
import org.hibernate.sql.results.jdbc.spi.RowProcessingState;
import org.hibernate.type.descriptor.java.JavaType;
/**
* Specialized DomainResultAssembler for {@link org.hibernate.annotations.NotFound} associations
*
* @author Steve Ebersole
*/
public class NotFoundSnapshotAssembler implements DomainResultAssembler {
private final NavigablePath navigablePath;
private final ToOneAttributeMapping toOneMapping;
private final DomainResultAssembler<?> keyValueAssembler;
private final DomainResultAssembler<?> targetValueAssembler;
public NotFoundSnapshotAssembler(
NavigablePath navigablePath,
ToOneAttributeMapping toOneMapping,
DomainResultAssembler<?> keyValueAssembler,
DomainResultAssembler<?> targetValueAssembler) {
assert toOneMapping.hasNotFoundAction();
this.navigablePath = navigablePath;
this.toOneMapping = toOneMapping;
this.keyValueAssembler = keyValueAssembler;
this.targetValueAssembler = targetValueAssembler;
}
@Override
public Object assemble(RowProcessingState rowProcessingState, JdbcValuesSourceProcessingOptions options) {
final Object keyValue = keyValueAssembler.assemble( rowProcessingState );
final Object targetValue = targetValueAssembler.assemble( rowProcessingState );
// because of `@NotFound` these could be mismatched
if ( keyValue != null ) {
if ( targetValue != null ) {
if ( toOneMapping.getNotFoundAction() == NotFoundAction.IGNORE ) {
return null;
}
else {
throw new FetchNotFoundException(
toOneMapping.getAssociatedEntityMappingType().getEntityName(),
keyValue
);
}
}
}
return targetValue;
}
@Override
public JavaType<?> getAssembledJavaType() {
return toOneMapping.getJavaType();
}
}

View File

@ -1,88 +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.graph.entity.internal;
import org.hibernate.metamodel.mapping.ForeignKeyDescriptor;
import org.hibernate.metamodel.mapping.internal.ToOneAttributeMapping;
import org.hibernate.spi.NavigablePath;
import org.hibernate.sql.ast.tree.from.TableGroup;
import org.hibernate.sql.results.graph.AssemblerCreationState;
import org.hibernate.sql.results.graph.DomainResult;
import org.hibernate.sql.results.graph.DomainResultAssembler;
import org.hibernate.sql.results.graph.DomainResultCreationState;
import org.hibernate.sql.results.graph.FetchParentAccess;
import org.hibernate.type.descriptor.java.JavaType;
/**
* @author Steve Ebersole
*/
public class NotFoundSnapshotResult implements DomainResult {
private final NavigablePath navigablePath;
private final ToOneAttributeMapping toOneMapping;
private final DomainResult<?> keyResult;
private final DomainResult<?> targetResult;
public NotFoundSnapshotResult(
NavigablePath navigablePath,
ToOneAttributeMapping toOneMapping,
TableGroup keyTableGroup,
TableGroup targetTableGroup,
DomainResultCreationState creationState) {
this.navigablePath = navigablePath;
this.toOneMapping = toOneMapping;
// NOTE: this currently assumes that only the key side can be
// defined with `@NotFound`. That feels like a reasonable
// assumption, though there is sme question whether to support
// this for the inverse side also when a join table is used.
//
// however, that would mean a 1-1 with a join-table which
// is pretty odd mapping
final ForeignKeyDescriptor fkDescriptor = toOneMapping.getForeignKeyDescriptor();
this.keyResult = fkDescriptor.createKeyDomainResult(
navigablePath,
targetTableGroup,
null,
creationState
);
this.targetResult = fkDescriptor.createTargetDomainResult(
navigablePath,
targetTableGroup,
null,
creationState
);
}
@Override
public NavigablePath getNavigablePath() {
return navigablePath;
}
@Override
public JavaType<?> getResultJavaType() {
return toOneMapping.getJavaType();
}
@Override
public String getResultVariable() {
return null;
}
@Override
public DomainResultAssembler<Object> createResultAssembler(
FetchParentAccess parentAccess,
AssemblerCreationState creationState) {
return new NotFoundSnapshotAssembler(
navigablePath,
toOneMapping,
keyResult.createResultAssembler( parentAccess, creationState ),
targetResult.createResultAssembler( parentAccess, creationState )
);
}
}

View File

@ -149,11 +149,10 @@ public class ManyToOneType extends EntityType {
return true; return true;
} }
assert current.getClass().isAssignableFrom( old.getClass() );
// the ids are fully resolved, so compare them with isDirty(), not isModified() // the ids are fully resolved, so compare them with isDirty(), not isModified()
return getIdentifierOrUniqueKeyType( session.getFactory() ) return getIdentifierOrUniqueKeyType( session.getFactory() )
.isDirty( getIdentifier( old, session ), getIdentifier( current, session ), session ); .isDirty( old, getIdentifier( current, session ), session );
} }
@Override @Override

View File

@ -29,7 +29,7 @@ public class VersionedBidirectionalOneToOneMergeTest {
AnotherTestEntity anotherTestEntity = new AnotherTestEntity(); AnotherTestEntity anotherTestEntity = new AnotherTestEntity();
scope.inTransaction( scope.inTransaction(
session -> { session -> {
session.persist( anotherTestEntity ); session.merge( anotherTestEntity );
} }
); );
@ -66,6 +66,8 @@ public class VersionedBidirectionalOneToOneMergeTest {
@Id @Id
UUID uuid = UUID.randomUUID(); UUID uuid = UUID.randomUUID();
String name;
@OneToOne(mappedBy = "anotherTestEntity") @OneToOne(mappedBy = "anotherTestEntity")
TestEntity testEntity; TestEntity testEntity;