Skip to content

Commit 5c13740

Browse files
committed
HSEARCH-5170 Ensure that setting an association to null before removing an entity triggers indexing
1 parent 1f3e4cc commit 5c13740

File tree

2 files changed

+125
-0
lines changed

2 files changed

+125
-0
lines changed

integrationtest/mapper/orm/src/test/java/org/hibernate/search/integrationtest/mapper/orm/automaticindexing/association/bytype/AbstractAutomaticIndexingAssociationBaseIT.java

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1161,6 +1161,59 @@ void directImplicitAssociationUpdateThroughDeleteWithAlreadyLoadedAssociation_in
11611161
backendMock.verifyExpectationsMet();
11621162
}
11631163

1164+
/**
1165+
* This test attempts to remove the entity after both sides of the association were set to null,
1166+
* and we expect that even after such steps we can identify what entities have to be re-indexed.
1167+
*/
1168+
@Test
1169+
@TestForIssue(jiraKey = "HSEARCH-5170")
1170+
void directAssociationSetNullThenRemoved_indexedEmbedded() {
1171+
assumeTrue(
1172+
!( isAssociationMultiValuedOnContainedSide() || isAssociationMultiValuedOnContainingSide() ),
1173+
"This test only makes sense if there's a one-to-one association."
1174+
);
1175+
PropertyAccessor<TContaining, TContained> containingAssociation = _containing().containedIndexedEmbedded();
1176+
PropertyAccessor<TContained, TContaining> containedAssociation = _contained().containingAsIndexedEmbedded();
1177+
PropertyAccessor<TContained, String> field = _contained().indexedField();
1178+
1179+
with( sessionFactory ).runInTransaction( session -> {
1180+
TIndexed entity1 = _indexed().newInstance( 1 );
1181+
TContained containedEntity = _contained().newInstance( 2 );
1182+
field.set( containedEntity, "initialValue" );
1183+
1184+
session.persist( entity1 );
1185+
session.persist( containedEntity );
1186+
1187+
containingAssociation.set( entity1, containedEntity );
1188+
containedAssociation.set( containedEntity, entity1 );
1189+
1190+
backendMock.expectWorks( _indexed().indexName() )
1191+
.add( "1", b -> b
1192+
.objectField( "containedIndexedEmbedded", b2 -> b2
1193+
.field( "indexedField", "initialValue" )
1194+
) );
1195+
} );
1196+
backendMock.verifyExpectationsMet();
1197+
1198+
with( sessionFactory ).runInTransaction( session -> {
1199+
TIndexed entity1 = session.get( _indexed().entityClass(), 1 );
1200+
// Make sure we initialize the association from indexed to contained;
1201+
// the magic is in the fact that Hibernate doesn't index the contained entity
1202+
// even though it's referenced by the Java representation of the association.
1203+
TContained containedEntity = containingAssociation.get( entity1 );
1204+
1205+
// We update both sides of the association which would mean that
1206+
// the deleted entity has no information to which indexed entity it was associated to.
1207+
containingAssociation.set( entity1, null );
1208+
containedAssociation.set( containedEntity, null );
1209+
session.remove( containedEntity );
1210+
1211+
backendMock.expectWorks( _indexed().indexName() )
1212+
.addOrUpdate( "1", b -> {} );
1213+
} );
1214+
backendMock.verifyExpectationsMet();
1215+
}
1216+
11641217
@Test
11651218
@TestForIssue(jiraKey = "HSEARCH-4708")
11661219
void directEmbeddedAssociationReplace_embeddedAssociationsIndexedEmbedded() {
@@ -3775,6 +3828,66 @@ void indirectImplicitAssociationUpdateThroughDeleteWithAlreadyLoadedAssociation_
37753828
backendMock.verifyExpectationsMet();
37763829
}
37773830

3831+
/**
3832+
* Same as {@link #directAssociationSetNullThenRemoved_indexedEmbedded()} ,
3833+
* but with an additional association: indexedEntity -> containingEntity -> containedEntity.
3834+
*/
3835+
@Test
3836+
@TestForIssue(jiraKey = "HSEARCH-5170")
3837+
void indirectAssociationSetNullThenRemoved_indexedEmbedded() {
3838+
assumeTrue(
3839+
!( isAssociationMultiValuedOnContainedSide() || isAssociationMultiValuedOnContainingSide() ),
3840+
"This test only makes sense if there's a one-to-one association."
3841+
);
3842+
3843+
PropertyAccessor<TContaining, TContained> containingAssociation = _containing().containedIndexedEmbedded();
3844+
PropertyAccessor<TContained, TContaining> containedAssociation = _contained().containingAsIndexedEmbedded();
3845+
PropertyAccessor<TContained, String> field = _contained().indexedField();
3846+
3847+
with( sessionFactory ).runInTransaction( session -> {
3848+
TIndexed entity1 = _indexed().newInstance( 1 );
3849+
TContaining containingEntity1 = _containing().newInstance( 2 );
3850+
_containing().child().set( entity1, containingEntity1 );
3851+
_containing().parent().set( containingEntity1, entity1 );
3852+
TContained containedEntity = _contained().newInstance( 2 );
3853+
field.set( containedEntity, "initialValue" );
3854+
3855+
session.persist( containingEntity1 );
3856+
session.persist( entity1 );
3857+
session.persist( containedEntity );
3858+
3859+
containingAssociation.set( containingEntity1, containedEntity );
3860+
containedAssociation.set( containedEntity, containingEntity1 );
3861+
3862+
backendMock.expectWorks( _indexed().indexName() )
3863+
.add( "1", b -> b
3864+
.objectField( "child", b2 -> b2
3865+
.objectField( "containedIndexedEmbedded", b3 -> b3
3866+
.field( "indexedField", "initialValue" )
3867+
) ) );
3868+
} );
3869+
backendMock.verifyExpectationsMet();
3870+
3871+
with( sessionFactory ).runInTransaction( session -> {
3872+
TContaining containing = session.get( _containing().entityClass(), 2 );
3873+
// Make sure we initialize the association from containing to contained;
3874+
// the magic is in the fact that Hibernate doesn't index the contained entity
3875+
// even though it's referenced by the Java representation of the association.
3876+
TContained containedEntity = containingAssociation.get( containing );
3877+
3878+
// Do *update* the association on both side and set them to null; that's on purpose.
3879+
containedAssociation.set( containedEntity, null );
3880+
containingAssociation.set( containing, null );
3881+
3882+
session.remove( containedEntity );
3883+
3884+
backendMock.expectWorks( _indexed().indexName() )
3885+
.addOrUpdate( "1", b -> b
3886+
.objectField( "child", b2 -> {} ) );
3887+
} );
3888+
backendMock.verifyExpectationsMet();
3889+
}
3890+
37783891
@Test
37793892
void indirectValueUpdate_compose_singleValue_indexed() {
37803893
PropertyAccessor<TContaining, TContained> containingAssociation = _containing().containedIndexedEmbedded();

mapper/orm/src/main/java/org/hibernate/search/mapper/orm/event/impl/HibernateSearchEventListener.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,18 @@ public void onPostDelete(PostDeleteEvent event) {
106106

107107
Object providedId = typeContext.toIndexingPlanProvidedId( event.getId() );
108108
plan.delete( providedId, null, entity );
109+
110+
// In case ToOne associations are updated and set to null after which the entity is removed,
111+
// we may end up with the `Object entity` from which we cannot derive what other entities are affected
112+
// and have to be re-indexed.
113+
//
114+
// Hence, we will look at the state of the deleted entity when it was loaded
115+
// and derive the required information from it:
116+
BitSet dirtyAssociationPaths = typeContext.dirtyContainingAssociationFilter().all();
117+
118+
if ( dirtyAssociationPaths != null ) {
119+
plan.updateAssociationInverseSide( dirtyAssociationPaths, null, event.getDeletedState() );
120+
}
109121
}
110122

111123
@Override

0 commit comments

Comments
 (0)