HHH-18212 Fix transient check for entities deleted during the same flush

This commit is contained in:
Marco Belladelli 2024-06-11 16:40:41 +02:00 committed by Christian Beikov
parent 52a539d727
commit 055570c8af
10 changed files with 180 additions and 57 deletions

View File

@ -382,28 +382,30 @@ public class CascadingActions {
// a proxy is always non-transient // a proxy is always non-transient
// and ForeignKeys.isTransient() // and ForeignKeys.isTransient()
// is not written to expect a proxy // is not written to expect a proxy
&& !isHibernateProxy( child ) && !isHibernateProxy( child ) ) {
// if it's associated with the session // if it's associated with the session
// we are good, even if it's not yet // we are good, even if it's not yet
// inserted, since ordering problems // inserted, since ordering problems
// are detected and handled elsewhere // are detected and handled elsewhere
&& !isInManagedState( child, session ) final EntityEntry entry = session.getPersistenceContextInternal().getEntry( child );
// TODO: check if it is a merged entity which has not yet been flushed if ( !isInManagedState( entry )
// Currently this throws if you directly reference a new transient // TODO: check if it is a merged entity which has not yet been flushed
// instance after a call to merge() that results in its managed copy // Currently this throws if you directly reference a new transient
// being scheduled for insertion, if the insert has not yet occurred. // instance after a call to merge() that results in its managed copy
// This is not terrible: it's more correct to "swap" the reference to // being scheduled for insertion, if the insert has not yet occurred.
// point to the managed instance, but it's probably too heavy-handed. // This is not terrible: it's more correct to "swap" the reference to
&& isTransient( entityName, child, null, session ) ) { // point to the managed instance, but it's probably too heavy-handed.
throw new TransientObjectException( "persistent instance references an unsaved transient instance of '" && ( entry != null && entry.getStatus() == Status.DELETED || isTransient( entityName, child, null, session ) ) ) {
+ entityName + "' (save the transient instance before flushing)" ); throw new TransientObjectException( "persistent instance references an unsaved transient instance of '" +
//TODO: should be TransientPropertyValueException entityName + "' (save the transient instance before flushing)" );
//TODO: should be TransientPropertyValueException
// throw new TransientPropertyValueException( // throw new TransientPropertyValueException(
// "object references an unsaved transient instance - save the transient instance before flushing", // "object references an unsaved transient instance - save the transient instance before flushing",
// entityName, // entityName,
// persister.getEntityName(), // persister.getEntityName(),
// persister.getPropertyNames()[propertyIndex] // persister.getPropertyNames()[propertyIndex]
// ); // );
}
} }
} }
@ -441,8 +443,7 @@ public class CascadingActions {
} }
}; };
private static boolean isInManagedState(Object child, EventSource session) { private static boolean isInManagedState(EntityEntry entry) {
final EntityEntry entry = session.getPersistenceContextInternal().getEntry( child );
if ( entry == null ) { if ( entry == null ) {
return false; return false;
} }

View File

@ -160,9 +160,19 @@ public abstract class AbstractFlushingEventListener implements JpaBootstrapSensi
// processed, so that all entities which will be persisted are // processed, so that all entities which will be persisted are
// persistent when we do the check (I wonder if we could move this // persistent when we do the check (I wonder if we could move this
// into Nullability, instead of abusing the Cascade infrastructure) // into Nullability, instead of abusing the Cascade infrastructure)
persistenceContext.getEntitiesByKey().forEach( (entry, entity) -> { for ( Map.Entry<Object, EntityEntry> me : persistenceContext.reentrantSafeEntityEntries() ) {
Cascade.cascade( CascadingActions.CHECK_ON_FLUSH, CascadePoint.BEFORE_FLUSH, session, entry.getPersister(), entity, null ); final EntityEntry entry = me.getValue();
} ); if ( flushable( entry ) ) {
Cascade.cascade(
CascadingActions.CHECK_ON_FLUSH,
CascadePoint.BEFORE_FLUSH,
session,
entry.getPersister(),
me.getKey(),
null
);
}
}
} }
private static boolean flushable(EntityEntry entry) { private static boolean flushable(EntityEntry entry) {

View File

@ -59,17 +59,15 @@ public class EmbeddableWithManyToOneCircularityTest {
public void tearDown(SessionFactoryScope scope) { public void tearDown(SessionFactoryScope scope) {
scope.inTransaction( scope.inTransaction(
session -> { session -> {
session.createQuery( "from EntityTest", EntityTest.class ).list().forEach( session.createQuery( "from EntityTest", EntityTest.class ).getResultList().forEach( entity -> {
entityTest -> { final EntityTest2 entity2 = entity.getEntity2();
session.delete( entityTest ); if ( entity2 != null && entity2.getEmbeddedAttribute() != null ) {
} entity2.getEmbeddedAttribute().setEntity( null );
); }
session.remove( entity );
session.createQuery( "from EntityTest2", EntityTest2.class ).list().forEach( } );
entityTest -> { session.flush();
session.delete( entityTest ); session.createMutationQuery( "delete from EntityTest2" ).executeUpdate();
}
);
} }
); );
} }

View File

@ -61,17 +61,14 @@ public class EmbeddableWithManyToOneTest {
public void tearDown(SessionFactoryScope scope) { public void tearDown(SessionFactoryScope scope) {
scope.inTransaction( scope.inTransaction(
session -> { session -> {
session.createQuery( "from EntityTest", EntityTest.class ).list().forEach( session.createQuery( "from EntityTest", EntityTest.class ).getResultList().forEach( entity -> {
entityTest -> { final EntityTest2 entity2 = entity.getEntity2();
session.delete( entityTest ); if ( entity2 != null && entity2.getEmbeddedAttribute() != null ) {
} entity2.getEmbeddedAttribute().setEntity( null );
); }
session.remove( entity );
session.createQuery( "from EntityTest2", EntityTest2.class ).list().forEach( } );
entityTest -> { session.createMutationQuery( "delete from EntityTest2" ).executeUpdate();
session.delete( entityTest );
}
);
} }
); );
} }

View File

@ -80,6 +80,7 @@ public class ManyToManyNotIgnoreLazyFetchingTest extends BaseEntityManagerFuncti
entityManager.flush(); entityManager.flush();
entityManager.remove(code); entityManager.remove(code);
stock1.getCodes().remove( code );
} ); } );
} }

