HHH-18212 Fix transient check for entities deleted during the same flush
This commit is contained in:
parent
52a539d727
commit
055570c8af
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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();
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 );
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -80,6 +80,7 @@ public class ManyToManyNotIgnoreLazyFetchingTest extends BaseEntityManagerFuncti
|
||||||
entityManager.flush();
|
entityManager.flush();
|
||||||
|
|
||||||
entityManager.remove(code);
|
entityManager.remove(code);
|
||||||
|
stock1.getCodes().remove( code );
|
||||||
} );
|
} );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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 );
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 );
|
||||||
} );
|
} );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue