HHH-8083 @OrderColumn not updated on @OneToMany cascade

This commit is contained in:
Brett Meyer 2013-05-20 17:05:41 -04:00 committed by Brett Meyer
parent 4ee980d9ff
commit f1f8600b54
14 changed files with 413 additions and 7 deletions

View File

@ -0,0 +1,74 @@
/*
* Hibernate, Relational Persistence for Idiomatic Java
*
* Copyright (c) 2008-2011, Red Hat Inc. or third-party contributors as
* indicated by the @author tags or express copyright attribution
* statements applied by the authors. All third-party contributions are
* distributed under license by Red Hat Inc.
*
* This copyrighted material is made available to anyone wishing to use, modify,
* copy, or redistribute it subject to the terms and conditions of the GNU
* Lesser General Public License, as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License
* for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this distribution; if not, write to:
* Free Software Foundation, Inc.
* 51 Franklin Street, Fifth Floor
* Boston, MA 02110-1301 USA
*/
package org.hibernate.action.internal;
import java.io.Serializable;
import org.hibernate.HibernateException;
import org.hibernate.collection.spi.PersistentCollection;
import org.hibernate.engine.spi.SessionImplementor;
import org.hibernate.persister.collection.CollectionPersister;
/**
* If a collection is extra lazy and has queued ops, we still need to
* process them. Ex: OneToManyPersister needs to insert indexes for List
* collections. See HHH-8083.
*
* @author Brett Meyer
*/
public final class QueuedOperationCollectionAction extends CollectionAction {
/**
* Constructs a CollectionUpdateAction
*
* @param collection The collection to update
* @param persister The collection persister
* @param id The collection key
* @param session The session
*/
public QueuedOperationCollectionAction(
final PersistentCollection collection,
final CollectionPersister persister,
final Serializable id,
final SessionImplementor session) {
super( persister, collection, id, session );
}
@Override
public void execute() throws HibernateException {
final Serializable id = getKey();
final SessionImplementor session = getSession();
final CollectionPersister persister = getPersister();
final PersistentCollection collection = getCollection();
persister.processQueuedOps( collection, id, session );
}
}

View File

