HHH-15602 Fix bidirectional association management code
This commit is contained in:
parent
5303295c31
commit
4a55422187
|
@ -35,6 +35,7 @@ import net.bytebuddy.dynamic.scaffold.FieldLocator;
|
||||||
import net.bytebuddy.dynamic.scaffold.InstrumentedType;
|
import net.bytebuddy.dynamic.scaffold.InstrumentedType;
|
||||||
import net.bytebuddy.implementation.Implementation;
|
import net.bytebuddy.implementation.Implementation;
|
||||||
import net.bytebuddy.implementation.bytecode.ByteCodeAppender;
|
import net.bytebuddy.implementation.bytecode.ByteCodeAppender;
|
||||||
|
import net.bytebuddy.implementation.bytecode.assign.Assigner;
|
||||||
import net.bytebuddy.jar.asm.MethodVisitor;
|
import net.bytebuddy.jar.asm.MethodVisitor;
|
||||||
import net.bytebuddy.jar.asm.Opcodes;
|
import net.bytebuddy.jar.asm.Opcodes;
|
||||||
import net.bytebuddy.jar.asm.Type;
|
import net.bytebuddy.jar.asm.Type;
|
||||||
|
@ -57,7 +58,14 @@ final class BiDirectionalAssociationHandler implements Implementation {
|
||||||
return implementation;
|
return implementation;
|
||||||
}
|
}
|
||||||
String mappedBy = getMappedBy( persistentField, targetEntity, enhancementContext );
|
String mappedBy = getMappedBy( persistentField, targetEntity, enhancementContext );
|
||||||
if ( mappedBy == null || mappedBy.isEmpty() ) {
|
String bidirectionalAttributeName;
|
||||||
|
if ( mappedBy == null ) {
|
||||||
|
bidirectionalAttributeName = getMappedByManyToMany( persistentField, targetEntity, enhancementContext );
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
bidirectionalAttributeName = mappedBy;
|
||||||
|
}
|
||||||
|
if ( bidirectionalAttributeName == null || bidirectionalAttributeName.isEmpty() ) {
|
||||||
if ( log.isInfoEnabled() ) {
|
if ( log.isInfoEnabled() ) {
|
||||||
log.infof(
|
log.infof(
|
||||||
"Bi-directional association not managed for field [%s#%s]: Could not find target field in [%s]",
|
"Bi-directional association not managed for field [%s#%s]: Could not find target field in [%s]",
|
||||||
|
@ -70,15 +78,24 @@ final class BiDirectionalAssociationHandler implements Implementation {
|
||||||
}
|
}
|
||||||
|
|
||||||
TypeDescription targetType = FieldLocator.ForClassHierarchy.Factory.INSTANCE.make( targetEntity )
|
TypeDescription targetType = FieldLocator.ForClassHierarchy.Factory.INSTANCE.make( targetEntity )
|
||||||
.locate( mappedBy )
|
.locate( bidirectionalAttributeName )
|
||||||
.getField()
|
.getField()
|
||||||
.getType()
|
.getType()
|
||||||
.asErasure();
|
.asErasure();
|
||||||
|
|
||||||
if ( persistentField.hasAnnotation( OneToOne.class ) ) {
|
if ( persistentField.hasAnnotation( OneToOne.class ) ) {
|
||||||
implementation = Advice.withCustomMapping()
|
implementation = Advice.withCustomMapping()
|
||||||
.bind( CodeTemplates.FieldValue.class, persistentField.getFieldDescription() )
|
.bind(
|
||||||
.bind( CodeTemplates.MappedBy.class, mappedBy )
|
// We need to make the fieldValue writable for one-to-one to avoid stack overflows
|
||||||
|
// when unsetting the inverse field
|
||||||
|
new Advice.OffsetMapping.ForField.Resolved.Factory<>(
|
||||||
|
CodeTemplates.FieldValue.class,
|
||||||
|
persistentField.getFieldDescription(),
|
||||||
|
false,
|
||||||
|
Assigner.Typing.DYNAMIC
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.bind( CodeTemplates.InverseSide.class, mappedBy != null )
|
||||||
.to( CodeTemplates.OneToOneHandler.class )
|
.to( CodeTemplates.OneToOneHandler.class )
|
||||||
.wrap( implementation );
|
.wrap( implementation );
|
||||||
}
|
}
|
||||||
|
@ -86,7 +103,7 @@ final class BiDirectionalAssociationHandler implements Implementation {
|
||||||
if ( persistentField.hasAnnotation( OneToMany.class ) ) {
|
if ( persistentField.hasAnnotation( OneToMany.class ) ) {
|
||||||
implementation = Advice.withCustomMapping()
|
implementation = Advice.withCustomMapping()
|
||||||
.bind( CodeTemplates.FieldValue.class, persistentField.getFieldDescription() )
|
.bind( CodeTemplates.FieldValue.class, persistentField.getFieldDescription() )
|
||||||
.bind( CodeTemplates.MappedBy.class, mappedBy )
|
.bind( CodeTemplates.InverseSide.class, mappedBy != null )
|
||||||
.to( persistentField.getType().asErasure().isAssignableTo( Map.class )
|
.to( persistentField.getType().asErasure().isAssignableTo( Map.class )
|
||||||
? CodeTemplates.OneToManyOnMapHandler.class
|
? CodeTemplates.OneToManyOnMapHandler.class
|
||||||
: CodeTemplates.OneToManyOnCollectionHandler.class )
|
: CodeTemplates.OneToManyOnCollectionHandler.class )
|
||||||
|
@ -96,7 +113,7 @@ final class BiDirectionalAssociationHandler implements Implementation {
|
||||||
if ( persistentField.hasAnnotation( ManyToOne.class ) ) {
|
if ( persistentField.hasAnnotation( ManyToOne.class ) ) {
|
||||||
implementation = Advice.withCustomMapping()
|
implementation = Advice.withCustomMapping()
|
||||||
.bind( CodeTemplates.FieldValue.class, persistentField.getFieldDescription() )
|
.bind( CodeTemplates.FieldValue.class, persistentField.getFieldDescription() )
|
||||||
.bind( CodeTemplates.MappedBy.class, mappedBy )
|
.bind( CodeTemplates.BidirectionalAttribute.class, bidirectionalAttributeName )
|
||||||
.to( CodeTemplates.ManyToOneHandler.class )
|
.to( CodeTemplates.ManyToOneHandler.class )
|
||||||
.wrap( implementation );
|
.wrap( implementation );
|
||||||
}
|
}
|
||||||
|
@ -116,12 +133,13 @@ final class BiDirectionalAssociationHandler implements Implementation {
|
||||||
|
|
||||||
implementation = Advice.withCustomMapping()
|
implementation = Advice.withCustomMapping()
|
||||||
.bind( CodeTemplates.FieldValue.class, persistentField.getFieldDescription() )
|
.bind( CodeTemplates.FieldValue.class, persistentField.getFieldDescription() )
|
||||||
.bind( CodeTemplates.MappedBy.class, mappedBy )
|
.bind( CodeTemplates.InverseSide.class, mappedBy != null )
|
||||||
|
.bind( CodeTemplates.BidirectionalAttribute.class, bidirectionalAttributeName )
|
||||||
.to( CodeTemplates.ManyToManyHandler.class )
|
.to( CodeTemplates.ManyToManyHandler.class )
|
||||||
.wrap( implementation );
|
.wrap( implementation );
|
||||||
}
|
}
|
||||||
|
|
||||||
return new BiDirectionalAssociationHandler( implementation, targetEntity, targetType, mappedBy );
|
return new BiDirectionalAssociationHandler( implementation, managedCtClass, persistentField, targetEntity, targetType, bidirectionalAttributeName );
|
||||||
}
|
}
|
||||||
|
|
||||||
public static TypeDescription getTargetEntityClass(TypeDescription managedCtClass, AnnotatedFieldDescription persistentField) {
|
public static TypeDescription getTargetEntityClass(TypeDescription managedCtClass, AnnotatedFieldDescription persistentField) {
|
||||||
|
@ -186,16 +204,16 @@ final class BiDirectionalAssociationHandler implements Implementation {
|
||||||
}
|
}
|
||||||
|
|
||||||
private static String getMappedBy(AnnotatedFieldDescription target, TypeDescription targetEntity, ByteBuddyEnhancementContext context) {
|
private static String getMappedBy(AnnotatedFieldDescription target, TypeDescription targetEntity, ByteBuddyEnhancementContext context) {
|
||||||
String mappedBy = getMappedByNotManyToMany( target );
|
final String mappedBy = getMappedByFromAnnotation( target );
|
||||||
if ( mappedBy == null || mappedBy.isEmpty() ) {
|
if ( mappedBy == null || mappedBy.isEmpty() ) {
|
||||||
return getMappedByManyToMany( target, targetEntity, context );
|
return null;
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
// HHH-13446 - mappedBy from annotation may not be a valid bi-directional association, verify by calling isValidMappedBy()
|
// HHH-13446 - mappedBy from annotation may not be a valid bi-directional association, verify by calling isValidMappedBy()
|
||||||
return isValidMappedBy( target, targetEntity, mappedBy, context ) ? mappedBy : "";
|
return isValidMappedBy( target, targetEntity, mappedBy, context ) ? mappedBy : null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static boolean isValidMappedBy(AnnotatedFieldDescription persistentField, TypeDescription targetEntity, String mappedBy, ByteBuddyEnhancementContext context) {
|
private static boolean isValidMappedBy(AnnotatedFieldDescription persistentField, TypeDescription targetEntity, String mappedBy, ByteBuddyEnhancementContext context) {
|
||||||
try {
|
try {
|
||||||
FieldDescription f = FieldLocator.ForClassHierarchy.Factory.INSTANCE.make( targetEntity ).locate( mappedBy ).getField();
|
FieldDescription f = FieldLocator.ForClassHierarchy.Factory.INSTANCE.make( targetEntity ).locate( mappedBy ).getField();
|
||||||
|
@ -207,8 +225,7 @@ final class BiDirectionalAssociationHandler implements Implementation {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
private static String getMappedByFromAnnotation(AnnotatedFieldDescription target) {
|
||||||
private static String getMappedByNotManyToMany(AnnotatedFieldDescription target) {
|
|
||||||
try {
|
try {
|
||||||
AnnotationDescription.Loadable<OneToOne> oto = target.getAnnotation( OneToOne.class );
|
AnnotationDescription.Loadable<OneToOne> oto = target.getAnnotation( OneToOne.class );
|
||||||
if ( oto != null ) {
|
if ( oto != null ) {
|
||||||
|
@ -235,7 +252,7 @@ final class BiDirectionalAssociationHandler implements Implementation {
|
||||||
for ( FieldDescription f : targetEntity.getDeclaredFields() ) {
|
for ( FieldDescription f : targetEntity.getDeclaredFields() ) {
|
||||||
AnnotatedFieldDescription annotatedF = new AnnotatedFieldDescription( context, f );
|
AnnotatedFieldDescription annotatedF = new AnnotatedFieldDescription( context, f );
|
||||||
if ( context.isPersistentField( annotatedF )
|
if ( context.isPersistentField( annotatedF )
|
||||||
&& target.getName().equals( getMappedByNotManyToMany( annotatedF ) )
|
&& target.getName().equals( getMappedBy( annotatedF, entityType( annotatedF.getType() ), context ) )
|
||||||
&& target.getDeclaringType().asErasure().isAssignableTo( entityType( annotatedF.getType() ) ) ) {
|
&& target.getDeclaringType().asErasure().isAssignableTo( entityType( annotatedF.getType() ) ) ) {
|
||||||
if ( log.isDebugEnabled() ) {
|
if ( log.isDebugEnabled() ) {
|
||||||
log.debugf(
|
log.debugf(
|
||||||
|
@ -267,21 +284,28 @@ final class BiDirectionalAssociationHandler implements Implementation {
|
||||||
|
|
||||||
private final Implementation delegate;
|
private final Implementation delegate;
|
||||||
|
|
||||||
|
private final TypeDescription entity;
|
||||||
|
private final AnnotatedFieldDescription field;
|
||||||
|
|
||||||
private final TypeDescription targetEntity;
|
private final TypeDescription targetEntity;
|
||||||
|
|
||||||
private final TypeDescription targetType;
|
private final TypeDescription targetType;
|
||||||
|
|
||||||
private final String mappedBy;
|
private final String bidirectionalAttributeName;
|
||||||
|
|
||||||
private BiDirectionalAssociationHandler(
|
private BiDirectionalAssociationHandler(
|
||||||
Implementation delegate,
|
Implementation delegate,
|
||||||
|
TypeDescription entity,
|
||||||
|
AnnotatedFieldDescription field,
|
||||||
TypeDescription targetEntity,
|
TypeDescription targetEntity,
|
||||||
TypeDescription targetType,
|
TypeDescription targetType,
|
||||||
String mappedBy) {
|
String bidirectionalAttributeName) {
|
||||||
this.delegate = delegate;
|
this.delegate = delegate;
|
||||||
|
this.entity = entity;
|
||||||
|
this.field = field;
|
||||||
this.targetEntity = targetEntity;
|
this.targetEntity = targetEntity;
|
||||||
this.targetType = targetType;
|
this.targetType = targetType;
|
||||||
this.mappedBy = mappedBy;
|
this.bidirectionalAttributeName = bidirectionalAttributeName;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -315,11 +339,21 @@ final class BiDirectionalAssociationHandler implements Implementation {
|
||||||
super.visitMethodInsn(
|
super.visitMethodInsn(
|
||||||
Opcodes.INVOKEVIRTUAL,
|
Opcodes.INVOKEVIRTUAL,
|
||||||
targetEntity.getInternalName(),
|
targetEntity.getInternalName(),
|
||||||
EnhancerConstants.PERSISTENT_FIELD_READER_PREFIX + mappedBy,
|
EnhancerConstants.PERSISTENT_FIELD_READER_PREFIX + bidirectionalAttributeName,
|
||||||
Type.getMethodDescriptor( Type.getType( targetType.getDescriptor() ) ),
|
Type.getMethodDescriptor( Type.getType( targetType.getDescriptor() ) ),
|
||||||
false
|
false
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
else if ( name.equals( "getterSelf" ) ) {
|
||||||
|
super.visitVarInsn( Opcodes.ALOAD, 0 );
|
||||||
|
super.visitMethodInsn(
|
||||||
|
Opcodes.INVOKEVIRTUAL,
|
||||||
|
entity.getInternalName(),
|
||||||
|
EnhancerConstants.PERSISTENT_FIELD_READER_PREFIX + field.getName(),
|
||||||
|
Type.getMethodDescriptor( Type.getType( field.getDescriptor() ) ),
|
||||||
|
false
|
||||||
|
);
|
||||||
|
}
|
||||||
else if ( name.equals( "setterSelf" ) ) {
|
else if ( name.equals( "setterSelf" ) ) {
|
||||||
super.visitInsn( Opcodes.POP );
|
super.visitInsn( Opcodes.POP );
|
||||||
super.visitTypeInsn( Opcodes.CHECKCAST, targetEntity.getInternalName() );
|
super.visitTypeInsn( Opcodes.CHECKCAST, targetEntity.getInternalName() );
|
||||||
|
@ -327,7 +361,7 @@ final class BiDirectionalAssociationHandler implements Implementation {
|
||||||
super.visitMethodInsn(
|
super.visitMethodInsn(
|
||||||
Opcodes.INVOKEVIRTUAL,
|
Opcodes.INVOKEVIRTUAL,
|
||||||
targetEntity.getInternalName(),
|
targetEntity.getInternalName(),
|
||||||
EnhancerConstants.PERSISTENT_FIELD_WRITER_PREFIX + mappedBy,
|
EnhancerConstants.PERSISTENT_FIELD_WRITER_PREFIX + bidirectionalAttributeName,
|
||||||
Type.getMethodDescriptor( Type.getType( void.class ), Type.getType( targetType.getDescriptor() ) ),
|
Type.getMethodDescriptor( Type.getType( void.class ), Type.getType( targetType.getDescriptor() ) ),
|
||||||
false
|
false
|
||||||
);
|
);
|
||||||
|
@ -339,7 +373,7 @@ final class BiDirectionalAssociationHandler implements Implementation {
|
||||||
super.visitMethodInsn(
|
super.visitMethodInsn(
|
||||||
Opcodes.INVOKEVIRTUAL,
|
Opcodes.INVOKEVIRTUAL,
|
||||||
targetEntity.getInternalName(),
|
targetEntity.getInternalName(),
|
||||||
EnhancerConstants.PERSISTENT_FIELD_WRITER_PREFIX + mappedBy,
|
EnhancerConstants.PERSISTENT_FIELD_WRITER_PREFIX + bidirectionalAttributeName,
|
||||||
Type.getMethodDescriptor( Type.getType( void.class ), Type.getType( targetType.getDescriptor() ) ),
|
Type.getMethodDescriptor( Type.getType( void.class ), Type.getType( targetType.getDescriptor() ) ),
|
||||||
false
|
false
|
||||||
);
|
);
|
||||||
|
@ -368,11 +402,11 @@ final class BiDirectionalAssociationHandler implements Implementation {
|
||||||
return Objects.equals( delegate, that.delegate ) &&
|
return Objects.equals( delegate, that.delegate ) &&
|
||||||
Objects.equals( targetEntity, that.targetEntity ) &&
|
Objects.equals( targetEntity, that.targetEntity ) &&
|
||||||
Objects.equals( targetType, that.targetType ) &&
|
Objects.equals( targetType, that.targetType ) &&
|
||||||
Objects.equals( mappedBy, that.mappedBy );
|
Objects.equals( bidirectionalAttributeName, that.bidirectionalAttributeName );
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int hashCode() {
|
public int hashCode() {
|
||||||
return Objects.hash( delegate, targetEntity, targetType, mappedBy );
|
return Objects.hash( delegate, targetEntity, targetType, bidirectionalAttributeName );
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -356,15 +356,24 @@ class CodeTemplates {
|
||||||
|
|
||||||
static class OneToOneHandler {
|
static class OneToOneHandler {
|
||||||
@Advice.OnMethodEnter
|
@Advice.OnMethodEnter
|
||||||
static void enter(@FieldValue Object field, @Advice.Argument(0) Object argument, @MappedBy String mappedBy) {
|
static void enter(@FieldValue Object field, @Advice.Argument(0) Object argument, @InverseSide boolean inverseSide) {
|
||||||
if ( field != null && Hibernate.isPropertyInitialized( field, mappedBy ) && argument != null ) {
|
// Unset the inverse attribute, which possibly initializes the old value,
|
||||||
setterNull( field, null );
|
// only if this is the inverse side, or the old value is already initialized
|
||||||
|
if ( ( inverseSide || Hibernate.isInitialized( field ) ) && getterSelf() != null ) {
|
||||||
|
// We copy the old value, then set the field to null which we must do before
|
||||||
|
// unsetting the inverse attribute, as we'd otherwise run into a stack overflow situation
|
||||||
|
// The field is writable, so setting it to null here is actually a field write.
|
||||||
|
Object fieldCopy = field;
|
||||||
|
field = null;
|
||||||
|
setterNull( fieldCopy, null );
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Advice.OnMethodExit
|
@Advice.OnMethodExit
|
||||||
static void exit(@Advice.This Object self, @Advice.Argument(0) Object argument, @MappedBy String mappedBy) {
|
static void exit(@Advice.This Object self, @Advice.Argument(0) Object argument, @InverseSide boolean inverseSide) {
|
||||||
if ( argument != null && Hibernate.isPropertyInitialized( argument, mappedBy ) && getter( argument ) != self ) {
|
// Update the inverse attribute, which possibly initializes the argument value,
|
||||||
|
// only if this is the inverse side, or the argument value is already initialized
|
||||||
|
if ( argument != null && ( inverseSide || Hibernate.isInitialized( argument ) ) && getter( argument ) != self ) {
|
||||||
setterSelf( argument, self );
|
setterSelf( argument, self );
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -374,6 +383,11 @@ class CodeTemplates {
|
||||||
throw new AssertionError();
|
throw new AssertionError();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static Object getterSelf() {
|
||||||
|
// is replaced by the actual method call
|
||||||
|
throw new AssertionError();
|
||||||
|
}
|
||||||
|
|
||||||
static void setterNull(Object target, Object argument) {
|
static void setterNull(Object target, Object argument) {
|
||||||
// is replaced by the actual method call
|
// is replaced by the actual method call
|
||||||
throw new AssertionError();
|
throw new AssertionError();
|
||||||
|
@ -387,24 +401,30 @@ class CodeTemplates {
|
||||||
|
|
||||||
static class OneToManyOnCollectionHandler {
|
static class OneToManyOnCollectionHandler {
|
||||||
@Advice.OnMethodEnter
|
@Advice.OnMethodEnter
|
||||||
static void enter(@FieldValue Collection<?> field, @Advice.Argument(0) Collection<?> argument, @MappedBy String mappedBy) {
|
static void enter(@FieldValue Collection<?> field, @Advice.Argument(0) Collection<?> argument, @InverseSide boolean inverseSide) {
|
||||||
if ( field != null && Hibernate.isPropertyInitialized( field, mappedBy ) ) {
|
// If this is the inverse side or the old collection is already initialized,
|
||||||
|
// we must unset the respective ManyToOne of the old collection elements,
|
||||||
|
// because only the owning side is responsible for persisting the state.
|
||||||
|
if ( ( inverseSide || Hibernate.isInitialized( field ) ) && getterSelf() != null ) {
|
||||||
Object[] array = field.toArray();
|
Object[] array = field.toArray();
|
||||||
for ( Object array1 : array ) {
|
for ( int i = 0; i < array.length; i++ ) {
|
||||||
if ( argument == null || !argument.contains( array1 ) ) {
|
if ( ( inverseSide || Hibernate.isInitialized( array[i] ) ) && ( argument == null || !argument.contains( array[i] ) ) ) {
|
||||||
setterNull( array1, null );
|
setterNull( array[i], null );
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Advice.OnMethodExit
|
@Advice.OnMethodExit
|
||||||
static void exit(@Advice.This Object self, @Advice.Argument(0) Collection<?> argument, @MappedBy String mappedBy) {
|
static void exit(@Advice.This Object self, @Advice.Argument(0) Collection<?> argument, @InverseSide boolean inverseSide) {
|
||||||
if ( argument != null && Hibernate.isPropertyInitialized( argument, mappedBy ) ) {
|
// If this is the inverse side or the new collection is already initialized,
|
||||||
|
// we must set the respective ManyToOne on the new collection elements,
|
||||||
|
// because only the owning side is responsible for persisting the state.
|
||||||
|
if ( argument != null && ( inverseSide || Hibernate.isInitialized( argument ) ) ) {
|
||||||
Object[] array = argument.toArray();
|
Object[] array = argument.toArray();
|
||||||
for ( Object array1 : array ) {
|
for ( int i = 0; i < array.length; i++ ) {
|
||||||
if ( Hibernate.isPropertyInitialized( array1, mappedBy ) && getter( array1 ) != self ) {
|
if ( ( inverseSide || Hibernate.isInitialized( array[i] ) ) && getter( array[i] ) != self ) {
|
||||||
setterSelf( array1, self );
|
setterSelf( array[i], self );
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -415,6 +435,11 @@ class CodeTemplates {
|
||||||
throw new AssertionError();
|
throw new AssertionError();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static Object getterSelf() {
|
||||||
|
// is replaced by the actual method call
|
||||||
|
throw new AssertionError();
|
||||||
|
}
|
||||||
|
|
||||||
static void setterNull(Object target, Object argument) {
|
static void setterNull(Object target, Object argument) {
|
||||||
// is replaced by the actual method call
|
// is replaced by the actual method call
|
||||||
throw new AssertionError();
|
throw new AssertionError();
|
||||||
|
@ -428,24 +453,31 @@ class CodeTemplates {
|
||||||
|
|
||||||
static class OneToManyOnMapHandler {
|
static class OneToManyOnMapHandler {
|
||||||
@Advice.OnMethodEnter
|
@Advice.OnMethodEnter
|
||||||
static void enter(@FieldValue Map<?, ?> field, @Advice.Argument(0) Map<?, ?> argument, @MappedBy String mappedBy) {
|
static void enter(@FieldValue Map<?, ?> field, @Advice.Argument(0) Map<?, ?> argument, @InverseSide boolean inverseSide) {
|
||||||
if ( field != null && Hibernate.isPropertyInitialized( field, mappedBy ) ) {
|
// If this is the inverse side or the old collection is already initialized,
|
||||||
|
// we must unset the respective ManyToOne of the old collection elements,
|
||||||
|
// because only the owning side is responsible for persisting the state.
|
||||||
|
if ( ( inverseSide || Hibernate.isInitialized( field ) ) && getterSelf() != null ) {
|
||||||
Object[] array = field.values().toArray();
|
Object[] array = field.values().toArray();
|
||||||
for ( Object array1 : array ) {
|
for ( int i = 0; i < array.length; i++ ) {
|
||||||
if ( argument == null || !argument.values().contains( array1 ) ) {
|
if ( ( inverseSide || Hibernate.isInitialized( array[i] ) )
|
||||||
setterNull( array1, null );
|
&& ( argument == null || !argument.containsValue( array[i] ) ) ) {
|
||||||
|
setterNull( array[i], null );
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Advice.OnMethodExit
|
@Advice.OnMethodExit
|
||||||
static void exit(@Advice.This Object self, @Advice.Argument(0) Map<?, ?> argument, @MappedBy String mappedBy) {
|
static void exit(@Advice.This Object self, @Advice.Argument(0) Map<?, ?> argument, @InverseSide boolean inverseSide) {
|
||||||
if ( argument != null && Hibernate.isPropertyInitialized( argument, mappedBy ) ) {
|
// If this is the inverse side or the new collection is already initialized,
|
||||||
|
// we must set the respective ManyToOne on the new collection elements,
|
||||||
|
// because only the owning side is responsible for persisting the state.
|
||||||
|
if ( argument != null && ( inverseSide || Hibernate.isInitialized( argument ) ) ) {
|
||||||
Object[] array = argument.values().toArray();
|
Object[] array = argument.values().toArray();
|
||||||
for ( Object array1 : array ) {
|
for ( int i = 0; i < array.length; i++ ) {
|
||||||
if ( Hibernate.isPropertyInitialized( array1, mappedBy ) && getter( array1 ) != self ) {
|
if ( ( inverseSide || Hibernate.isInitialized( array[i] ) ) && getter( array[i] ) != self ) {
|
||||||
setterSelf( array1, self );
|
setterSelf( array[i], self );
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -456,6 +488,11 @@ class CodeTemplates {
|
||||||
throw new AssertionError();
|
throw new AssertionError();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static Object getterSelf() {
|
||||||
|
// is replaced by the actual method call
|
||||||
|
throw new AssertionError();
|
||||||
|
}
|
||||||
|
|
||||||
static void setterNull(Object target, Object argument) {
|
static void setterNull(Object target, Object argument) {
|
||||||
// is replaced with the actual setter call during instrumentation.
|
// is replaced with the actual setter call during instrumentation.
|
||||||
throw new AssertionError();
|
throw new AssertionError();
|
||||||
|
@ -469,8 +506,9 @@ class CodeTemplates {
|
||||||
|
|
||||||
static class ManyToOneHandler {
|
static class ManyToOneHandler {
|
||||||
@Advice.OnMethodEnter
|
@Advice.OnMethodEnter
|
||||||
static void enter(@Advice.This Object self, @FieldValue Object field, @MappedBy String mappedBy) {
|
static void enter(@Advice.This Object self, @FieldValue Object field, @BidirectionalAttribute String inverseAttribute) {
|
||||||
if ( field != null && Hibernate.isPropertyInitialized( field, mappedBy ) ) {
|
// This is always the owning side, so we only need to update the inverse side if the collection is initialized
|
||||||
|
if ( getterSelf() != null && Hibernate.isPropertyInitialized( field, inverseAttribute ) ) {
|
||||||
Collection<?> c = getter( field );
|
Collection<?> c = getter( field );
|
||||||
if ( c != null ) {
|
if ( c != null ) {
|
||||||
c.remove( self );
|
c.remove( self );
|
||||||
|
@ -479,8 +517,9 @@ class CodeTemplates {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Advice.OnMethodExit
|
@Advice.OnMethodExit
|
||||||
static void exit(@Advice.This Object self, @Advice.Argument(0) Object argument, @MappedBy String mappedBy) {
|
static void exit(@Advice.This Object self, @Advice.Argument(0) Object argument, @BidirectionalAttribute String inverseAttribute) {
|
||||||
if ( argument != null && Hibernate.isPropertyInitialized( argument, mappedBy ) ) {
|
// This is always the owning side, so we only need to update the inverse side if the collection is initialized
|
||||||
|
if ( argument != null && Hibernate.isPropertyInitialized( argument, inverseAttribute ) ) {
|
||||||
Collection<Object> c = getter( argument );
|
Collection<Object> c = getter( argument );
|
||||||
if ( c != null && !c.contains( self ) ) {
|
if ( c != null && !c.contains( self ) ) {
|
||||||
c.add( self );
|
c.add( self );
|
||||||
|
@ -492,29 +531,41 @@ class CodeTemplates {
|
||||||
// is replaced by the actual method call
|
// is replaced by the actual method call
|
||||||
throw new AssertionError();
|
throw new AssertionError();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static Object getterSelf() {
|
||||||
|
// is replaced by the actual method call
|
||||||
|
throw new AssertionError();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static class ManyToManyHandler {
|
static class ManyToManyHandler {
|
||||||
@Advice.OnMethodEnter
|
@Advice.OnMethodEnter
|
||||||
static void enter(@Advice.This Object self, @FieldValue Collection<?> field, @Advice.Argument(0) Collection<?> argument, @MappedBy String mappedBy) {
|
static void enter(@Advice.This Object self, @FieldValue Collection<?> field, @Advice.Argument(0) Collection<?> argument, @InverseSide boolean inverseSide, @BidirectionalAttribute String bidirectionalAttribute) {
|
||||||
if ( field != null && Hibernate.isPropertyInitialized( field, mappedBy ) ) {
|
// If this is the inverse side or the old collection is already initialized,
|
||||||
|
// we must remove self from the respective old collection elements inverse collections,
|
||||||
|
// because only the owning side is responsible for persisting the state.
|
||||||
|
if ( ( inverseSide || Hibernate.isInitialized( argument ) ) && getterSelf() != null ) {
|
||||||
Object[] array = field.toArray();
|
Object[] array = field.toArray();
|
||||||
for ( Object array1 : array ) {
|
for ( int i = 0; i < array.length; i++ ) {
|
||||||
if ( argument == null || !argument.contains( array1 ) ) {
|
if ( ( inverseSide || Hibernate.isPropertyInitialized( array[i], bidirectionalAttribute ) )
|
||||||
getter( array1 ).remove( self );
|
&& ( argument == null || !argument.contains( array[i] ) ) ) {
|
||||||
|
getter( array[i] ).remove( self );
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Advice.OnMethodExit
|
@Advice.OnMethodExit
|
||||||
static void exit(@Advice.This Object self, @Advice.Argument(0) Collection<?> argument, @MappedBy String mappedBy) {
|
static void exit(@Advice.This Object self, @Advice.Argument(0) Collection<?> argument, @InverseSide boolean inverseSide, @BidirectionalAttribute String bidirectionalAttribute) {
|
||||||
if ( argument != null && Hibernate.isPropertyInitialized( argument, mappedBy ) ) {
|
// If this is the inverse side or the new collection is already initialized,
|
||||||
|
// we must add self to the respective new collection elements inverse collections,
|
||||||
|
// because only the owning side is responsible for persisting the state.
|
||||||
|
if ( argument != null && ( inverseSide || Hibernate.isInitialized( argument ) ) ) {
|
||||||
Object[] array = argument.toArray();
|
Object[] array = argument.toArray();
|
||||||
for ( Object array1 : array ) {
|
for ( Object array1 : array ) {
|
||||||
if ( Hibernate.isPropertyInitialized( array1, mappedBy ) ) {
|
if ( inverseSide || Hibernate.isPropertyInitialized( array1, bidirectionalAttribute ) ) {
|
||||||
Collection<Object> c = getter( array1 );
|
Collection<Object> c = getter( array1 );
|
||||||
if ( c != self && c != null ) {
|
if ( c != null && !c.contains( self ) ) {
|
||||||
c.add( self );
|
c.add( self );
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -526,6 +577,11 @@ class CodeTemplates {
|
||||||
// is replaced by the actual method call
|
// is replaced by the actual method call
|
||||||
throw new AssertionError();
|
throw new AssertionError();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static Object getterSelf() {
|
||||||
|
// is replaced by the actual method call
|
||||||
|
throw new AssertionError();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Retention(RetentionPolicy.RUNTIME)
|
@Retention(RetentionPolicy.RUNTIME)
|
||||||
|
@ -539,7 +595,12 @@ class CodeTemplates {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Retention(RetentionPolicy.RUNTIME)
|
@Retention(RetentionPolicy.RUNTIME)
|
||||||
@interface MappedBy {
|
@interface InverseSide {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Retention(RetentionPolicy.RUNTIME)
|
||||||
|
@interface BidirectionalAttribute {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,92 @@
|
||||||
|
/*
|
||||||
|
* 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.bytecode.enhancement.association;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
import jakarta.persistence.Column;
|
||||||
|
import jakarta.persistence.Entity;
|
||||||
|
import jakarta.persistence.Id;
|
||||||
|
import jakarta.persistence.ManyToMany;
|
||||||
|
|
||||||
|
import org.hibernate.testing.bytecode.enhancement.BytecodeEnhancerRunner;
|
||||||
|
import org.junit.Assert;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.junit.runner.RunWith;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author Luis Barreiro
|
||||||
|
*/
|
||||||
|
@RunWith( BytecodeEnhancerRunner.class )
|
||||||
|
public class ManyToManyAssociationListTest {
|
||||||
|
@Test
|
||||||
|
public void testBidirectionalExisting() {
|
||||||
|
Group group = new Group();
|
||||||
|
Group anotherGroup = new Group();
|
||||||
|
|
||||||
|
User user = new User();
|
||||||
|
anotherGroup.users.add( user );
|
||||||
|
|
||||||
|
user.setGroups( new ArrayList<>( Collections.singleton( group ) ) );
|
||||||
|
user.setGroups( new ArrayList<>( Arrays.asList( group, anotherGroup ) ) );
|
||||||
|
|
||||||
|
Assert.assertEquals( 1, group.getUsers().size() );
|
||||||
|
Assert.assertEquals( 1, anotherGroup.getUsers().size() );
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- //
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
private static class Group {
|
||||||
|
|
||||||
|
@Id
|
||||||
|
Long id;
|
||||||
|
|
||||||
|
@Column
|
||||||
|
String name;
|
||||||
|
|
||||||
|
@ManyToMany( mappedBy = "groups" )
|
||||||
|
List<User> users = new ArrayList<>();
|
||||||
|
|
||||||
|
List<User> getUsers() {
|
||||||
|
return Collections.unmodifiableList( users );
|
||||||
|
}
|
||||||
|
|
||||||
|
void resetUsers() {
|
||||||
|
// this wouldn't trigger association management: users.clear();
|
||||||
|
users = new ArrayList<>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
private static class User {
|
||||||
|
|
||||||
|
@Id
|
||||||
|
Long id;
|
||||||
|
|
||||||
|
String password;
|
||||||
|
|
||||||
|
@ManyToMany
|
||||||
|
List<Group> groups;
|
||||||
|
|
||||||
|
void addGroup(Group group) {
|
||||||
|
List<Group> groups = this.groups == null ? new ArrayList<>() : this.groups;
|
||||||
|
groups.add( group );
|
||||||
|
this.groups = groups;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Group> getGroups() {
|
||||||
|
return Collections.unmodifiableList( groups );
|
||||||
|
}
|
||||||
|
|
||||||
|
void setGroups(List<Group> groups) {
|
||||||
|
this.groups = groups;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -49,6 +49,25 @@ public class OneToOneAssociationTest {
|
||||||
Assert.assertEquals( user, user.getCustomer().getUser() );
|
Assert.assertEquals( user, user.getCustomer().getUser() );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSetNull() {
|
||||||
|
User user = new User();
|
||||||
|
user.setLogin( UUID.randomUUID().toString() );
|
||||||
|
|
||||||
|
Customer customer = new Customer();
|
||||||
|
customer.setUser( user );
|
||||||
|
|
||||||
|
Assert.assertEquals( customer, user.getCustomer() );
|
||||||
|
|
||||||
|
// check dirty tracking is set automatically with bi-directional association management
|
||||||
|
EnhancerTestUtils.checkDirtyTracking( user, "login", "customer" );
|
||||||
|
|
||||||
|
user.setCustomer( null );
|
||||||
|
|
||||||
|
Assert.assertNull( user.getCustomer() );
|
||||||
|
Assert.assertNull( customer.getUser() );
|
||||||
|
}
|
||||||
|
|
||||||
// --- //
|
// --- //
|
||||||
|
|
||||||
@Entity
|
@Entity
|
||||||
|
|
Loading…
Reference in New Issue