Skip to content

Commit 055570c

Browse files
mbelladebeikov
authored andcommitted
HHH-18212 Fix transient check for entities deleted during the same flush
1 parent 52a539d commit 055570c

File tree

10 files changed

+180
-57
lines changed

10 files changed

+180
-57
lines changed

hibernate-core/src/main/java/org/hibernate/engine/spi/CascadingActions.java

Lines changed: 19 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -382,28 +382,30 @@ public void cascade(
382382
// a proxy is always non-transient
383383
// and ForeignKeys.isTransient()
384384
// is not written to expect a proxy
385-
&& !isHibernateProxy( child )
386-
// if it's associated with the session
387-
// we are good, even if it's not yet
388-
// inserted, since ordering problems
389-
// are detected and handled elsewhere
390-
&& !isInManagedState( child, session )
391-
// TODO: check if it is a merged entity which has not yet been flushed
392-
// Currently this throws if you directly reference a new transient
393-
// instance after a call to merge() that results in its managed copy
394-
// being scheduled for insertion, if the insert has not yet occurred.
395-
// This is not terrible: it's more correct to "swap" the reference to
396-
// point to the managed instance, but it's probably too heavy-handed.
397-
&& isTransient( entityName, child, null, session ) ) {
398-
throw new TransientObjectException( "persistent instance references an unsaved transient instance of '"
399-
+ entityName + "' (save the transient instance before flushing)" );
400-
//TODO: should be TransientPropertyValueException
385+
&& !isHibernateProxy( child ) ) {
386+
// if it's associated with the session
387+
// we are good, even if it's not yet
388+
// inserted, since ordering problems
389+
// are detected and handled elsewhere
390+
final EntityEntry entry = session.getPersistenceContextInternal().getEntry( child );
391+
if ( !isInManagedState( entry )
392+
// TODO: check if it is a merged entity which has not yet been flushed
393+
// Currently this throws if you directly reference a new transient
394+
// instance after a call to merge() that results in its managed copy
395+
// being scheduled for insertion, if the insert has not yet occurred.
396+
// This is not terrible: it's more correct to "swap" the reference to
397+
// point to the managed instance, but it's probably too heavy-handed.
398+
&& ( entry != null && entry.getStatus() == Status.DELETED || isTransient( entityName, child, null, session ) ) ) {
399+
throw new TransientObjectException( "persistent instance references an unsaved transient instance of '" +
400+
entityName + "' (save the transient instance before flushing)" );
401+
//TODO: should be TransientPropertyValueException
401402
// throw new TransientPropertyValueException(
402403
// "object references an unsaved transient instance - save the transient instance before flushing",
403404
// entityName,
404405
// persister.getEntityName(),
405406
// persister.getPropertyNames()[propertyIndex]
406407
// );
408+
}
407409
}
408410
}
409411

@@ -441,8 +443,7 @@ public String toString() {
441443
}
442444
};
443445

