HHH-11209 : Test cases

HHH-11209 : NullPointerException in EntityType.replace() with a PersistentBag

HHH-11209 : Add test for merging a detached collection with queued operations

HHH-11209 : Throw UnsupportedOperationException if a detached collection with queued operations is merged

HHH-11209 : Ignore queued operations when merging a detached collection with queued operations; add warnings

HHH-11209 : Fix typo in comment
This commit is contained in:
Gail Badner 2018-07-26 16:00:22 -07:00
parent fcb65c075e
commit 6f5b1e5543
8 changed files with 797 additions and 24 deletions

View File

@ -57,8 +57,11 @@ public final class CollectionUpdateAction extends CollectionAction {
preUpdate(); preUpdate();
if ( !collection.wasInitialized() ) { if ( !collection.wasInitialized() ) {
if ( !collection.hasQueuedOperations() ) { // If there were queued operations, they would have been processed
throw new AssertionFailure( "no queued adds" ); // and cleared by now.
// The collection should still be dirty.
if ( !collection.isDirty() ) {
throw new AssertionFailure( "collection is not dirty" );
} }
//do nothing - we only need to notify the cache... //do nothing - we only need to notify the cache...
} }

View File

@ -9,7 +9,9 @@ package org.hibernate.action.internal;
import java.io.Serializable; import java.io.Serializable;
import org.hibernate.HibernateException; import org.hibernate.HibernateException;
import org.hibernate.collection.internal.AbstractPersistentCollection;
import org.hibernate.collection.spi.PersistentCollection; import org.hibernate.collection.spi.PersistentCollection;
import org.hibernate.engine.spi.CollectionEntry;
import org.hibernate.engine.spi.SharedSessionContractImplementor; import org.hibernate.engine.spi.SharedSessionContractImplementor;
import org.hibernate.persister.collection.CollectionPersister; import org.hibernate.persister.collection.CollectionPersister;
@ -40,6 +42,21 @@ public final class QueuedOperationCollectionAction extends CollectionAction {
@Override @Override
public void execute() throws HibernateException { public void execute() throws HibernateException {
// this QueuedOperationCollectionAction has to be executed before any other
// CollectionAction involving the same collection.
getPersister().processQueuedOps( getCollection(), getKey(), getSession() ); getPersister().processQueuedOps( getCollection(), getKey(), getSession() );
// TODO: It would be nice if this could be done safely by CollectionPersister#processQueuedOps;
// Can't change the SPI to do this though.
((AbstractPersistentCollection) getCollection() ).clearOperationQueue();
// The other CollectionAction types call CollectionEntry#afterAction, which
// clears the dirty flag. We don't want to call CollectionEntry#afterAction unless
// there is no other CollectionAction that will be executed on the same collection.
final CollectionEntry ce = getSession().getPersistenceContext().getCollectionEntry( getCollection() );
if ( !ce.isDoremove() && !ce.isDoupdate() && !ce.isDorecreate() ) {
ce.afterAction( getCollection() );
}
} }
} }

View File

