HHH-15602 Fix bidirectional association management code
This commit is contained in:
parent
a5ae8737a6
commit
982b132213
|
@ -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<OneToOne> 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 );
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<Object> 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<Object> 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 {
|
||||
|
||||
}
|
||||
|
||||
|
|
|
@ -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() );
|
||||
}
|
||||
|
||||
@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
|
||||
|
|
Loading…
Reference in New Issue