diff --git a/hibernate-core/src/main/java/org/hibernate/bytecode/enhance/internal/bytebuddy/BiDirectionalAssociationHandler.java b/hibernate-core/src/main/java/org/hibernate/bytecode/enhance/internal/bytebuddy/BiDirectionalAssociationHandler.java index 7579386afa..1cb4542d67 100644 --- a/hibernate-core/src/main/java/org/hibernate/bytecode/enhance/internal/bytebuddy/BiDirectionalAssociationHandler.java +++ b/hibernate-core/src/main/java/org/hibernate/bytecode/enhance/internal/bytebuddy/BiDirectionalAssociationHandler.java @@ -35,6 +35,7 @@ import net.bytebuddy.dynamic.scaffold.FieldLocator; import net.bytebuddy.dynamic.scaffold.InstrumentedType; import net.bytebuddy.implementation.Implementation; import net.bytebuddy.implementation.bytecode.ByteCodeAppender; +import net.bytebuddy.implementation.bytecode.assign.Assigner; import net.bytebuddy.jar.asm.MethodVisitor; import net.bytebuddy.jar.asm.Opcodes; import net.bytebuddy.jar.asm.Type; @@ -57,7 +58,14 @@ final class BiDirectionalAssociationHandler implements Implementation { return implementation; } 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() ) { log.infof( "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 ) - .locate( mappedBy ) + .locate( bidirectionalAttributeName ) .getField() .getType() .asErasure(); if ( persistentField.hasAnnotation( OneToOne.class ) ) { implementation = Advice.withCustomMapping() - .bind( CodeTemplates.FieldValue.class, persistentField.getFieldDescription() ) - .bind( CodeTemplates.MappedBy.class, mappedBy ) + .bind( + // 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 ) .wrap( implementation ); } @@ -86,7 +103,7 @@ final class BiDirectionalAssociationHandler implements Implementation { if ( persistentField.hasAnnotation( OneToMany.class ) ) { implementation = Advice.withCustomMapping() .bind( CodeTemplates.FieldValue.class, persistentField.getFieldDescription() ) - .bind( CodeTemplates.MappedBy.class, mappedBy ) + .bind( CodeTemplates.InverseSide.class, mappedBy != null ) .to( persistentField.getType().asErasure().isAssignableTo( Map.class ) ? CodeTemplates.OneToManyOnMapHandler.class : CodeTemplates.OneToManyOnCollectionHandler.class ) @@ -96,7 +113,7 @@ final class BiDirectionalAssociationHandler implements Implementation { if ( persistentField.hasAnnotation( ManyToOne.class ) ) { implementation = Advice.withCustomMapping() .bind( CodeTemplates.FieldValue.class, persistentField.getFieldDescription() ) - .bind( CodeTemplates.MappedBy.class, mappedBy ) + .bind( CodeTemplates.BidirectionalAttribute.class, bidirectionalAttributeName ) .to( CodeTemplates.ManyToOneHandler.class ) .wrap( implementation ); } @@ -116,12 +133,13 @@ final class BiDirectionalAssociationHandler implements Implementation { implementation = Advice.withCustomMapping() .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 ) .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) { @@ -186,16 +204,16 @@ final class BiDirectionalAssociationHandler implements Implementation { } private static String getMappedBy(AnnotatedFieldDescription target, TypeDescription targetEntity, ByteBuddyEnhancementContext context) { - String mappedBy = getMappedByNotManyToMany( target ); + final String mappedBy = getMappedByFromAnnotation( target ); if ( mappedBy == null || mappedBy.isEmpty() ) { - return getMappedByManyToMany( target, targetEntity, context ); + return null; } else { // 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) { try { FieldDescription f = FieldLocator.ForClassHierarchy.Factory.INSTANCE.make( targetEntity ).locate( mappedBy ).getField(); @@ -207,8 +225,7 @@ final class BiDirectionalAssociationHandler implements Implementation { return false; } } - - private static String getMappedByNotManyToMany(AnnotatedFieldDescription target) { + private static String getMappedByFromAnnotation(AnnotatedFieldDescription target) { try { AnnotationDescription.Loadable oto = target.getAnnotation( OneToOne.class ); if ( oto != null ) { @@ -235,7 +252,7 @@ final class BiDirectionalAssociationHandler implements Implementation { for ( FieldDescription f : targetEntity.getDeclaredFields() ) { AnnotatedFieldDescription annotatedF = new AnnotatedFieldDescription( context, f ); 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() ) ) ) { if ( log.isDebugEnabled() ) { log.debugf( @@ -267,21 +284,28 @@ final class BiDirectionalAssociationHandler implements Implementation { private final Implementation delegate; + private final TypeDescription entity; + private final AnnotatedFieldDescription field; + private final TypeDescription targetEntity; private final TypeDescription targetType; - private final String mappedBy; + private final String bidirectionalAttributeName; private BiDirectionalAssociationHandler( Implementation delegate, + TypeDescription entity, + AnnotatedFieldDescription field, TypeDescription targetEntity, TypeDescription targetType, - String mappedBy) { + String bidirectionalAttributeName) { this.delegate = delegate; + this.entity = entity; + this.field = field; this.targetEntity = targetEntity; this.targetType = targetType; - this.mappedBy = mappedBy; + this.bidirectionalAttributeName = bidirectionalAttributeName; } @Override @@ -315,11 +339,21 @@ final class BiDirectionalAssociationHandler implements Implementation { super.visitMethodInsn( Opcodes.INVOKEVIRTUAL, targetEntity.getInternalName(), - EnhancerConstants.PERSISTENT_FIELD_READER_PREFIX + mappedBy, + EnhancerConstants.PERSISTENT_FIELD_READER_PREFIX + bidirectionalAttributeName, Type.getMethodDescriptor( Type.getType( targetType.getDescriptor() ) ), 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" ) ) { super.visitInsn( Opcodes.POP ); super.visitTypeInsn( Opcodes.CHECKCAST, targetEntity.getInternalName() ); @@ -327,7 +361,7 @@ final class BiDirectionalAssociationHandler implements Implementation { super.visitMethodInsn( Opcodes.INVOKEVIRTUAL, targetEntity.getInternalName(), - EnhancerConstants.PERSISTENT_FIELD_WRITER_PREFIX + mappedBy, + EnhancerConstants.PERSISTENT_FIELD_WRITER_PREFIX + bidirectionalAttributeName, Type.getMethodDescriptor( Type.getType( void.class ), Type.getType( targetType.getDescriptor() ) ), false ); @@ -339,7 +373,7 @@ final class BiDirectionalAssociationHandler implements Implementation { super.visitMethodInsn( Opcodes.INVOKEVIRTUAL, targetEntity.getInternalName(), - EnhancerConstants.PERSISTENT_FIELD_WRITER_PREFIX + mappedBy, + EnhancerConstants.PERSISTENT_FIELD_WRITER_PREFIX + bidirectionalAttributeName, Type.getMethodDescriptor( Type.getType( void.class ), Type.getType( targetType.getDescriptor() ) ), false ); @@ -368,11 +402,11 @@ final class BiDirectionalAssociationHandler implements Implementation { return Objects.equals( delegate, that.delegate ) && Objects.equals( targetEntity, that.targetEntity ) && Objects.equals( targetType, that.targetType ) && - Objects.equals( mappedBy, that.mappedBy ); + Objects.equals( bidirectionalAttributeName, that.bidirectionalAttributeName ); } @Override public int hashCode() { - return Objects.hash( delegate, targetEntity, targetType, mappedBy ); + return Objects.hash( delegate, targetEntity, targetType, bidirectionalAttributeName ); } } diff --git a/hibernate-core/src/main/java/org/hibernate/bytecode/enhance/internal/bytebuddy/CodeTemplates.java b/hibernate-core/src/main/java/org/hibernate/bytecode/enhance/internal/bytebuddy/CodeTemplates.java index cddc79f1cc..52ca538e95 100644 --- a/hibernate-core/src/main/java/org/hibernate/bytecode/enhance/internal/bytebuddy/CodeTemplates.java +++ b/hibernate-core/src/main/java/org/hibernate/bytecode/enhance/internal/bytebuddy/CodeTemplates.java @@ -356,15 +356,24 @@ class CodeTemplates { static class OneToOneHandler { @Advice.OnMethodEnter - static void enter(@FieldValue Object field, @Advice.Argument(0) Object argument, @MappedBy String mappedBy) { - if ( field != null && Hibernate.isPropertyInitialized( field, mappedBy ) && argument != null ) { - setterNull( field, null ); + static void enter(@FieldValue Object field, @Advice.Argument(0) Object argument, @InverseSide boolean inverseSide) { + // Unset the inverse attribute, which possibly initializes the old value, + // 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 - static void exit(@Advice.This Object self, @Advice.Argument(0) Object argument, @MappedBy String mappedBy) { - if ( argument != null && Hibernate.isPropertyInitialized( argument, mappedBy ) && getter( argument ) != self ) { + static void exit(@Advice.This Object self, @Advice.Argument(0) Object argument, @InverseSide boolean inverseSide) { + // 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 ); } } @@ -374,6 +383,11 @@ class CodeTemplates { throw new AssertionError(); } + static Object getterSelf() { + // is replaced by the actual method call + throw new AssertionError(); + } + static void setterNull(Object target, Object argument) { // is replaced by the actual method call throw new AssertionError(); @@ -387,24 +401,30 @@ class CodeTemplates { static class OneToManyOnCollectionHandler { @Advice.OnMethodEnter - static void enter(@FieldValue Collection field, @Advice.Argument(0) Collection argument, @MappedBy String mappedBy) { - if ( field != null && Hibernate.isPropertyInitialized( field, mappedBy ) ) { + static void enter(@FieldValue Collection field, @Advice.Argument(0) Collection argument, @InverseSide boolean inverseSide) { + // 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(); - for ( Object array1 : array ) { - if ( argument == null || !argument.contains( array1 ) ) { - setterNull( array1, null ); + for ( int i = 0; i < array.length; i++ ) { + if ( ( inverseSide || Hibernate.isInitialized( array[i] ) ) && ( argument == null || !argument.contains( array[i] ) ) ) { + setterNull( array[i], null ); } } } } @Advice.OnMethodExit - static void exit(@Advice.This Object self, @Advice.Argument(0) Collection argument, @MappedBy String mappedBy) { - if ( argument != null && Hibernate.isPropertyInitialized( argument, mappedBy ) ) { + static void exit(@Advice.This Object self, @Advice.Argument(0) Collection argument, @InverseSide boolean inverseSide) { + // 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(); - for ( Object array1 : array ) { - if ( Hibernate.isPropertyInitialized( array1, mappedBy ) && getter( array1 ) != self ) { - setterSelf( array1, self ); + for ( int i = 0; i < array.length; i++ ) { + if ( ( inverseSide || Hibernate.isInitialized( array[i] ) ) && getter( array[i] ) != self ) { + setterSelf( array[i], self ); } } } @@ -415,6 +435,11 @@ class CodeTemplates { throw new AssertionError(); } + static Object getterSelf() { + // is replaced by the actual method call + throw new AssertionError(); + } + static void setterNull(Object target, Object argument) { // is replaced by the actual method call throw new AssertionError(); @@ -428,24 +453,31 @@ class CodeTemplates { static class OneToManyOnMapHandler { @Advice.OnMethodEnter - static void enter(@FieldValue Map field, @Advice.Argument(0) Map argument, @MappedBy String mappedBy) { - if ( field != null && Hibernate.isPropertyInitialized( field, mappedBy ) ) { + static void enter(@FieldValue Map field, @Advice.Argument(0) Map argument, @InverseSide boolean inverseSide) { + // 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(); - for ( Object array1 : array ) { - if ( argument == null || !argument.values().contains( array1 ) ) { - setterNull( array1, null ); + for ( int i = 0; i < array.length; i++ ) { + if ( ( inverseSide || Hibernate.isInitialized( array[i] ) ) + && ( argument == null || !argument.containsValue( array[i] ) ) ) { + setterNull( array[i], null ); } } } } @Advice.OnMethodExit - static void exit(@Advice.This Object self, @Advice.Argument(0) Map argument, @MappedBy String mappedBy) { - if ( argument != null && Hibernate.isPropertyInitialized( argument, mappedBy ) ) { + static void exit(@Advice.This Object self, @Advice.Argument(0) Map argument, @InverseSide boolean inverseSide) { + // 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(); - for ( Object array1 : array ) { - if ( Hibernate.isPropertyInitialized( array1, mappedBy ) && getter( array1 ) != self ) { - setterSelf( array1, self ); + for ( int i = 0; i < array.length; i++ ) { + if ( ( inverseSide || Hibernate.isInitialized( array[i] ) ) && getter( array[i] ) != self ) { + setterSelf( array[i], self ); } } } @@ -456,6 +488,11 @@ class CodeTemplates { throw new AssertionError(); } + static Object getterSelf() { + // is replaced by the actual method call + throw new AssertionError(); + } + static void setterNull(Object target, Object argument) { // is replaced with the actual setter call during instrumentation. throw new AssertionError(); @@ -469,8 +506,9 @@ class CodeTemplates { static class ManyToOneHandler { @Advice.OnMethodEnter - static void enter(@Advice.This Object self, @FieldValue Object field, @MappedBy String mappedBy) { - if ( field != null && Hibernate.isPropertyInitialized( field, mappedBy ) ) { + static void enter(@Advice.This Object self, @FieldValue Object field, @BidirectionalAttribute String inverseAttribute) { + // 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 ); if ( c != null ) { c.remove( self ); @@ -479,8 +517,9 @@ class CodeTemplates { } @Advice.OnMethodExit - static void exit(@Advice.This Object self, @Advice.Argument(0) Object argument, @MappedBy String mappedBy) { - if ( argument != null && Hibernate.isPropertyInitialized( argument, mappedBy ) ) { + static void exit(@Advice.This Object self, @Advice.Argument(0) Object argument, @BidirectionalAttribute String inverseAttribute) { + // 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 c = getter( argument ); if ( c != null && !c.contains( self ) ) { c.add( self ); @@ -492,29 +531,41 @@ class CodeTemplates { // is replaced by the actual method call throw new AssertionError(); } + + static Object getterSelf() { + // is replaced by the actual method call + throw new AssertionError(); + } } static class ManyToManyHandler { @Advice.OnMethodEnter - static void enter(@Advice.This Object self, @FieldValue Collection field, @Advice.Argument(0) Collection argument, @MappedBy String mappedBy) { - if ( field != null && Hibernate.isPropertyInitialized( field, mappedBy ) ) { + static void enter(@Advice.This Object self, @FieldValue Collection field, @Advice.Argument(0) Collection argument, @InverseSide boolean inverseSide, @BidirectionalAttribute String bidirectionalAttribute) { + // 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(); - for ( Object array1 : array ) { - if ( argument == null || !argument.contains( array1 ) ) { - getter( array1 ).remove( self ); + for ( int i = 0; i < array.length; i++ ) { + if ( ( inverseSide || Hibernate.isPropertyInitialized( array[i], bidirectionalAttribute ) ) + && ( argument == null || !argument.contains( array[i] ) ) ) { + getter( array[i] ).remove( self ); } } } } @Advice.OnMethodExit - static void exit(@Advice.This Object self, @Advice.Argument(0) Collection argument, @MappedBy String mappedBy) { - if ( argument != null && Hibernate.isPropertyInitialized( argument, mappedBy ) ) { + static void exit(@Advice.This Object self, @Advice.Argument(0) Collection argument, @InverseSide boolean inverseSide, @BidirectionalAttribute String bidirectionalAttribute) { + // 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(); for ( Object array1 : array ) { - if ( Hibernate.isPropertyInitialized( array1, mappedBy ) ) { + if ( inverseSide || Hibernate.isPropertyInitialized( array1, bidirectionalAttribute ) ) { Collection c = getter( array1 ); - if ( c != self && c != null ) { + if ( c != null && !c.contains( self ) ) { c.add( self ); } } @@ -526,6 +577,11 @@ class CodeTemplates { // is replaced by the actual method call throw new AssertionError(); } + + static Object getterSelf() { + // is replaced by the actual method call + throw new AssertionError(); + } } @Retention(RetentionPolicy.RUNTIME) @@ -539,7 +595,12 @@ class CodeTemplates { } @Retention(RetentionPolicy.RUNTIME) - @interface MappedBy { + @interface InverseSide { + + } + + @Retention(RetentionPolicy.RUNTIME) + @interface BidirectionalAttribute { } diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/association/ManyToManyAssociationListTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/association/ManyToManyAssociationListTest.java new file mode 100644 index 0000000000..9b0dae57b7 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/association/ManyToManyAssociationListTest.java @@ -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 . + */ +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 users = new ArrayList<>(); + + List 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 groups; + + void addGroup(Group group) { + List groups = this.groups == null ? new ArrayList<>() : this.groups; + groups.add( group ); + this.groups = groups; + } + + List getGroups() { + return Collections.unmodifiableList( groups ); + } + + void setGroups(List groups) { + this.groups = groups; + } + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/association/OneToOneAssociationTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/association/OneToOneAssociationTest.java index 8c060bac92..2ea16f9018 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/association/OneToOneAssociationTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/association/OneToOneAssociationTest.java @@ -49,6 +49,25 @@ public class OneToOneAssociationTest { 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