HHH-11328 : Persist of transient entity in derived ID that is already in merge process throws javax.persistence.EntityExistsException

(cherry picked from commit 54f3409b41)
This commit is contained in:
Gail Badner 2016-12-14 23:16:25 -08:00
parent 4abaa5cf68
commit 911d1a661d
2 changed files with 289 additions and 30 deletions

View File

@ -17,6 +17,7 @@ import org.hibernate.MappingException;
import org.hibernate.bytecode.enhance.spi.LazyPropertyInitializer;
import org.hibernate.bytecode.enhance.spi.interceptor.LazyAttributeLoadingInterceptor;
import org.hibernate.bytecode.spi.BytecodeEnhancementMetadata;
import org.hibernate.engine.internal.ForeignKeys;
import org.hibernate.engine.spi.EntityEntry;
import org.hibernate.engine.spi.EntityKey;
import org.hibernate.engine.spi.PersistenceContext;
@ -33,12 +34,14 @@ import org.hibernate.loader.PropertyPath;
import org.hibernate.mapping.Component;
import org.hibernate.mapping.PersistentClass;
import org.hibernate.mapping.Property;
import org.hibernate.persister.entity.EntityPersister;
import org.hibernate.property.access.spi.Getter;
import org.hibernate.property.access.spi.Setter;
import org.hibernate.proxy.HibernateProxy;
import org.hibernate.proxy.ProxyFactory;
import org.hibernate.tuple.Instantiator;
import org.hibernate.tuple.NonIdentifierAttribute;
import org.hibernate.type.AssociationType;
import org.hibernate.type.ComponentType;
import org.hibernate.type.CompositeType;
import org.hibernate.type.EntityType;
@ -352,7 +355,6 @@ public abstract class AbstractEntityTuplizer implements EntityTuplizer {
final Type[] subTypes = virtualIdComponent.getSubtypes();
final Type[] copierSubTypes = mappedIdentifierType.getSubtypes();
final Iterable<PersistEventListener> persistEventListeners = persistEventListeners( session );
final PersistenceContext persistenceContext = session.getPersistenceContext();
final int length = subTypes.length;
for ( int i = 0; i < length; i++ ) {
if ( propertyValues[i] == null ) {
@ -366,35 +368,12 @@ public abstract class AbstractEntityTuplizer implements EntityTuplizer {
"Deprecated version of getIdentifier (no session) was used but session was required"
);
}
final Object subId;
if ( HibernateProxy.class.isInstance( propertyValues[i] ) ) {
subId = ( (HibernateProxy) propertyValues[i] ).getHibernateLazyInitializer().getIdentifier();
}
else {
EntityEntry pcEntry = session.getPersistenceContext().getEntry( propertyValues[i] );
if ( pcEntry != null ) {
subId = pcEntry.getId();
}
else {
LOG.debug( "Performing implicit derived identity cascade" );
final PersistEvent event = new PersistEvent(
null,
propertyValues[i] = determineEntityIdPersistIfNecessary(
propertyValues[i],
(EventSource) session
(AssociationType) subTypes[i],
session,
persistEventListeners
);
for ( PersistEventListener listener : persistEventListeners ) {
listener.onPersist( event );
}
pcEntry = persistenceContext.getEntry( propertyValues[i] );
if ( pcEntry == null || pcEntry.getId() == null ) {
throw new HibernateException( "Unable to process implicit derived identity cascade" );
}
else {
subId = pcEntry.getId();
}
}
}
propertyValues[i] = subId;
}
}
mappedIdentifierType.setPropertyValues( id, propertyValues, entityMode );
@ -445,6 +424,75 @@ public abstract class AbstractEntityTuplizer implements EntityTuplizer {
.listeners();
}
private static Serializable determineEntityIdPersistIfNecessary(
Object entity,
AssociationType associationType,
SharedSessionContractImplementor session,
Iterable<PersistEventListener> persistEventListeners) {
if ( HibernateProxy.class.isInstance( entity ) ) {
// entity is a proxy, so we know it is not transient; just return ID from proxy
return ( (HibernateProxy) entity ).getHibernateLazyInitializer().getIdentifier();
}
else {
EntityEntry pcEntry = session.getPersistenceContext().getEntry( entity );
if ( pcEntry != null ) {
// entity managed; return ID.
return pcEntry.getId();
}
else {
final EntityPersister persister = session.getEntityPersister(
associationType.getAssociatedEntityName( session.getFactory() ),
entity
);
Serializable entityId = persister.getIdentifier( entity, session );
if ( entityId == null ) {
// entity is transient with no ID; we need to persist the entity to get the ID.
entityId = persistTransientEntity( entity, session, persistEventListeners );
}
else {
// entity has an ID.
final EntityKey entityKey = session.generateEntityKey( entityId, persister );
// if the entity is in the process of being merged, it may be stored in the
// PC already, but doesn't have an EntityEntry yet. If this is the case,
// then don't persist even if it is transient because doing so can lead
// to having 2 entities in the PC with the same ID (HHH-11328).
if ( session.getPersistenceContext().getEntity( entityKey ) == null &&
ForeignKeys.isTransient(
persister.getEntityName(),
entity,
null,
session
) ) {
// entity is transient and it is not in the PersistenceContext.
// entity needs to be persisted.
persistTransientEntity( entity, session, persistEventListeners );
}
}
return entityId;
}
}
}
private static Serializable persistTransientEntity(
Object entity,
SharedSessionContractImplementor session,
Iterable<PersistEventListener> persistEventListeners) {
LOG.debug( "Performing implicit derived identity cascade" );
final PersistEvent event = new PersistEvent(
null,
entity,
(EventSource) session
);
for ( PersistEventListener listener : persistEventListeners ) {
listener.onPersist( event );
}
final EntityEntry pcEntry = session.getPersistenceContext().getEntry( entity );
if ( pcEntry == null || pcEntry.getId() == null ) {
throw new HibernateException( "Unable to process implicit derived identity cascade" );
}
return pcEntry.getId();
}
@Override
public void resetIdentifier(Object entity, Serializable currentId, Object currentVersion) {
// 99% of the time the session is not needed. Its only needed for certain brain-dead

View File

@ -0,0 +1,211 @@
/*
* 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.annotations.derivedidentities.bidirectional;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import javax.persistence.CascadeType;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.IdClass;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.OneToMany;
import org.junit.After;
import org.junit.Test;
import org.hibernate.Session;
import org.hibernate.testing.TestForIssue;
import org.hibernate.testing.junit4.BaseCoreFunctionalTestCase;
import static org.junit.Assert.assertEquals;
public class CompositeIdDerivedIdWithIdClassTest extends BaseCoreFunctionalTestCase {
@Override
protected Class<?>[] getAnnotatedClasses() {
return new Class<?>[] {
ShoppingCart.class,
LineItem.class
};
}
@After
public void cleanup() {
Session s = openSession();
s.getTransaction().begin();
s.createQuery( "delete from LineItem" ).executeUpdate();
s.createQuery( "delete from Cart" ).executeUpdate();
s.getTransaction().commit();
s.close();
}
@Test
@TestForIssue(jiraKey = "HHH-11328")
public void testMergeTransientIdManyToOne() throws Exception {
ShoppingCart transientCart = new ShoppingCart( "cart1" );
transientCart.addLineItem( new LineItem( 0, "description2", transientCart ) );
// merge ID with transient many-to-one
Session s = openSession();
s.getTransaction().begin();
s.merge( transientCart );
s.getTransaction().commit();
s.close();
s = openSession();
s.getTransaction().begin();
ShoppingCart updatedCart = s.get( ShoppingCart.class, "cart1" );
assertEquals( 1, updatedCart.getLineItems().size() );
assertEquals( "description2", updatedCart.getLineItems().get( 0 ).getDescription() );
s.getTransaction().commit();
s.close();
}
@Test
@TestForIssue(jiraKey = "HHH-10623")
public void testMergeDetachedIdManyToOne() throws Exception {
ShoppingCart cart = new ShoppingCart("cart1");
Session s = openSession();
s.getTransaction().begin();
s.persist( cart );
s.getTransaction().commit();
s.close();
// cart is detached now
LineItem lineItem = new LineItem( 0, "description2", cart );
cart.addLineItem( lineItem );
// merge lineItem with an ID with detached many-to-one
s = openSession();
s.getTransaction().begin();
s.merge(lineItem);
s.getTransaction().commit();
s.close();
s = openSession();
s.getTransaction().begin();
ShoppingCart updatedCart = s.get( ShoppingCart.class, "cart1" );
assertEquals( 1, updatedCart.getLineItems().size() );
assertEquals("description2", updatedCart.getLineItems().get( 0 ).getDescription());
s.getTransaction().commit();
s.close();
}
@Entity(name = "Cart")
public static class ShoppingCart {
@Id
@Column(name = "id", nullable = false)
private String id;
@OneToMany(mappedBy = "cart", cascade = CascadeType.ALL, orphanRemoval = true)
private List<LineItem> lineItems = new ArrayList<>();
protected ShoppingCart() {
}
public ShoppingCart(String id) {
this.id = id;
}
public List<LineItem> getLineItems() {
return lineItems;
}
public void addLineItem(LineItem lineItem) {
lineItems.add(lineItem);
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
ShoppingCart that = (ShoppingCart) o;
return Objects.equals( id, that.id );
}
@Override
public int hashCode() {
return Objects.hash(id);
}
}
@Entity(name = "LineItem")
@IdClass(LineItem.Pk.class)
public static class LineItem {
@Id
@Column(name = "item_seq_number", nullable = false)
private Integer sequenceNumber;
@Column(name = "description")
private String description;
@Id
@ManyToOne
@JoinColumn(name = "cart_id")
private ShoppingCart cart;
protected LineItem() {
}
public LineItem(Integer sequenceNumber, String description, ShoppingCart cart) {
this.sequenceNumber = sequenceNumber;
this.description = description;
this.cart = cart;
}
public Integer getSequenceNumber() {
return sequenceNumber;
}
public ShoppingCart getCart() {
return cart;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof LineItem)) return false;
LineItem lineItem = (LineItem) o;
return Objects.equals(getSequenceNumber(), lineItem.getSequenceNumber()) &&
Objects.equals(getCart(), lineItem.getCart());
}
@Override
public int hashCode() {
return Objects.hash( getSequenceNumber(), getCart() );
}
public String getDescription() {
return description;
}
public static class Pk implements Serializable {
public Integer sequenceNumber;
public String cart;
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Pk)) return false;
Pk pk = (Pk) o;
return Objects.equals(sequenceNumber, pk.sequenceNumber) &&
Objects.equals(cart, pk.cart);
}
@Override
public int hashCode() {
return Objects.hash(sequenceNumber, cart);
}
}
}
}