@ -52,6 +52,7 @@ import org.hibernate.action.internal.EntityDeleteAction;
import org.hibernate.action.internal.EntityIdentityInsertAction;
import org.hibernate.action.internal.EntityInsertAction;
import org.hibernate.action.internal.EntityUpdateAction;
import org.hibernate.action.internal.QueuedOperationCollectionAction;
import org.hibernate.action.internal.UnresolvedEntityInsertActions;
import org.hibernate.action.spi.AfterTransactionCompletionProcess;
import org.hibernate.action.spi.BeforeTransactionCompletionProcess;
@ -63,7 +64,7 @@ import org.hibernate.type.Type;
/**
* Responsible for maintaining the queue of actions related to events.
* </p>
*
* The ActionQueue holds the DML operations queued as part of a session's
* transactional-write-behind semantics. DML operations are queued here
* until a flush forces them to be executed against the database.
@ -91,6 +92,7 @@ public class ActionQueue {
// just re-use the same Lists for convenience.
private ArrayList collectionCreations;
private ArrayList collectionUpdates;
private ArrayList collectionQueuedOps;
private ArrayList collectionRemovals;
private AfterTransactionCompletionProcessQueue afterTransactionProcesses;
@ -115,6 +117,7 @@ public class ActionQueue {
collectionCreations = new ArrayList( INIT_QUEUE_LIST_SIZE );
collectionRemovals = new ArrayList( INIT_QUEUE_LIST_SIZE );
collectionUpdates = new ArrayList( INIT_QUEUE_LIST_SIZE );
collectionQueuedOps = new ArrayList( INIT_QUEUE_LIST_SIZE );
afterTransactionProcesses = new AfterTransactionCompletionProcessQueue( session );
beforeTransactionProcesses = new BeforeTransactionCompletionProcessQueue( session );
@ -128,6 +131,7 @@ public class ActionQueue {
collectionCreations.clear();
collectionRemovals.clear();
collectionUpdates.clear();
collectionQueuedOps.clear();
unresolvedInsertions.clear();
}
@ -163,6 +167,11 @@ public class ActionQueue {
collectionUpdates.add( action );
}
@SuppressWarnings({ "unchecked" })
public void addAction(QueuedOperationCollectionAction action) {
collectionQueuedOps.add( action );
}
@SuppressWarnings({ "unchecked" })
public void addAction(EntityIdentityInsertAction insert) {
LOG.tracev( "Adding an EntityIdentityInsertAction for [{0}] object", insert.getEntityName() );
@ -276,6 +285,8 @@ public class ActionQueue {
}
executeActions( insertions );
executeActions( updates );
// do before actions are handled in the other collection queues
executeActions( collectionQueuedOps );
executeActions( collectionRemovals );
executeActions( collectionUpdates );
executeActions( collectionCreations );
@ -291,6 +302,7 @@ public class ActionQueue {
prepareActions( collectionRemovals );
prepareActions( collectionUpdates );
prepareActions( collectionCreations );
prepareActions( collectionQueuedOps );
}
/**
@ -325,6 +337,7 @@ public class ActionQueue {
areTablesToUpdated( deletions, tables ) ||
areTablesToUpdated( collectionUpdates, tables ) ||
areTablesToUpdated( collectionCreations, tables ) ||
areTablesToUpdated( collectionQueuedOps, tables ) ||
areTablesToUpdated( collectionRemovals, tables );
}
@ -401,6 +414,7 @@ public class ActionQueue {
.append( " collectionCreations=" ).append( collectionCreations )
.append( " collectionRemovals=" ).append( collectionRemovals )
.append( " collectionUpdates=" ).append( collectionUpdates )
.append( " collectionQueuedOps=" ).append( collectionQueuedOps )
.append( " unresolvedInsertDependencies=" ).append( unresolvedInsertions )
.append( "]" )
.toString();
@ -436,6 +450,7 @@ public class ActionQueue {
//sort the updates by fk
java.util.Collections.sort( collectionCreations );
java.util.Collections.sort( collectionUpdates );
java.util.Collections.sort( collectionQueuedOps );
java.util.Collections.sort( collectionRemovals );
}
}
@ -472,6 +487,7 @@ public class ActionQueue {
public void clearFromFlushNeededCheck(int previousCollectionRemovalSize) {
collectionCreations.clear();
collectionUpdates.clear();
collectionQueuedOps.clear();
updates.clear();
// collection deletions are a special case since update() can add
// deletions of collections not loaded by the session.
@ -495,6 +511,7 @@ public class ActionQueue {
! unresolvedInsertions.isEmpty() ||
deletions.size() > 0 ||
collectionUpdates.size() > 0 ||
collectionQueuedOps.size() > 0 ||
collectionRemovals.size() > 0 ||
collectionCreations.size() > 0;
}
@ -564,6 +581,13 @@ public class ActionQueue {
for ( int i = 0; i < queueSize; i++ ) {
oos.writeObject( collectionCreations.get( i ) );
}
queueSize = collectionQueuedOps.size();
LOG.tracev( "Starting serialization of [{0}] collectionQueuedOps entries", queueSize );
oos.writeInt( queueSize );
for ( int i = 0; i < queueSize; i++ ) {
oos.writeObject( collectionQueuedOps.get( i ) );
}
}
/**
@ -640,6 +664,15 @@ public class ActionQueue {
action.afterDeserialize( session );
rtn.collectionCreations.add( action );
}
queueSize = ois.readInt();
LOG.tracev( "Starting deserialization of [{0}] collectionQueuedOps entries", queueSize );
rtn.collectionQueuedOps = new ArrayList<Executable>( queueSize );
for ( int i = 0; i < queueSize; i++ ) {
CollectionAction action = ( CollectionAction ) ois.readObject();
action.afterDeserialize( session );
rtn.collectionQueuedOps.add( action );
}
return rtn;
}

View File

@ -32,6 +32,7 @@ import org.hibernate.HibernateException;
import org.hibernate.action.internal.CollectionRecreateAction;
import org.hibernate.action.internal.CollectionRemoveAction;
import org.hibernate.action.internal.CollectionUpdateAction;
import org.hibernate.action.internal.QueuedOperationCollectionAction;
import org.hibernate.collection.spi.PersistentCollection;
import org.hibernate.engine.internal.Cascade;
import org.hibernate.engine.internal.CascadePoint;
@ -294,6 +295,16 @@ public abstract class AbstractFlushingEventListener implements Serializable {
)
);
}
if ( !coll.wasInitialized() && coll.hasQueuedOperations() ) {
actionQueue.addAction(
new QueuedOperationCollectionAction(
coll,
ce.getLoadedPersister(),
ce.getLoadedKey(),
session
)
);
}
}

View File

@ -1643,6 +1643,16 @@ public abstract class AbstractCollectionPersister
protected abstract int doUpdateRows(Serializable key, PersistentCollection collection, SessionImplementor session)
throws HibernateException;
public void processQueuedOps(PersistentCollection collection, Serializable key, SessionImplementor session)
throws HibernateException {
if ( collection.hasQueuedOperations() ) {
doProcessQueuedOps( collection, key, session );
}
}
protected abstract void doProcessQueuedOps(PersistentCollection collection, Serializable key, SessionImplementor session)
throws HibernateException;
public CollectionMetadata getCollectionMetadata() {
return this;
}

View File

@ -153,6 +153,12 @@ public class BasicCollectionPersister extends AbstractCollectionPersister {
return update.toStatementString();
}
@Override
protected void doProcessQueuedOps(PersistentCollection collection, Serializable id, SessionImplementor session)
throws HibernateException {
// nothing to do
}
/**
* Generate the SQL DELETE that deletes a particular row
*/

