HHH-15602 Fix bidirectional association management code

This commit is contained in:
Christian Beikov 2023-04-17 15:33:49 +02:00
parent 5303295c31
commit 4a55422187
4 changed files with 268 additions and 62 deletions

View File

@ -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,13 +204,13 @@ 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;
} }
} }
@ -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 );
} }
} }

View File

@ -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 {
} }

View File

@ -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;
}
}
}

View File

@ -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