@ -512,6 +512,7 @@ public abstract class AbstractPersistentCollection implements Serializable, Pers
for ( DelayedOperation operation : operationQueue ) { for ( DelayedOperation operation : operationQueue ) {
operation.operate(); operation.operate();
} }
clearOperationQueue();
} }
@Override @Override
@ -523,11 +524,15 @@ public abstract class AbstractPersistentCollection implements Serializable, Pers
@Override @Override
public void postAction() { public void postAction() {
operationQueue = null; clearOperationQueue();
cachedSize = -1; cachedSize = -1;
clearDirty(); clearDirty();
} }
public final void clearOperationQueue() {
operationQueue = null;
}
@Override @Override
public Object getValue() { public Object getValue() {
return this; return this;
@ -549,9 +554,8 @@ public abstract class AbstractPersistentCollection implements Serializable, Pers
public boolean afterInitialize() { public boolean afterInitialize() {
setInitialized(); setInitialized();
//do this bit after setting initialized to true or it will recurse //do this bit after setting initialized to true or it will recurse
if ( operationQueue != null ) { if ( hasQueuedOperations() ) {
performQueuedOperations(); performQueuedOperations();
operationQueue = null;
cachedSize = -1; cachedSize = -1;
return false; return false;
} }
@ -620,6 +624,9 @@ public abstract class AbstractPersistentCollection implements Serializable, Pers
prepareForPossibleLoadingOutsideTransaction(); prepareForPossibleLoadingOutsideTransaction();
if ( currentSession == this.session ) { if ( currentSession == this.session ) {
if ( !isTempSession ) { if ( !isTempSession ) {
if ( hasQueuedOperations() ) {
LOG.queuedOperationWhenDetachFromSession( MessageHelper.collectionInfoString( getRole(), getKey() ) );
}
this.session = null; this.session = null;
} }
return true; return true;
@ -647,25 +654,22 @@ public abstract class AbstractPersistentCollection implements Serializable, Pers
if ( session == this.session ) { if ( session == this.session ) {
return false; return false;
} }
else { else if ( this.session != null ) {
if ( this.session != null ) { final String msg = generateUnexpectedSessionStateMessage( session );
final String msg = generateUnexpectedSessionStateMessage( session ); if ( isConnectedToSession() ) {
if ( isConnectedToSession() ) { throw new HibernateException(
throw new HibernateException( "Illegal attempt to associate a collection with two open sessions. " + msg
"Illegal attempt to associate a collection with two open sessions. " + msg );
);
}
else {
LOG.logUnexpectedSessionInCollectionNotConnected( msg );
this.session = session;
return true;
}
} }
else { else {
this.session = session; LOG.logUnexpectedSessionInCollectionNotConnected( msg );
return true;
} }
} }
if ( hasQueuedOperations() ) {
LOG.queuedOperationWhenAttachToSession( MessageHelper.collectionInfoString( getRole(), getKey() ) );
}
this.session = session;
return true;
} }
private String generateUnexpectedSessionStateMessage(SharedSessionContractImplementor session) { private String generateUnexpectedSessionStateMessage(SharedSessionContractImplementor session) {

View File

@ -1831,4 +1831,16 @@ public interface CoreMessageLogger extends BasicLogger {
@LogMessage(level = INFO) @LogMessage(level = INFO)
@Message(value = "Query plan cache misses: %s", id = 493) @Message(value = "Query plan cache misses: %s", id = 493)
void queryPlanCacheMisses(long queryPlanCacheMissCount); void queryPlanCacheMisses(long queryPlanCacheMissCount);
@LogMessage(level = WARN)
@Message(value = "Attempt to merge an uninitialized collection with queued operations; queued operations will be ignored: %s", id = 494)
void ignoreQueuedOperationsOnMerge(String collectionInfoString);
@LogMessage(level = WARN)
@Message(value = "Attaching an uninitialized collection with queued operations to a session: %s", id = 495)
void queuedOperationWhenAttachToSession(String collectionInfoString);
@LogMessage(level = WARN)
@Message(value = "Detaching an uninitialized collection with queued operations from a session: %s", id = 496)
void queuedOperationWhenDetachFromSession(String collectionInfoString);
} }

View File

@ -454,7 +454,7 @@ public abstract class CollectionType extends AbstractType implements Association
public Object resolve(Object value, SharedSessionContractImplementor session, Object owner) public Object resolve(Object value, SharedSessionContractImplementor session, Object owner)
throws HibernateException { throws HibernateException {
return resolve(value, session, owner, null); return resolve( value, session, owner, null );
} }
@Override @Override
@ -681,8 +681,23 @@ public abstract class CollectionType extends AbstractType implements Association
} }
if ( !Hibernate.isInitialized( original ) ) { if ( !Hibernate.isInitialized( original ) ) {
if ( ( (PersistentCollection) original ).hasQueuedOperations() ) { if ( ( (PersistentCollection) original ).hasQueuedOperations() ) {
final AbstractPersistentCollection pc = (AbstractPersistentCollection) original; if ( original == target ) {
pc.replaceQueuedOperationValues( getPersister( session ), copyCache ); // A managed entity with an uninitialized collection is being merged,
// We need to replace any detached entities in the queued operations
// with managed copies.
final AbstractPersistentCollection pc = (AbstractPersistentCollection) original;
pc.replaceQueuedOperationValues( getPersister( session ), copyCache );
}
else {
// original is a detached copy of the collection;
// it contains queued operations, which will be ignored
LOG.ignoreQueuedOperationsOnMerge(
MessageHelper.collectionInfoString(
getRole(),
( (PersistentCollection) original ).getKey()
)
);
}
} }
return target; return target;
} }

View File

@ -0,0 +1,288 @@
/*
* 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.test.collection.delayedOperation;
import java.util.ArrayList;
import java.util.List;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.ManyToOne;
import javax.persistence.OneToMany;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.hibernate.Hibernate;
import org.hibernate.Session;
import org.hibernate.collection.internal.AbstractPersistentCollection;
import org.hibernate.testing.TestForIssue;
import org.hibernate.testing.junit4.BaseCoreFunctionalTestCase;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
/**
* Tests delayed operations that are queued for a PersistentBag. The Bag does not have
* to be extra-lazy to queue the operations.
* @author Gail Badner
*/
public class BagDelayedOperationNoCascadeTest extends BaseCoreFunctionalTestCase {
private Long parentId;
@Override
protected Class[] getAnnotatedClasses() {
return new Class[] {
Parent.class,
Child.class
};
}
@Before
public void setup() {
// start by cleaning up in case a test fails
if ( parentId != null ) {
cleanup();
}
Parent parent = new Parent();
Child child1 = new Child( "Sherman" );
Child child2 = new Child( "Yogi" );
parent.addChild( child1 );
parent.addChild( child2 );
Session s = openSession();
s.getTransaction().begin();
s.persist( child1 );
s.persist( child2 );
s.persist( parent );
s.getTransaction().commit();
s.close();
parentId = parent.getId();
}
@After
public void cleanup() {
Session s = openSession();
s.getTransaction().begin();
s.createQuery( "delete from Child" ).executeUpdate();
s.createQuery( "delete from Parent" ).executeUpdate();
s.getTransaction().commit();
s.close();
parentId = null;
}
@Test
@TestForIssue( jiraKey = "HHH-5855")
public void testSimpleAddManaged() {
// Add 2 Child entities
Session s = openSession();
s.getTransaction().begin();
Child c1 = new Child( "Darwin" );
s.persist( c1 );
Child c2 = new Child( "Comet" );
s.persist( c2 );
s.getTransaction().commit();
s.close();
// Add a managed Child and commit
s = openSession();
s.getTransaction().begin();
Parent p = s.get( Parent.class, parentId );
assertFalse( Hibernate.isInitialized( p.getChildren() ) );
// get the first Child so it is managed; add to collection
p.addChild( s.get( Child.class, c1.getId() ) );
// collection should still be uninitialized
assertFalse( Hibernate.isInitialized( p.getChildren() ) );
s.getTransaction().commit();
s.close();
s = openSession();
s.getTransaction().begin();
p = s.get( Parent.class, parentId );
assertFalse( Hibernate.isInitialized( p.getChildren() ) );
assertEquals( 3, p.getChildren().size() );
s.getTransaction().commit();
s.close();
// Add the other managed Child, merge and commit.
s = openSession();
s.getTransaction().begin();
p = s.get( Parent.class, parentId );
assertFalse( Hibernate.isInitialized( p.getChildren() ) );
// get the second Child so it is managed; add to collection
p.addChild( s.get( Child.class, c2.getId() ) );
// collection should still be uninitialized
assertFalse( Hibernate.isInitialized( p.getChildren() ) );
s.merge( p );
s.getTransaction().commit();
s.close();
s = openSession();
s.getTransaction().begin();
p = s.get( Parent.class, parentId );
assertFalse( Hibernate.isInitialized( p.getChildren() ) );
assertEquals( 4, p.getChildren().size() );
s.getTransaction().commit();
s.close();
}
@Test
@TestForIssue( jiraKey = "HHH-11209")
public void testMergeInitializedBagAndRemerge() {
Session s = openSession();
s.getTransaction().begin();
Parent p = s.get( Parent.class, parentId );
assertFalse( Hibernate.isInitialized( p.getChildren() ) );
// initialize
Hibernate.initialize( p.getChildren() );
assertTrue( Hibernate.isInitialized( p.getChildren() ) );
s.getTransaction().commit();
s.close();
s = openSession();
s.getTransaction().begin();
p = (Parent) s.merge( p );
Child c = new Child( "Zeke" );
c.setParent( p );
s.persist( c );
assertFalse( Hibernate.isInitialized( p.getChildren() ) );
p.getChildren().size();
p.getChildren().add( c );
s.getTransaction().commit();
s.close();
// Merge detached Parent with initialized children
s = openSession();
s.getTransaction().begin();
p = (Parent) s.merge( p );
// after merging, p#children will be uninitialized
assertFalse( Hibernate.isInitialized( p.getChildren() ) );
assertTrue( ( (AbstractPersistentCollection) p.getChildren() ).hasQueuedOperations() );
s.getTransaction().commit();
assertFalse( ( (AbstractPersistentCollection) p.getChildren() ).hasQueuedOperations() );
s.close();
// Merge detached Parent, now with uninitialized children no queued operations
s = openSession();
s.getTransaction().begin();
p = (Parent) s.merge( p );
assertFalse( Hibernate.isInitialized( p.getChildren() ) );
assertFalse( ( (AbstractPersistentCollection) p.getChildren() ).hasQueuedOperations() );
s.getTransaction().commit();
s.close();
}
@Entity(name = "Parent")
public static class Parent {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
// Don't need extra-lazy to delay add operations to a bag.
@OneToMany(mappedBy = "parent")
private List<Child> children = new ArrayList<Child>();
public Parent() {
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public List<Child> getChildren() {
return children;
}
public void addChild(Child child) {
children.add(child);
child.setParent(this);
}
}
@Entity(name = "Child")
public static class Child {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
@Column(nullable = false)
private String name;
@ManyToOne
private Parent parent;
public Child() {
}
public Child(String name) {
this.name = name;
}
public Long getId() {
return id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Parent getParent() {
return parent;
}
public void setParent(Parent parent) {
this.parent = parent;
}
@Override
public String toString() {
return "Child{" +
"id=" + id +
", name='" + name + '\'' +
'}';
}
@Override
public boolean equals(Object o) {
if ( this == o ) {
return true;
}
if ( o == null || getClass() != o.getClass() ) {
return false;
}
Child child = (Child) o;
return name.equals( child.name );
}
@Override
public int hashCode() {
return name.hashCode();
}
}
}

View File

@ -24,11 +24,13 @@ import org.junit.Test;
import org.hibernate.Hibernate; import org.hibernate.Hibernate;
import org.hibernate.Session; import org.hibernate.Session;
import org.hibernate.collection.internal.AbstractPersistentCollection;
import org.hibernate.testing.TestForIssue; import org.hibernate.testing.TestForIssue;
import org.hibernate.testing.junit4.BaseCoreFunctionalTestCase; import org.hibernate.testing.junit4.BaseCoreFunctionalTestCase;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
/** /**
* Tests delayed operations that are queued for a PersistentBag. The Bag does not have * Tests delayed operations that are queued for a PersistentBag. The Bag does not have
@ -123,7 +125,7 @@ public class BagDelayedOperationTest extends BaseCoreFunctionalTestCase {
assertFalse( Hibernate.isInitialized( p.getChildren() ) ); assertFalse( Hibernate.isInitialized( p.getChildren() ) );
p.addChild( c2 ); p.addChild( c2 );
assertFalse( Hibernate.isInitialized( p.getChildren() ) ); assertFalse( Hibernate.isInitialized( p.getChildren() ) );
s.merge( p ); p = (Parent) s.merge( p );
s.getTransaction().commit(); s.getTransaction().commit();
s.close(); s.close();
@ -236,6 +238,51 @@ public class BagDelayedOperationTest extends BaseCoreFunctionalTestCase {
s.close(); s.close();
} }
@Test
@TestForIssue( jiraKey = "HHH-11209")
public void testMergeInitializedBagAndRemerge() {
Session s = openSession();
s.getTransaction().begin();
Parent p = s.get( Parent.class, parentId );
assertFalse( Hibernate.isInitialized( p.getChildren() ) );
// initialize
Hibernate.initialize( p.getChildren() );
assertTrue( Hibernate.isInitialized( p.getChildren() ) );
s.getTransaction().commit();
s.close();
s = openSession();
s.getTransaction().begin();
p = (Parent) s.merge( p );
assertTrue( Hibernate.isInitialized( p.getChildren() ) );
Child c = new Child( "Zeke" );
c.setParent( p );
s.persist( c );
p.getChildren().size();
p.getChildren().add( c );
s.getTransaction().commit();
s.close();
// Merge detached Parent with initialized children
s = openSession();
s.getTransaction().begin();
p = (Parent) s.merge( p );
// after merging, p#children will be initialized
assertTrue( Hibernate.isInitialized( p.getChildren() ) );
assertFalse( ( (AbstractPersistentCollection) p.getChildren() ).hasQueuedOperations() );
s.getTransaction().commit();
s.close();
// Merge detached Parent
s = openSession();
s.getTransaction().begin();
p = (Parent) s.merge( p );
assertTrue( Hibernate.isInitialized( p.getChildren() ) );
assertFalse( ( (AbstractPersistentCollection) p.getChildren() ).hasQueuedOperations() );
s.getTransaction().commit();
s.close();
}
@Entity(name = "Parent") @Entity(name = "Parent")
public static class Parent { public static class Parent {

View File

@ -0,0 +1,387 @@
/*
* 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.test.collection.delayedOperation;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import javax.persistence.CascadeType;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.ManyToOne;
import javax.persistence.OneToMany;
import org.hibernate.Hibernate;
import org.hibernate.collection.internal.AbstractPersistentCollection;
import org.hibernate.internal.CoreMessageLogger;
import org.hibernate.type.CollectionType;
import org.hibernate.testing.TestForIssue;
import org.hibernate.testing.junit4.BaseCoreFunctionalTestCase;
import org.hibernate.testing.logger.LoggerInspectionRule;
import org.hibernate.testing.logger.Triggerable;
import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.jboss.logging.Logger;
import static org.hibernate.testing.transaction.TransactionUtil.doInHibernate;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
/**
* Tests merge of detached PersistentBag
*
* @author Gail Badner
*/
public class DetachedBagDelayedOperationTest extends BaseCoreFunctionalTestCase {
@Override
protected Class[] getAnnotatedClasses() {
return new Class[] {
Parent.class,
Child.class
};
}
@Rule
public LoggerInspectionRule logInspectionCollectionType = new LoggerInspectionRule(
Logger.getMessageLogger( CoreMessageLogger.class, CollectionType.class.getName() )
);
@Rule
public LoggerInspectionRule logInspectionAbstractPersistentCollection = new LoggerInspectionRule(
Logger.getMessageLogger( CoreMessageLogger.class, AbstractPersistentCollection.class.getName() )
);
private Triggerable triggerableIgnoreQueuedOperationsOnMerge;
private Triggerable triggerableQueuedOperationWhenAttachToSession;
private Triggerable triggerableQueuedOperationWhenDetachFromSession;
@Before
public void setup() {
Parent parent = new Parent();
parent.id = 1L;
Child child1 = new Child( "Sherman" );
Child child2 = new Child( "Yogi" );
parent.addChild( child1 );
parent.addChild( child2 );
doInHibernate(
this::sessionFactory, session -> {
session.persist( child1 );
session.persist( child2 );
session.persist( parent );
}
);
triggerableIgnoreQueuedOperationsOnMerge = logInspectionCollectionType.watchForLogMessages( "HHH000494" );
triggerableQueuedOperationWhenAttachToSession = logInspectionAbstractPersistentCollection.watchForLogMessages( "HHH000495" );
triggerableQueuedOperationWhenDetachFromSession = logInspectionAbstractPersistentCollection.watchForLogMessages( "HHH000496" );
resetTriggerables();
}
@After
public void cleanup() {
doInHibernate(
this::sessionFactory, session -> {
session.createQuery( "delete from Child" ).executeUpdate();
session.createQuery( "delete from Parent" ).executeUpdate();
}
);
}
@Test
@TestForIssue( jiraKey = "HHH-11209" )
public void testMergeDetachedCollectionWithQueuedOperations() {
final Parent pOriginal = doInHibernate(
this::sessionFactory, session -> {
Parent p = session.get( Parent.class, 1L );
assertFalse( Hibernate.isInitialized( p.getChildren() ) );
// initialize
Hibernate.initialize( p.getChildren() );
assertTrue( Hibernate.isInitialized( p.getChildren() ) );
return p;
}
);
final Parent pWithQueuedOperations = doInHibernate(
this::sessionFactory, session -> {
Parent p = (Parent) session.merge( pOriginal );
Child c = new Child( "Zeke" );
c.setParent( p );
session.persist( c );
assertFalse( Hibernate.isInitialized( p.getChildren() ) );
p.getChildren().add( c );
assertFalse( Hibernate.isInitialized( p.getChildren() ) );
assertTrue( ( (AbstractPersistentCollection) p.getChildren() ).hasQueuedOperations() );
checkTriggerablesNotTriggered();
session.detach( p );
assertTrue( triggerableQueuedOperationWhenDetachFromSession.wasTriggered() );
assertEquals(
"HHH000496: Detaching an uninitialized collection with queued operations from a session: [org.hibernate.test.collection.delayedOperation.DetachedBagDelayedOperationTest$Parent.children#1]",
triggerableQueuedOperationWhenDetachFromSession.triggerMessage()
);
triggerableQueuedOperationWhenDetachFromSession.reset();
// Make sure nothing else got triggered
checkTriggerablesNotTriggered();
return p;
}
);
checkTriggerablesNotTriggered();
assertTrue( ( (AbstractPersistentCollection) pWithQueuedOperations.getChildren() ).hasQueuedOperations() );
// Merge detached Parent with uninitialized collection with queued operations
doInHibernate(
this::sessionFactory, session -> {
checkTriggerablesNotTriggered();
assertFalse( triggerableIgnoreQueuedOperationsOnMerge.wasTriggered() );
Parent p = (Parent) session.merge( pWithQueuedOperations );
assertTrue( triggerableIgnoreQueuedOperationsOnMerge.wasTriggered() );
assertEquals(
"HHH000494: Attempt to merge an uninitialized collection with queued operations; queued operations will be ignored: [org.hibernate.test.collection.delayedOperation.DetachedBagDelayedOperationTest$Parent.children#1]",
triggerableIgnoreQueuedOperationsOnMerge.triggerMessage()
);
triggerableIgnoreQueuedOperationsOnMerge.reset();
assertFalse( Hibernate.isInitialized( p.getChildren() ) );
assertFalse( ( (AbstractPersistentCollection) p.getChildren() ).hasQueuedOperations() );
// When initialized, p.children will not include the new Child ("Zeke"),
// because that Child was flushed without a parent before being detached
// along with its parent.
Hibernate.initialize( p.getChildren() );
final Set<String> childNames = new HashSet<String>(
Arrays.asList( new String[] { "Yogi", "Sherman" } )
);
assertEquals( childNames.size(), p.getChildren().size() );
for ( Child child : p.getChildren() ) {
childNames.remove( child.getName() );
}
assertEquals( 0, childNames.size() );
}
);
checkTriggerablesNotTriggered();
}
@Test
@TestForIssue( jiraKey = "HHH-11209" )
public void testSaveOrUpdateDetachedCollectionWithQueuedOperations() {
final Parent pOriginal = doInHibernate(
this::sessionFactory, session -> {
Parent p = session.get( Parent.class, 1L );
assertFalse( Hibernate.isInitialized( p.getChildren() ) );
// initialize
Hibernate.initialize( p.getChildren() );
assertTrue( Hibernate.isInitialized( p.getChildren() ) );
return p;
}
);
final Parent pAfterDetachWithQueuedOperations = doInHibernate(
this::sessionFactory, session -> {
Parent p = (Parent) session.merge( pOriginal );
Child c = new Child( "Zeke" );
c.setParent( p );
session.persist( c );
assertFalse( Hibernate.isInitialized( p.getChildren() ) );
p.getChildren().add( c );
assertFalse( Hibernate.isInitialized( p.getChildren() ) );
assertTrue( ( (AbstractPersistentCollection) p.getChildren() ).hasQueuedOperations() );
checkTriggerablesNotTriggered();
session.detach( p );
assertTrue( triggerableQueuedOperationWhenDetachFromSession.wasTriggered() );
assertEquals(
"HHH000496: Detaching an uninitialized collection with queued operations from a session: [org.hibernate.test.collection.delayedOperation.DetachedBagDelayedOperationTest$Parent.children#1]",
triggerableQueuedOperationWhenDetachFromSession.triggerMessage()
);
triggerableQueuedOperationWhenDetachFromSession.reset();
// Make sure nothing else got triggered
checkTriggerablesNotTriggered();
return p;
}
);
checkTriggerablesNotTriggered();
assertTrue( ( (AbstractPersistentCollection) pAfterDetachWithQueuedOperations.getChildren() ).hasQueuedOperations() );
// Save detached Parent with uninitialized collection with queued operations
doInHibernate(
this::sessionFactory, session -> {
checkTriggerablesNotTriggered();
assertFalse( triggerableQueuedOperationWhenAttachToSession.wasTriggered() );
session.saveOrUpdate( pAfterDetachWithQueuedOperations );
assertTrue( triggerableQueuedOperationWhenAttachToSession.wasTriggered() );
assertEquals(
"HHH000495: Attaching an uninitialized collection with queued operations to a session: [org.hibernate.test.collection.delayedOperation.DetachedBagDelayedOperationTest$Parent.children#1]",
triggerableQueuedOperationWhenAttachToSession.triggerMessage()
);
triggerableQueuedOperationWhenAttachToSession.reset();
// Make sure nothing else got triggered
checkTriggerablesNotTriggered();
assertFalse( Hibernate.isInitialized( pAfterDetachWithQueuedOperations.getChildren() ) );
assertTrue( ( (AbstractPersistentCollection) pAfterDetachWithQueuedOperations.getChildren() ).hasQueuedOperations() );
// Queued operations will be executed when the collection is initialized,
// After initialization, the collection will contain the Child that was added as a
// queued operation before being detached above.
Hibernate.initialize( pAfterDetachWithQueuedOperations.getChildren() );
final Set<String> childNames = new HashSet<String>(
Arrays.asList( new String[] { "Yogi", "Sherman", "Zeke" } )
);
assertEquals( childNames.size(), pAfterDetachWithQueuedOperations.getChildren().size() );
for ( Child child : pAfterDetachWithQueuedOperations.getChildren() ) {
childNames.remove( child.getName() );
}
assertEquals( 0, childNames.size() );
}
);
checkTriggerablesNotTriggered();
}
private void resetTriggerables() {
triggerableIgnoreQueuedOperationsOnMerge.reset();
triggerableQueuedOperationWhenAttachToSession.reset();
triggerableQueuedOperationWhenDetachFromSession.reset();
}
private void checkTriggerablesNotTriggered() {
assertFalse( triggerableIgnoreQueuedOperationsOnMerge.wasTriggered() );
assertFalse( triggerableQueuedOperationWhenAttachToSession.wasTriggered() );
assertFalse( triggerableQueuedOperationWhenDetachFromSession.wasTriggered() );
}
@Entity(name = "Parent")
public static class Parent {
@Id
private Long id;
// Don't need extra-lazy to delay add operations to a bag.
@OneToMany(mappedBy = "parent", cascade = CascadeType.DETACH)
private List<Child> children ;
public Parent() {
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public List<Child> getChildren() {
return children;
}
public void addChild(Child child) {
if ( children == null ) {
children = new ArrayList<>();
}
children.add(child);
child.setParent(this);
}
}
@Entity(name = "Child")
public static class Child {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
@Column(nullable = false)
private String name;
@ManyToOne
private Parent parent;
public Child() {
}
public Child(String name) {
this.name = name;
}
public Long getId() {
return id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Parent getParent() {
return parent;
}
public void setParent(Parent parent) {
this.parent = parent;
}
@Override
public String toString() {
return "Child{" +
"id=" + id +
", name='" + name + '\'' +
'}';
}
@Override
public boolean equals(Object o) {
if ( this == o ) {
return true;
}
if ( o == null || getClass() != o.getClass() ) {
return false;
}
Child child = (Child) o;
return name.equals( child.name );
}
@Override
public int hashCode() {
return name.hashCode();
}
}
}