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:
parent
4abaa5cf68
commit
911d1a661d
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue