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
(cherry picked from commit 6f5b1e5543
)
This commit is contained in:
parent
18ed6d93b8
commit
0c1ca11375
|
@ -57,8 +57,11 @@ public final class CollectionUpdateAction extends CollectionAction {
|
|||
preUpdate();
|
||||
|
||||
if ( !collection.wasInitialized() ) {
|
||||
if ( !collection.hasQueuedOperations() ) {
|
||||
throw new AssertionFailure( "no queued adds" );
|
||||
// If there were queued operations, they would have been processed
|
||||
// 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...
|
||||
}
|
||||
|
|
|
@ -9,7 +9,9 @@ package org.hibernate.action.internal;
|
|||
import java.io.Serializable;
|
||||
|
||||
import org.hibernate.HibernateException;
|
||||
import org.hibernate.collection.internal.AbstractPersistentCollection;
|
||||
import org.hibernate.collection.spi.PersistentCollection;
|
||||
import org.hibernate.engine.spi.CollectionEntry;
|
||||
import org.hibernate.engine.spi.SharedSessionContractImplementor;
|
||||
import org.hibernate.persister.collection.CollectionPersister;
|
||||
|
||||
|
@ -40,6 +42,21 @@ public final class QueuedOperationCollectionAction extends CollectionAction {
|
|||
|
||||
@Override
|
||||
public void execute() throws HibernateException {
|
||||
// this QueuedOperationCollectionAction has to be executed before any other
|
||||
// CollectionAction involving the same collection.
|
||||
|
||||
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() );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -497,6 +497,7 @@ public abstract class AbstractPersistentCollection implements Serializable, Pers
|
|||
for ( DelayedOperation operation : operationQueue ) {
|
||||
operation.operate();
|
||||
}
|
||||
clearOperationQueue();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -508,11 +509,15 @@ public abstract class AbstractPersistentCollection implements Serializable, Pers
|
|||
|
||||
@Override
|
||||
public void postAction() {
|
||||
operationQueue = null;
|
||||
clearOperationQueue();
|
||||
cachedSize = -1;
|
||||
clearDirty();
|
||||
}
|
||||
|
||||
public final void clearOperationQueue() {
|
||||
operationQueue = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getValue() {
|
||||
return this;
|
||||
|
@ -534,9 +539,8 @@ public abstract class AbstractPersistentCollection implements Serializable, Pers
|
|||
public boolean afterInitialize() {
|
||||
setInitialized();
|
||||
//do this bit after setting initialized to true or it will recurse
|
||||
if ( operationQueue != null ) {
|
||||
if ( hasQueuedOperations() ) {
|
||||
performQueuedOperations();
|
||||
operationQueue = null;
|
||||
cachedSize = -1;
|
||||
return false;
|
||||
}
|
||||
|
@ -605,6 +609,9 @@ public abstract class AbstractPersistentCollection implements Serializable, Pers
|
|||
prepareForPossibleLoadingOutsideTransaction();
|
||||
if ( currentSession == this.session ) {
|
||||
if ( !isTempSession ) {
|
||||
if ( hasQueuedOperations() ) {
|
||||
LOG.queuedOperationWhenDetachFromSession( MessageHelper.collectionInfoString( getRole(), getKey() ) );
|
||||
}
|
||||
this.session = null;
|
||||
}
|
||||
return true;
|
||||
|
@ -632,25 +639,22 @@ public abstract class AbstractPersistentCollection implements Serializable, Pers
|
|||
if ( session == this.session ) {
|
||||
return false;
|
||||
}
|
||||
else {
|
||||
if ( this.session != null ) {
|
||||
final String msg = generateUnexpectedSessionStateMessage( session );
|
||||
if ( isConnectedToSession() ) {
|
||||
throw new HibernateException(
|
||||
"Illegal attempt to associate a collection with two open sessions. " + msg
|
||||
);
|
||||
}
|
||||
else {
|
||||
LOG.logUnexpectedSessionInCollectionNotConnected( msg );
|
||||
this.session = session;
|
||||
return true;
|
||||
}
|
||||
else if ( this.session != null ) {
|
||||
final String msg = generateUnexpectedSessionStateMessage( session );
|
||||
if ( isConnectedToSession() ) {
|
||||
throw new HibernateException(
|
||||
"Illegal attempt to associate a collection with two open sessions. " + msg
|
||||
);
|
||||
}
|
||||
else {
|
||||
this.session = session;
|
||||
return true;
|
||||
LOG.logUnexpectedSessionInCollectionNotConnected( msg );
|
||||
}
|
||||
}
|
||||
if ( hasQueuedOperations() ) {
|
||||
LOG.queuedOperationWhenAttachToSession( MessageHelper.collectionInfoString( getRole(), getKey() ) );
|
||||
}
|
||||
this.session = session;
|
||||
return true;
|
||||
}
|
||||
|
||||
private String generateUnexpectedSessionStateMessage(SharedSessionContractImplementor session) {
|
||||
|
|
|
@ -1792,4 +1792,15 @@ public interface CoreMessageLogger extends BasicLogger {
|
|||
id = 487)
|
||||
void immutableEntityUpdateQuery(String sourceQuery, String querySpaces);
|
||||
|
||||
@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);
|
||||
}
|
||||
|
|
|
@ -452,7 +452,7 @@ public abstract class CollectionType extends AbstractType implements Association
|
|||
public Object resolve(Object value, SharedSessionContractImplementor session, Object owner)
|
||||
throws HibernateException {
|
||||
|
||||
return resolve(value, session, owner, null);
|
||||
return resolve( value, session, owner, null );
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -679,8 +679,23 @@ public abstract class CollectionType extends AbstractType implements Association
|
|||
}
|
||||
if ( !Hibernate.isInitialized( original ) ) {
|
||||
if ( ( (PersistentCollection) original ).hasQueuedOperations() ) {
|
||||
final AbstractPersistentCollection pc = (AbstractPersistentCollection) original;
|
||||
pc.replaceQueuedOperationValues( getPersister( session ), copyCache );
|
||||
if ( original == target ) {
|
||||
// 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;
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -24,11 +24,13 @@ 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
|
||||
|
@ -123,7 +125,7 @@ public class BagDelayedOperationTest extends BaseCoreFunctionalTestCase {
|
|||
assertFalse( Hibernate.isInitialized( p.getChildren() ) );
|
||||
p.addChild( c2 );
|
||||
assertFalse( Hibernate.isInitialized( p.getChildren() ) );
|
||||
s.merge( p );
|
||||
p = (Parent) s.merge( p );
|
||||
s.getTransaction().commit();
|
||||
s.close();
|
||||
|
||||
|
@ -236,6 +238,51 @@ public class BagDelayedOperationTest extends BaseCoreFunctionalTestCase {
|
|||
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")
|
||||
public static class Parent {
|
||||
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
Loading…
Reference in New Issue