Skip to content

Commit 8e84c7d

Browse files
committed
HHH-9091 : Collection deleted due to orphan removal fails with constraint violation
(cherry picked from commit f1a9a9f)
1 parent b238d7b commit 8e84c7d

File tree

14 files changed

+958
-28
lines changed

14 files changed

+958
-28
lines changed

hibernate-core/src/main/java/org/hibernate/engine/internal/StatefulPersistenceContext.java

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,7 @@ public class StatefulPersistenceContext implements PersistenceContext {
145145

146146
private int cascading;
147147
private int loadCounter;
148+
private int removeOrphanBeforeUpdatesCounter;
148149
private boolean flushing;
149150

150151
private boolean defaultReadOnly;
@@ -1029,6 +1030,42 @@ public void setFlushing(boolean flushing) {
10291030
}
10301031
}
10311032

1033+
public boolean isRemovingOrphanBeforeUpates() {
1034+
return removeOrphanBeforeUpdatesCounter > 0;
1035+
}
1036+
1037+
public void beginRemoveOrphanBeforeUpdates() {
1038+
if ( getCascadeLevel() < 1 ) {
1039+
throw new IllegalStateException( "Attempt to remove orphan when not cascading." );
1040+
}
1041+
if ( removeOrphanBeforeUpdatesCounter >= getCascadeLevel() ) {
1042+
throw new IllegalStateException(
1043+
String.format(
1044+
"Cascade level [%d] is out of sync with removeOrphanBeforeUpdatesCounter [%d] before incrementing removeOrphanBeforeUpdatesCounter",
1045+
getCascadeLevel(),
1046+
removeOrphanBeforeUpdatesCounter
1047+
)
1048+
);
1049+
}
1050+
removeOrphanBeforeUpdatesCounter++;
1051+
}
1052+
1053+
public void endRemoveOrphanBeforeUpdates() {
1054+
if ( getCascadeLevel() < 1 ) {
1055+
throw new IllegalStateException( "Finished removing orphan when not cascading." );
1056+
}
1057+
if ( removeOrphanBeforeUpdatesCounter > getCascadeLevel() ) {
1058+
throw new IllegalStateException(
1059+
String.format(
1060+
"Cascade level [%d] is out of sync with removeOrphanBeforeUpdatesCounter [%d] before decrementing removeOrphanBeforeUpdatesCounter",
1061+
getCascadeLevel(),
1062+
removeOrphanBeforeUpdatesCounter
1063+
)
1064+
);
1065+
}
1066+
removeOrphanBeforeUpdatesCounter--;
1067+
}
1068+
10321069
/**
10331070
* Call this before beginning a two-phase load
10341071
*/
@@ -1369,7 +1406,7 @@ public void replaceDelayedEntityIdentityInsertKeys(EntityKey oldKey, Serializabl
13691406
public void serialize(ObjectOutputStream oos) throws IOException {
13701407
final boolean tracing = LOG.isTraceEnabled();
13711408
if ( tracing ) {
1372-
LOG.trace( "Serializing persistence-context" );
1409+
LOG.trace( "Serializing persisatence-context" );
13731410
}
13741411

13751412
oos.writeBoolean( defaultReadOnly );

hibernate-core/src/main/java/org/hibernate/internal/SessionImpl.java

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -903,14 +903,50 @@ public void delete(String entityName, Object object) throws HibernateException {
903903

904904
@Override
905905
public void delete(String entityName, Object object, boolean isCascadeDeleteEnabled, Set transientEntities) throws HibernateException {
906-
fireDelete( new DeleteEvent( entityName, object, isCascadeDeleteEnabled, this ), transientEntities );
906+
if ( TRACE_ENABLED && persistenceContext.isRemovingOrphanBeforeUpates() ) {
907+
logRemoveOrphanBeforeUpdates( "before continuing", entityName, object );
908+
}
909+
fireDelete(
910+
new DeleteEvent(
911+
entityName,
912+
object,
913+
isCascadeDeleteEnabled,
914+
persistenceContext.isRemovingOrphanBeforeUpates(),
915+
this
916+
),
917+
transientEntities
918+
);
919+
if ( TRACE_ENABLED && persistenceContext.isRemovingOrphanBeforeUpates() ) {
920+
logRemoveOrphanBeforeUpdates( "after continuing", entityName, object );
921+
}
907922
}
908923

909924
@Override
910925
public void removeOrphanBeforeUpdates(String entityName, Object child) {
911926
// TODO: The removeOrphan concept is a temporary "hack" for HHH-6484. This should be removed once action/task
912927
// ordering is improved.
913-
fireDelete( new DeleteEvent( entityName, child, false, true, this ) );
928+
if ( TRACE_ENABLED ) {
929+
logRemoveOrphanBeforeUpdates( "begin", entityName, child );
930+
}
931+
persistenceContext.beginRemoveOrphanBeforeUpdates();
932+
try {
933+
fireDelete( new DeleteEvent( entityName, child, false, true, this ) );
934+
}
935+
finally {
936+
persistenceContext.endRemoveOrphanBeforeUpdates();
937+
if ( TRACE_ENABLED ) {
938+
logRemoveOrphanBeforeUpdates( "end", entityName, child );
939+
}
940+
}
941+
}
942+
943+
private void logRemoveOrphanBeforeUpdates(String timing, String entityName, Object entity) {
944+
final EntityEntry entityEntry = persistenceContext.getEntry( entity );
945+
LOG.tracef(
946+
"%s remove orphan before updates: [%s]",
947+
timing,
948+
entityEntry == null ? entityName : MessageHelper.infoString( entityName, entityEntry.getId() )
949+
);
914950
}
915951

916952
private void fireDelete(DeleteEvent event) {

hibernate-core/src/test/java/org/hibernate/test/orphan/one2one/fk/bidirectional/multilevelcascade/DeleteMultiLevelOrphansTest.java

Lines changed: 129 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,6 @@
2828
import org.junit.Test;
2929

3030
import org.hibernate.Session;
31-
import org.hibernate.test.orphan.one2one.fk.bidirectional.Employee;
32-
import org.hibernate.test.orphan.one2one.fk.bidirectional.EmployeeInfo;
33-
import org.hibernate.testing.FailureExpected;
3431
import org.hibernate.testing.TestForIssue;
3532
import org.hibernate.testing.junit4.BaseCoreFunctionalTestCase;
3633

@@ -46,25 +43,31 @@ public class DeleteMultiLevelOrphansTest extends BaseCoreFunctionalTestCase {
4643

4744
private void createData() {
4845
Preisregelung preisregelung = new Preisregelung();
49-
preisregelung.setId( 17960L );
5046

5147
Tranchenmodell tranchenmodell = new Tranchenmodell();
52-
tranchenmodell.setId( 1951L );
48+
49+
X x = new X();
5350

5451
Tranche tranche1 = new Tranche();
55-
tranche1.setId( 1951L);
52+
53+
Y y = new Y();
5654

5755
Tranche tranche2 = new Tranche();
58-
tranche2.setId( 1952L);
5956

6057
preisregelung.setTranchenmodell( tranchenmodell );
6158
tranchenmodell.setPreisregelung( preisregelung );
6259

60+
tranchenmodell.setX( x );
61+
x.setTranchenmodell( tranchenmodell );
62+
6363
tranchenmodell.getTranchen().add( tranche1 );
6464
tranche1.setTranchenmodell( tranchenmodell );
6565
tranchenmodell.getTranchen().add( tranche2 );
6666
tranche2.setTranchenmodell( tranchenmodell );
6767

68+
tranche1.setY( y );
69+
y.setTranche( tranche1 );
70+
6871
Session session = openSession();
6972
session.beginTransaction();
7073
session.save( preisregelung );
@@ -84,8 +87,7 @@ private void cleanupData() {
8487

8588
@Test
8689
@TestForIssue( jiraKey = "HHH-9091")
87-
@FailureExpected( jiraKey = "HHH-9091")
88-
public void testOrphanedWhileManaged() {
90+
public void testDirectAssociationOrphanedWhileManaged() {
8991
createData();
9092

9193
Session session = openSession();
@@ -95,7 +97,11 @@ public void testOrphanedWhileManaged() {
9597
results = session.createQuery( "from Preisregelung" ).list();
9698
assertEquals( 1, results.size() );
9799
Preisregelung preisregelung = ( Preisregelung ) results.get( 0 );
98-
assertNotNull( preisregelung.getTranchenmodell() );
100+
Tranchenmodell tranchenmodell = preisregelung.getTranchenmodell();
101+
assertNotNull( tranchenmodell );
102+
assertNotNull( tranchenmodell.getX() );
103+
assertEquals( 2, tranchenmodell.getTranchen().size() );
104+
assertNotNull( tranchenmodell.getTranchen().get( 0 ).getY() );
99105
preisregelung.setTranchenmodell( null );
100106
session.getTransaction().commit();
101107
session.close();
@@ -107,6 +113,13 @@ public void testOrphanedWhileManaged() {
107113
assertNull( preisregelung.getTranchenmodell() );
108114
results = session.createQuery( "from Tranchenmodell" ).list();
109115
assertEquals( 0, results.size() );
116+
results = session.createQuery( "from Tranche" ).list();
117+
assertEquals( 0, results.size() );
118+
results = session.createQuery( "from X" ).list();
119+
assertEquals( 0, results.size() );
120+
results = session.createQuery( "from Y" ).list();
121+
assertEquals( 0, results.size() );
122+
110123
results = session.createQuery( "from Preisregelung" ).list();
111124
assertEquals( 1, results.size() );
112125

@@ -115,11 +128,10 @@ public void testOrphanedWhileManaged() {
115128

116129
cleanupData();
117130
}
118-
131+
119132
@Test
120133
@TestForIssue( jiraKey = "HHH-9091")
121-
@FailureExpected( jiraKey = "HHH-9091")
122-
public void testReplacedWhileManaged() {
134+
public void testReplacedDirectAssociationWhileManaged() {
123135
createData();
124136

125137
Session session = openSession();
@@ -129,11 +141,56 @@ public void testReplacedWhileManaged() {
129141
results = session.createQuery( "from Preisregelung" ).list();
130142
assertEquals( 1, results.size() );
131143
Preisregelung preisregelung = ( Preisregelung ) results.get( 0 );
132-
assertNotNull( preisregelung.getTranchenmodell() );
144+
Tranchenmodell tranchenmodell = preisregelung.getTranchenmodell();
145+
assertNotNull( tranchenmodell );
146+
assertNotNull( tranchenmodell.getX() );
147+
assertEquals( 2, tranchenmodell.getTranchen().size() );
148+
assertNotNull( tranchenmodell.getTranchen().get( 0 ).getY() );
133149

134-
// Replace with a new Tranchenmodell instance
150+
// Create a new Tranchenmodell with new direct and nested associations
135151
Tranchenmodell tranchenmodellNew = new Tranchenmodell();
136-
tranchenmodellNew.setId( 1952L );
152+
X xNew = new X();
153+
tranchenmodellNew.setX( xNew );
154+
xNew.setTranchenmodell( tranchenmodellNew );
155+
Tranche trancheNew = new Tranche();
156+
tranchenmodellNew.getTranchen().add( trancheNew );
157+
trancheNew.setTranchenmodell( tranchenmodellNew );
158+
Y yNew = new Y();
159+
trancheNew.setY( yNew );
160+
yNew.setTranche( trancheNew );
161+
162+
// Replace with a new Tranchenmodell instance containing new direct and nested associations
163+
preisregelung.setTranchenmodell(tranchenmodellNew );
164+
tranchenmodellNew.setPreisregelung( preisregelung );
165+
166+
session.getTransaction().commit();
167+
session.close();
168+
169+
session = openSession();
170+
session.getTransaction().begin();
171+
172+
results = session.createQuery( "from Tranche" ).list();
173+
assertEquals( 1, results.size() );
174+
results = session.createQuery( "from Tranchenmodell" ).list();
175+
assertEquals( 1, results.size() );
176+
results = session.createQuery( "from X" ).list();
177+
assertEquals( 1, results.size() );
178+
results = session.createQuery( "from Y" ).list();
179+
assertEquals( 1, results.size() );
180+
results = session.createQuery( "from Preisregelung" ).list();
181+
assertEquals( 1, results.size() );
182+
preisregelung = ( Preisregelung ) results.get( 0 );
183+
tranchenmodell = preisregelung.getTranchenmodell();
184+
assertNotNull( tranchenmodell );
185+
assertEquals( tranchenmodellNew.getId(), tranchenmodell.getId() );
186+
assertNotNull( tranchenmodell.getX() );
187+
assertEquals( xNew.getId(), tranchenmodell.getX().getId() );
188+
assertEquals( 1, tranchenmodell.getTranchen().size() );
189+
assertEquals( trancheNew.getId(), tranchenmodell.getTranchen().get( 0 ).getId() );
190+
assertEquals( yNew.getId(), tranchenmodell.getTranchen().get( 0 ).getY().getId() );
191+
192+
// Replace with a new Tranchenmodell instance with no associations
193+
tranchenmodellNew = new Tranchenmodell();
137194
preisregelung.setTranchenmodell(tranchenmodellNew );
138195
tranchenmodellNew.setPreisregelung( preisregelung );
139196
session.getTransaction().commit();
@@ -143,14 +200,63 @@ public void testReplacedWhileManaged() {
143200
session.beginTransaction();
144201
results = session.createQuery( "from Tranchenmodell" ).list();
145202
assertEquals( 1, results.size() );
146-
Tranchenmodell tranchenmodellQueried = (Tranchenmodell) results.get( 0 );
147-
assertEquals( tranchenmodellNew.getId(), tranchenmodellQueried.getId() );
203+
tranchenmodell = (Tranchenmodell) results.get( 0 );
204+
assertEquals( tranchenmodellNew.getId(), tranchenmodell.getId() );
148205
results = session.createQuery( "from Preisregelung" ).list();
149206
assertEquals( 1, results.size() );
150-
Preisregelung preisregelung1Queried = (Preisregelung) results.get( 0 );
151-
assertEquals( tranchenmodellQueried, preisregelung1Queried.getTranchenmodell() );
207+
preisregelung = (Preisregelung) results.get( 0 );
208+
assertEquals( tranchenmodell, preisregelung.getTranchenmodell() );
152209
results = session.createQuery( "from Tranche" ).list();
153210
assertEquals( 0, results.size() );
211+
results = session.createQuery( "from X" ).list();
212+
assertEquals( 0, results.size() );
213+
results = session.createQuery( "from Y" ).list();
214+
assertEquals( 0, results.size() );
215+
session.getTransaction().commit();
216+
session.close();
217+
218+
cleanupData();
219+
}
220+
221+
@Test
222+
@TestForIssue( jiraKey = "HHH-9091")
223+
public void testDirectAndNestedAssociationsOrphanedWhileManaged() {
224+
createData();
225+
226+
Session session = openSession();
227+
session.beginTransaction();
228+
List results = session.createQuery( "from Tranchenmodell" ).list();
229+
assertEquals( 1, results.size() );
230+
results = session.createQuery( "from Preisregelung" ).list();
231+
assertEquals( 1, results.size() );
232+
Preisregelung preisregelung = ( Preisregelung ) results.get( 0 );
233+
Tranchenmodell tranchenmodell = preisregelung.getTranchenmodell();
234+
assertNotNull( tranchenmodell );
235+
assertNotNull( tranchenmodell.getX() );
236+
assertEquals( 2, tranchenmodell.getTranchen().size() );
237+
assertNotNull( tranchenmodell.getTranchen().get( 0 ).getY() );
238+
preisregelung.setTranchenmodell( null );
239+
tranchenmodell.setX( null );
240+
tranchenmodell.getTranchen().get( 0 ).setY( null );
241+
session.getTransaction().commit();
242+
session.close();
243+
244+
session = openSession();
245+
session.beginTransaction();
246+
247+
preisregelung = ( Preisregelung ) session.get( Preisregelung.class, preisregelung.getId() );
248+
assertNull( preisregelung.getTranchenmodell() );
249+
results = session.createQuery( "from Tranchenmodell" ).list();
250+
assertEquals( 0, results.size() );
251+
results = session.createQuery( "from Tranche" ).list();
252+
assertEquals( 0, results.size() );
253+
results = session.createQuery( "from X" ).list();
254+
assertEquals( 0, results.size() );
255+
results = session.createQuery( "from Y" ).list();
256+
assertEquals( 0, results.size() );
257+
258+
results = session.createQuery( "from Preisregelung" ).list();
259+
assertEquals( 1, results.size() );
154260

155261
session.getTransaction().commit();
156262
session.close();
@@ -163,7 +269,9 @@ protected Class<?>[] getAnnotatedClasses() {
163269
return new Class[]{
164270
Preisregelung.class,
165271
Tranche.class,
166-
Tranchenmodell.class
272+
Tranchenmodell.class,
273+
X.class,
274+
Y.class
167275
};
168276
}
169277

hibernate-core/src/test/java/org/hibernate/test/orphan/one2one/fk/bidirectional/multilevelcascade/Preisregelung.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
@Entity
2929
public class Preisregelung {
3030
@Id
31+
@GeneratedValue
3132
private Long id;
3233

3334
@OneToOne(mappedBy = "preisregelung", cascade = CascadeType.ALL, fetch = FetchType.LAZY, orphanRemoval = true)

hibernate-core/src/test/java/org/hibernate/test/orphan/one2one/fk/bidirectional/multilevelcascade/Tranche.java

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,13 +29,16 @@
2929
public class Tranche {
3030

3131
@Id
32-
private Long id;
32+
@GeneratedValue
33+
private Long id;
3334

3435
@ManyToOne(optional = false, fetch = FetchType.EAGER)
3536
private Tranchenmodell tranchenmodell;
3637

38+
@OneToOne(mappedBy = "tranche", cascade = CascadeType.ALL, fetch = FetchType.LAZY, orphanRemoval = true)
39+
private Y y;
3740

38-
public Long getId() {
41+
public Long getId() {
3942
return id;
4043
}
4144

@@ -50,4 +53,12 @@ public Tranchenmodell getTranchenmodell() {
5053
public void setTranchenmodell(Tranchenmodell tranchenmodell) {
5154
this.tranchenmodell = tranchenmodell;
5255
}
56+
57+
public Y getY() {
58+
return y;
59+
}
60+
61+
public void setY(Y y) {
62+
this.y = y;
63+
}
5364
}

0 commit comments

Comments
 (0)