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.LazyPropertyInitializer;
|
||||||
import org.hibernate.bytecode.enhance.spi.interceptor.LazyAttributeLoadingInterceptor;
|
import org.hibernate.bytecode.enhance.spi.interceptor.LazyAttributeLoadingInterceptor;
|
||||||
import org.hibernate.bytecode.spi.BytecodeEnhancementMetadata;
|
import org.hibernate.bytecode.spi.BytecodeEnhancementMetadata;
|
||||||
|
import org.hibernate.engine.internal.ForeignKeys;
|
||||||
import org.hibernate.engine.spi.EntityEntry;
|
import org.hibernate.engine.spi.EntityEntry;
|
||||||
import org.hibernate.engine.spi.EntityKey;
|
import org.hibernate.engine.spi.EntityKey;
|
||||||
import org.hibernate.engine.spi.PersistenceContext;
|
import org.hibernate.engine.spi.PersistenceContext;
|
||||||
|
@ -33,12 +34,14 @@ import org.hibernate.loader.PropertyPath;
|
||||||
import org.hibernate.mapping.Component;
|
import org.hibernate.mapping.Component;
|
||||||
import org.hibernate.mapping.PersistentClass;
|
import org.hibernate.mapping.PersistentClass;
|
||||||
import org.hibernate.mapping.Property;
|
import org.hibernate.mapping.Property;
|
||||||
|
import org.hibernate.persister.entity.EntityPersister;
|
||||||
import org.hibernate.property.access.spi.Getter;
|
import org.hibernate.property.access.spi.Getter;
|
||||||
import org.hibernate.property.access.spi.Setter;
|
import org.hibernate.property.access.spi.Setter;
|
||||||
import org.hibernate.proxy.HibernateProxy;
|
import org.hibernate.proxy.HibernateProxy;
|
||||||
import org.hibernate.proxy.ProxyFactory;
|
import org.hibernate.proxy.ProxyFactory;
|
||||||
import org.hibernate.tuple.Instantiator;
|
import org.hibernate.tuple.Instantiator;
|
||||||
import org.hibernate.tuple.NonIdentifierAttribute;
|
import org.hibernate.tuple.NonIdentifierAttribute;
|
||||||
|
import org.hibernate.type.AssociationType;
|
||||||
import org.hibernate.type.ComponentType;
|
import org.hibernate.type.ComponentType;
|
||||||
import org.hibernate.type.CompositeType;
|
import org.hibernate.type.CompositeType;
|
||||||
import org.hibernate.type.EntityType;
|
import org.hibernate.type.EntityType;
|
||||||
|
@ -352,7 +355,6 @@ public abstract class AbstractEntityTuplizer implements EntityTuplizer {
|
||||||
final Type[] subTypes = virtualIdComponent.getSubtypes();
|
final Type[] subTypes = virtualIdComponent.getSubtypes();
|
||||||
final Type[] copierSubTypes = mappedIdentifierType.getSubtypes();
|
final Type[] copierSubTypes = mappedIdentifierType.getSubtypes();
|
||||||
final Iterable<PersistEventListener> persistEventListeners = persistEventListeners( session );
|
final Iterable<PersistEventListener> persistEventListeners = persistEventListeners( session );
|
||||||
final PersistenceContext persistenceContext = session.getPersistenceContext();
|
|
||||||
final int length = subTypes.length;
|
final int length = subTypes.length;
|
||||||
for ( int i = 0; i < length; i++ ) {
|
for ( int i = 0; i < length; i++ ) {
|
||||||
if ( propertyValues[i] == null ) {
|
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"
|
"Deprecated version of getIdentifier (no session) was used but session was required"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
final Object subId;
|
propertyValues[i] = determineEntityIdPersistIfNecessary(
|
||||||
if ( HibernateProxy.class.isInstance( propertyValues[i] ) ) {
|
propertyValues[i],
|
||||||
subId = ( (HibernateProxy) propertyValues[i] ).getHibernateLazyInitializer().getIdentifier();
|
(AssociationType) subTypes[i],
|
||||||
}
|
session,
|
||||||
else {
|
persistEventListeners
|
||||||
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],
|
|
||||||
(EventSource) session
|
|
||||||
);
|
|
||||||
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 );
|
mappedIdentifierType.setPropertyValues( id, propertyValues, entityMode );
|
||||||
|
@ -445,6 +424,75 @@ public abstract class AbstractEntityTuplizer implements EntityTuplizer {
|
||||||
.listeners();
|
.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
|
@Override
|
||||||
public void resetIdentifier(Object entity, Serializable currentId, Object currentVersion) {
|
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
|
// 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