View File

@ -18,6 +18,7 @@ import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertSame;
/** /**
* @author Emmanuel Bernard * @author Emmanuel Bernard
@ -36,18 +37,9 @@ public class ManyToOneJoinTest {
public void teardDown(SessionFactoryScope scope) { public void teardDown(SessionFactoryScope scope) {
scope.inTransaction( scope.inTransaction(
session -> { session -> {
List<ForestType> forestTypes = session.createQuery( "from ForestType" ).list(); session.createMutationQuery( "delete from TreeType" ).executeUpdate();
forestTypes.forEach( session.createMutationQuery( "delete from ForestType" ).executeUpdate();
forestType -> { session.createMutationQuery( "delete from BiggestForest" ).executeUpdate();
forestType.getTrees().forEach(
tree ->{
session.delete( tree.getForestType() );
session.delete( tree );
}
);
session.delete( forestType );
}
);
} }
); );
} }

View File

@ -22,6 +22,7 @@ import static org.hibernate.testing.junit4.ExtraAssertions.assertTyping;
import static org.hibernate.testing.transaction.TransactionUtil.doInHibernate; import static org.hibernate.testing.transaction.TransactionUtil.doInHibernate;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNotSame;
import static org.junit.Assert.assertNull; import static org.junit.Assert.assertNull;
import static org.junit.Assert.fail; import static org.junit.Assert.fail;
@ -182,6 +183,8 @@ public class OptionalOneToOneMappedByTest extends BaseCoreFunctionalTestCase {
// .uniqueResult(); // .uniqueResult();
session.delete( personAddress ); session.delete( personAddress );
assertNotSame( person, personAddress.getPerson() );
personAddress.getPerson().setPersonAddress( null );
} ); } );
} }

View File

@ -116,6 +116,7 @@ public class LoadUninitializedCollectionTest {
bank.getDepartments().forEach( bank.getDepartments().forEach(
department -> entityManager.remove( department ) department -> entityManager.remove( department )
); );
bank.getDepartments().clear();
List<BankAccount> accounts = entityManager.createQuery( "from BankAccount" ).getResultList(); List<BankAccount> accounts = entityManager.createQuery( "from BankAccount" ).getResultList();
accounts.forEach( accounts.forEach(

View File

@ -258,8 +258,7 @@ public class MultiCircleJpaCascadeTest {
IllegalStateException ise = (IllegalStateException) ex.getCause(); IllegalStateException ise = (IllegalStateException) ex.getCause();
assertTyping( TransientObjectException.class, ise.getCause() ); assertTyping( TransientObjectException.class, ise.getCause() );
String message = ise.getCause().getMessage(); String message = ise.getCause().getMessage();
assertTrue( message.contains("'org.hibernate.orm.test.jpa.cascade.multicircle.F'") ); assertTrue( message.contains("org.hibernate.orm.test.jpa.cascade.multicircle") );
assertTrue( message.contains("'g'") );
} }
finally { finally {
entityManager.getTransaction().rollback(); entityManager.getTransaction().rollback();

View File

@ -0,0 +1,121 @@
/*
* 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.onetoone.bidirectional;
import org.hibernate.testing.orm.junit.DomainModel;
import org.hibernate.testing.orm.junit.SessionFactory;
import org.hibernate.testing.orm.junit.SessionFactoryScope;
import org.junit.jupiter.api.Test;
import jakarta.persistence.CascadeType;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.OneToOne;
import static org.assertj.core.api.Assertions.assertThat;
/**
* @author Marco Belladelli
*/
@DomainModel( annotatedClasses = {
BidirectionalOneToOneCascadeRemoveTest.A.class,
BidirectionalOneToOneCascadeRemoveTest.B.class,
} )
@SessionFactory
public class BidirectionalOneToOneCascadeRemoveTest {
@Test
public void testWithFlush(SessionFactoryScope scope) {
scope.inTransaction( session -> {
final A a1 = new A( "1", "a1", 1 );
session.persist( a1 );
final B bRef = new B( "2", "b2", 2, a1 );
session.persist( bRef );
session.flush();
session.remove( bRef );
} );
scope.inTransaction( session -> {
assertThat( session.find( A.class, "1" ) ).isNull();
assertThat( session.find( B.class, "2" ) ).isNull();
} );
}
@Test
public void testWithoutFlush(SessionFactoryScope scope) {
scope.inTransaction( session -> {
final A a1 = new A( "1", "a1", 1 );
session.persist( a1 );
final B bRef = new B( "2", "b2", 2, a1 );
session.persist( bRef );
session.remove( bRef );
} );
scope.inTransaction( session -> {
assertThat( session.find( A.class, "1" ) ).isNull();
assertThat( session.find( B.class, "2" ) ).isNull();
} );
}
@Entity( name = "EntityA" )
static class A {
@Id
protected String id;
@Column( name = "name_col" )
protected String name;
@Column( name = "value_col" )
protected int value;
@OneToOne( mappedBy = "a1" )
protected B b1;
public A() {
}
public A(String id, String name, int value) {
this.id = id;
this.name = name;
this.value = value;
}
}
@Entity( name = "EntityB" )
static class B {
@Id
protected String id;
@Column( name = "name_col" )
protected String name;
@Column( name = "value_col" )
protected int value;
// ===========================================================
// relationship fields
@OneToOne( cascade = CascadeType.REMOVE )
@JoinColumn( name = "a1_id" )
protected A a1;
// ===========================================================
// constructors
public B() {
}
public B(String id, String name, int value, A a1) {
this.id = id;
this.name = name;
this.value = value;
this.a1 = a1;
}
}
}