444-
private static boolean isInManagedState(Object child, EventSource session) {
445-
final EntityEntry entry = session.getPersistenceContextInternal().getEntry( child );
446+
private static boolean isInManagedState(EntityEntry entry) {
446447
if ( entry == null ) {
447448
return false;
448449
}

hibernate-core/src/main/java/org/hibernate/event/internal/AbstractFlushingEventListener.java

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -160,9 +160,19 @@ private void prepareEntityFlushes(EventSource session, PersistenceContext persis
160160
// processed, so that all entities which will be persisted are
161161
// persistent when we do the check (I wonder if we could move this
162162
// into Nullability, instead of abusing the Cascade infrastructure)
163-
persistenceContext.getEntitiesByKey().forEach( (entry, entity) -> {
164-
Cascade.cascade( CascadingActions.CHECK_ON_FLUSH, CascadePoint.BEFORE_FLUSH, session, entry.getPersister(), entity, null );
165-
} );
163+
for ( Map.Entry<Object, EntityEntry> me : persistenceContext.reentrantSafeEntityEntries() ) {
164+
final EntityEntry entry = me.getValue();
165+
if ( flushable( entry ) ) {
166+
Cascade.cascade(
167+
CascadingActions.CHECK_ON_FLUSH,
168+
CascadePoint.BEFORE_FLUSH,
169+
session,
170+
entry.getPersister(),
171+
me.getKey(),
172+
null
173+
);
174+
}
175+
}
166176
}
167177

168178
private static boolean flushable(EntityEntry entry) {

hibernate-core/src/test/java/org/hibernate/orm/test/annotations/embedded/EmbeddableWithManyToOneCircularityTest.java

Lines changed: 9 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -59,17 +59,15 @@ public void setUp(SessionFactoryScope scope) {
5959
public void tearDown(SessionFactoryScope scope) {
6060
scope.inTransaction(
6161
session -> {
62-
session.createQuery( "from EntityTest", EntityTest.class ).list().forEach(
63-
entityTest -> {
64-
session.delete( entityTest );
65-
}
66-
);
67-
68-
session.createQuery( "from EntityTest2", EntityTest2.class ).list().forEach(
69-
entityTest -> {
70-
session.delete( entityTest );
71-
}
72-
);
62+
session.createQuery( "from EntityTest", EntityTest.class ).getResultList().forEach( entity -> {
63+
final EntityTest2 entity2 = entity.getEntity2();
64+
if ( entity2 != null && entity2.getEmbeddedAttribute() != null ) {
65+
entity2.getEmbeddedAttribute().setEntity( null );
66+
}
67+
session.remove( entity );
68+
} );
69+
session.flush();
70+
session.createMutationQuery( "delete from EntityTest2" ).executeUpdate();
7371
}
7472
);
7573
}

hibernate-core/src/test/java/org/hibernate/orm/test/annotations/embedded/EmbeddableWithManyToOneTest.java

Lines changed: 8 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -61,17 +61,14 @@ public void setUp(SessionFactoryScope scope) {
6161
public void tearDown(SessionFactoryScope scope) {
6262
scope.inTransaction(
6363
session -> {
64-
session.createQuery( "from EntityTest", EntityTest.class ).list().forEach(
65-
entityTest -> {
66-
session.delete( entityTest );
67-
}
68-
);
69-
70-
session.createQuery( "from EntityTest2", EntityTest2.class ).list().forEach(
71-
entityTest -> {
72-
session.delete( entityTest );
73-
}
74-
);
64+
session.createQuery( "from EntityTest", EntityTest.class ).getResultList().forEach( entity -> {
65+
final EntityTest2 entity2 = entity.getEntity2();
66+
if ( entity2 != null && entity2.getEmbeddedAttribute() != null ) {
67+
entity2.getEmbeddedAttribute().setEntity( null );
68+
}
69+
session.remove( entity );
70+
} );
71+
session.createMutationQuery( "delete from EntityTest2" ).executeUpdate();
7572
}
7673
);
7774
}

hibernate-core/src/test/java/org/hibernate/orm/test/annotations/formula/ManyToManyNotIgnoreLazyFetchingTest.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ protected void afterEntityManagerFactoryBuilt() {
8080
entityManager.flush();
8181

8282
entityManager.remove(code);
83+
stock1.getCodes().remove( code );
8384
} );
8485
}
8586

hibernate-core/src/test/java/org/hibernate/orm/test/annotations/manytoone/ManyToOneJoinTest.java

Lines changed: 4 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
import static org.junit.jupiter.api.Assertions.assertEquals;
1919
import static org.junit.jupiter.api.Assertions.assertNotNull;
2020
import static org.junit.jupiter.api.Assertions.assertNull;
21+
import static org.junit.jupiter.api.Assertions.assertSame;
2122

2223
/**
2324
* @author Emmanuel Bernard
@@ -36,18 +37,9 @@ public class ManyToOneJoinTest {
3637
public void teardDown(SessionFactoryScope scope) {
3738
scope.inTransaction(
3839
session -> {
39-
List<ForestType> forestTypes = session.createQuery( "from ForestType" ).list();
40-
forestTypes.forEach(
41-
forestType -> {
42-
forestType.getTrees().forEach(
43-
tree ->{
44-
session.delete( tree.getForestType() );
45-
session.delete( tree );
46-
}
47-
);
48-
session.delete( forestType );
49-
}
50-
);
40+
session.createMutationQuery( "delete from TreeType" ).executeUpdate();
41+
session.createMutationQuery( "delete from ForestType" ).executeUpdate();
42+
session.createMutationQuery( "delete from BiggestForest" ).executeUpdate();
5143
}
5244
);
5345
}

hibernate-core/src/test/java/org/hibernate/orm/test/annotations/onetoone/OptionalOneToOneMappedByTest.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import static org.hibernate.testing.transaction.TransactionUtil.doInHibernate;
2323
import static org.junit.Assert.assertEquals;
2424
import static org.junit.Assert.assertNotNull;
25+
import static org.junit.Assert.assertNotSame;
2526
import static org.junit.Assert.assertNull;
2627
import static org.junit.Assert.fail;
2728

@@ -182,6 +183,8 @@ public void testBidirQueryEntityProperty() {
182183
// .uniqueResult();
183184

184185
session.delete( personAddress );
186+
assertNotSame( person, personAddress.getPerson() );
187+
personAddress.getPerson().setPersonAddress( null );
185188
} );
186189
}
187190

hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/lazy/proxy/inlinedirtychecking/LoadUninitializedCollectionTest.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,7 @@ public void tearDown(SessionFactoryScope scope) {
116116
bank.getDepartments().forEach(
117117
department -> entityManager.remove( department )
118118
);
119+
bank.getDepartments().clear();
119120
List<BankAccount> accounts = entityManager.createQuery( "from BankAccount" ).getResultList();
120121

121122
accounts.forEach(

hibernate-core/src/test/java/org/hibernate/orm/test/jpa/cascade/multicircle/MultiCircleJpaCascadeTest.java

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -258,8 +258,7 @@ public void testPersistThenUpdateNoCascadeToTransient3(EntityManagerFactoryScope
258258
IllegalStateException ise = (IllegalStateException) ex.getCause();
259259
assertTyping( TransientObjectException.class, ise.getCause() );
260260
String message = ise.getCause().getMessage();
261-
assertTrue( message.contains("'org.hibernate.orm.test.jpa.cascade.multicircle.F'") );
262-
assertTrue( message.contains("'g'") );
261+
assertTrue( message.contains("org.hibernate.orm.test.jpa.cascade.multicircle") );
263262
}
264263
finally {
265264
entityManager.getTransaction().rollback();
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
/*
2+
* Hibernate, Relational Persistence for Idiomatic Java
3+
*
4+
* License: GNU Lesser General Public License (LGPL), version 2.1 or later
5+
* See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html
6+
*/
7+
package org.hibernate.orm.test.onetoone.bidirectional;
8+
9+
import org.hibernate.testing.orm.junit.DomainModel;
10+
import org.hibernate.testing.orm.junit.SessionFactory;
11+
import org.hibernate.testing.orm.junit.SessionFactoryScope;
12+
import org.junit.jupiter.api.Test;
13+
14+
import jakarta.persistence.CascadeType;
15+
import jakarta.persistence.Column;
16+
import jakarta.persistence.Entity;
17+
import jakarta.persistence.Id;
18+
import jakarta.persistence.JoinColumn;
19+
import jakarta.persistence.OneToOne;
20+
21+
import static org.assertj.core.api.Assertions.assertThat;
22+
23+
/**
24+
* @author Marco Belladelli
25+
*/
26+
@DomainModel( annotatedClasses = {
27+
BidirectionalOneToOneCascadeRemoveTest.A.class,
28+
BidirectionalOneToOneCascadeRemoveTest.B.class,
29+
} )
30+
@SessionFactory
31+
public class BidirectionalOneToOneCascadeRemoveTest {
32+
@Test
33+
public void testWithFlush(SessionFactoryScope scope) {
34+
scope.inTransaction( session -> {
35+
final A a1 = new A( "1", "a1", 1 );
36+
session.persist( a1 );
37+
final B bRef = new B( "2", "b2", 2, a1 );
38+
session.persist( bRef );
39+
session.flush();
40+
41+
session.remove( bRef );
42+
} );
43+
scope.inTransaction( session -> {
44+
assertThat( session.find( A.class, "1" ) ).isNull();
45+
assertThat( session.find( B.class, "2" ) ).isNull();
46+
} );
47+
}
48+
49+
@Test
50+
public void testWithoutFlush(SessionFactoryScope scope) {
51+
scope.inTransaction( session -> {
52+
final A a1 = new A( "1", "a1", 1 );
53+
session.persist( a1 );
54+
final B bRef = new B( "2", "b2", 2, a1 );
55+
session.persist( bRef );
56+
57+
session.remove( bRef );
58+
} );
59+
scope.inTransaction( session -> {
60+
assertThat( session.find( A.class, "1" ) ).isNull();
61+
assertThat( session.find( B.class, "2" ) ).isNull();
62+
} );
63+
}
64+
65+
@Entity( name = "EntityA" )
66+
static class A {
67+
@Id
68+
protected String id;
69+
70+
@Column( name = "name_col" )
71+
protected String name;
72+
73+
@Column( name = "value_col" )
74+
protected int value;
75+
76+
77+
@OneToOne( mappedBy = "a1" )
78+
protected B b1;
79+
80+
public A() {
81+
}
82+
83+
public A(String id, String name, int value) {
84+
this.id = id;
85+
this.name = name;
86+
this.value = value;
87+
}
88+
}
89+
90+
@Entity( name = "EntityB" )
91+
static class B {
92+
@Id
93+
protected String id;
94+
95+
@Column( name = "name_col" )
96+
protected String name;
97+
98+
@Column( name = "value_col" )
99+
protected int value;
100+
101+
// ===========================================================
102+
// relationship fields
103+
104+
@OneToOne( cascade = CascadeType.REMOVE )
105+
@JoinColumn( name = "a1_id" )
106+
protected A a1;
107+
108+
// ===========================================================
109+
// constructors
110+
111+
public B() {
112+
}
113+
114+
public B(String id, String name, int value, A a1) {
115+
this.id = id;
116+
this.name = name;
117+
this.value = value;
118+
this.a1 = a1;
119+
}
120+
}
121+
}

0 commit comments

Comments
 (0)