View File

@ -199,6 +199,16 @@ public interface CollectionPersister extends CollectionDefinition {
Serializable key,
SessionImplementor session)
throws HibernateException;
/**
* Process queued operations within the PersistentCollection.
*/
public void processQueuedOps(
PersistentCollection collection,
Serializable key,
SessionImplementor session)
throws HibernateException;
/**
* Get the name of this collection role (the fully qualified class name,
* extended by a "property path")

View File

@ -181,21 +181,26 @@ public class OneToManyPersister extends AbstractCollectionPersister {
public void recreate(PersistentCollection collection, Serializable id, SessionImplementor session)
throws HibernateException {
super.recreate( collection, id, session );
writeIndex( collection, id, session );
writeIndex( collection, collection.entries( this ), id, session );
}
@Override
public void insertRows(PersistentCollection collection, Serializable id, SessionImplementor session)
throws HibernateException {
super.insertRows( collection, id, session );
writeIndex( collection, id, session );
writeIndex( collection, collection.entries( this ), id, session );
}
private void writeIndex(PersistentCollection collection, Serializable id, SessionImplementor session) {
@Override
protected void doProcessQueuedOps(PersistentCollection collection, Serializable id, SessionImplementor session)
throws HibernateException {
writeIndex( collection, collection.queuedAdditionIterator(), id, session );
}
private void writeIndex(PersistentCollection collection, Iterator entries, Serializable id, SessionImplementor session) {
// If one-to-many and inverse, still need to create the index. See HHH-5732.
if ( isInverse && hasIndex && !indexContainsFormula ) {
try {
Iterator entries = collection.entries( this );
if ( entries.hasNext() ) {
Expectation expectation = Expectations.appropriateExpectation( getUpdateCheckStyle() );
int i = 0;

View File

@ -0,0 +1,67 @@
package org.hibernate.test.annotations.onetomany;
import javax.persistence.Column;
import javax.persistence.DiscriminatorColumn;
import javax.persistence.DiscriminatorType;
import javax.persistence.DiscriminatorValue;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Inheritance;
import javax.persistence.InheritanceType;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
@Entity(name="Comment")
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(name = "DTYPE", discriminatorType= DiscriminatorType.STRING, length = 3)
@DiscriminatorValue(value = "WPT")
public class Comment {
private Long id;
private Post post;
private String name;
private Forum forum;
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
@Column(name = "id", updatable = false, insertable = false)
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
@ManyToOne(optional=true,fetch=FetchType.LAZY)
@JoinColumn(name="FK_PostId", nullable=true, insertable=true,updatable=false)
public Post getPost() {
return post;
}
public void setPost(Post family) {
this.post = family;
}
@ManyToOne(optional=true,fetch=FetchType.LAZY)
@JoinColumn(name="FK_ForumId", nullable=true, insertable=true,updatable=false)
public Forum getForum() {
return forum;
}
public void setForum(Forum forum) {
this.forum = forum;
}
@Column
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}

View File

@ -0,0 +1,68 @@
package org.hibernate.test.annotations.onetomany;
import java.util.ArrayList;
import java.util.List;
import javax.persistence.CascadeType;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.OneToMany;
import javax.persistence.OrderColumn;
import org.hibernate.annotations.LazyCollection;
import org.hibernate.annotations.LazyCollectionOption;
@Entity(name="Forum")
public class Forum{
private Long id;
private String name;
protected List<Comment> posts = new ArrayList<Comment>();
protected List<User> users = new ArrayList<User>();
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
@Column(name = "id", updatable = false, insertable = false)
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
@OneToMany(mappedBy = "forum", cascade = CascadeType.ALL , orphanRemoval = false, fetch = FetchType.LAZY)
@LazyCollection(LazyCollectionOption.EXTRA)
@OrderColumn(name = "idx2")
public List<Comment> getPosts() {
return posts;
}
public void setPosts(List<Comment> children) {
this.posts = children;
}
@OneToMany(mappedBy = "forum", cascade = CascadeType.ALL , orphanRemoval = true, fetch = FetchType.LAZY)
@LazyCollection(LazyCollectionOption.EXTRA)
@OrderColumn(name = "idx3")
public List<User> getUsers() {
return users;
}
public void setUsers(List<User> users) {
this.users = users;
}
@Column
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}

View File

@ -366,12 +366,53 @@ public class OrderByTest extends BaseCoreFunctionalTestCase {
}
}
@Test
@TestForIssue( jiraKey = "HHH-8083" )
public void testInverseIndexCascaded() {
final Session s = openSession();
s.getTransaction().begin();
Forum forum = new Forum();
forum.setName( "forum1" );
forum = (Forum) s.merge( forum );
s.flush();
s.clear();
sessionFactory().getCache().evictEntityRegions();
forum = (Forum) s.get( Forum.class, forum.getId() );
final Post post = new Post();
post.setName( "post1" );
post.setForum( forum );
forum.getPosts().add( post );
final User user = new User();
user.setName( "john" );
user.setForum( forum );
forum.getUsers().add( user );
forum = (Forum) s.merge( forum );
s.flush();
s.clear();
sessionFactory().getCache().evictEntityRegions();
forum = (Forum) s.get( Forum.class, forum.getId() );
assertEquals( 1, forum.getPosts().size() );
assertEquals( "post1", forum.getPosts().get( 0 ).getName() );
assertEquals( 1, forum.getUsers().size() );
assertEquals( "john", forum.getUsers().get( 0 ).getName() );
}
@Override
protected Class[] getAnnotatedClasses() {
return new Class[] {
Order.class, OrderItem.class, Zoo.class, Tiger.class,
Monkey.class, Visitor.class, Box.class, Item.class,
BankAccount.class, Transaction.class
BankAccount.class, Transaction.class,
Comment.class, Forum.class, Post.class, User.class
};
}
}

View File

@ -0,0 +1,32 @@
package org.hibernate.test.annotations.onetomany;
import java.util.ArrayList;
import java.util.List;
import javax.persistence.CascadeType;
import javax.persistence.DiscriminatorValue;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.OneToMany;
import javax.persistence.OrderColumn;
import org.hibernate.annotations.LazyCollection;
import org.hibernate.annotations.LazyCollectionOption;
@Entity(name = "Post")
@DiscriminatorValue(value = "WCT")
public class Post extends Comment{
protected List<Comment> comments = new ArrayList<Comment>();
@OneToMany(mappedBy = "post", cascade = CascadeType.ALL , orphanRemoval = false, fetch = FetchType.LAZY)
@LazyCollection(LazyCollectionOption.EXTRA)
@OrderColumn(name = "idx")
public List<Comment> getComments() {
return comments;
}
public void setComments(List<Comment> comments) {
this.comments = comments;
}
}

View File

@ -0,0 +1,44 @@
package org.hibernate.test.annotations.onetomany;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
@Entity(name="Userx")
public class User {
private Long id;
private String name;
private Forum forum;
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
@Column(name = "id", updatable = false, insertable = false)
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
@Column
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@ManyToOne(optional=false,fetch=FetchType.LAZY)
@JoinColumn(name="FK_ForumId", nullable=false, insertable=true,updatable=false)
public Forum getForum() {
return forum;
}
public void setForum(Forum forum) {
this.forum = forum;
}
}

View File

@ -849,5 +849,10 @@ public class GoofyPersisterClassProvider implements PersisterClassResolver {
public int getBatchSize() {
return 0;
}
@Override
public void processQueuedOps(PersistentCollection collection, Serializable key, SessionImplementor session)
throws HibernateException {
}
}
}