diff --git a/hibernate-core/src/main/java/org/hibernate/event/internal/DefaultMergeEventListener.java b/hibernate-core/src/main/java/org/hibernate/event/internal/DefaultMergeEventListener.java index 452abc16e9b7..3dbcb8dc1174 100644 --- a/hibernate-core/src/main/java/org/hibernate/event/internal/DefaultMergeEventListener.java +++ b/hibernate-core/src/main/java/org/hibernate/event/internal/DefaultMergeEventListener.java @@ -164,7 +164,7 @@ private void merge(MergeEvent event, MergeContext copiedAlready, Object entity) final Object originalId; if ( entry == null ) { final EntityPersister persister = source.getEntityPersister( event.getEntityName(), entity ); - originalId = persister.getIdentifier( entity, source ); + originalId = persister.getIdentifier( entity, copiedAlready ); if ( originalId != null ) { final EntityKey entityKey; if ( persister.getIdentifierType() instanceof ComponentType ) { diff --git a/hibernate-core/src/main/java/org/hibernate/event/spi/MergeContext.java b/hibernate-core/src/main/java/org/hibernate/event/spi/MergeContext.java index c9a83e3e45ab..17cf332e617a 100644 --- a/hibernate-core/src/main/java/org/hibernate/event/spi/MergeContext.java +++ b/hibernate-core/src/main/java/org/hibernate/event/spi/MergeContext.java @@ -366,4 +366,8 @@ private String printEntity(Object entity) { // Entity was not found in current persistence context. Use Object#toString() method. return "[" + entity + "]"; } + + public EventSource getEventSource() { + return session; + } } diff --git a/hibernate-core/src/main/java/org/hibernate/internal/util/EntityPrinter.java b/hibernate-core/src/main/java/org/hibernate/internal/util/EntityPrinter.java index 12ccc44062e5..78618d829302 100644 --- a/hibernate-core/src/main/java/org/hibernate/internal/util/EntityPrinter.java +++ b/hibernate-core/src/main/java/org/hibernate/internal/util/EntityPrinter.java @@ -15,6 +15,7 @@ import org.hibernate.engine.spi.EntityHolder; import org.hibernate.engine.spi.EntityKey; import org.hibernate.engine.spi.SessionFactoryImplementor; +import org.hibernate.engine.spi.SharedSessionContractImplementor; import org.hibernate.engine.spi.TypedValue; import org.hibernate.internal.CoreLogging; import org.hibernate.internal.CoreMessageLogger; @@ -53,7 +54,7 @@ public String toString(String entityName, Object entity) throws HibernateExcepti result.put( entityPersister.getIdentifierPropertyName(), entityPersister.getIdentifierType().toLoggableString( - entityPersister.getIdentifier( entity, null ), + entityPersister.getIdentifier( entity, (SharedSessionContractImplementor) null ), factory ) ); diff --git a/hibernate-core/src/main/java/org/hibernate/jpa/internal/PersistenceUnitUtilImpl.java b/hibernate-core/src/main/java/org/hibernate/jpa/internal/PersistenceUnitUtilImpl.java index 2cfbe054cdf8..b3572934bcd7 100644 --- a/hibernate-core/src/main/java/org/hibernate/jpa/internal/PersistenceUnitUtilImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/jpa/internal/PersistenceUnitUtilImpl.java @@ -13,6 +13,7 @@ import org.hibernate.MappingException; import org.hibernate.engine.spi.EntityEntry; import org.hibernate.engine.spi.SessionFactoryImplementor; +import org.hibernate.engine.spi.SharedSessionContractImplementor; import org.hibernate.jpa.internal.util.PersistenceUtilHelper; import org.hibernate.persister.entity.EntityPersister; import org.hibernate.proxy.LazyInitializer; @@ -106,7 +107,7 @@ private Object getIdentifierFromPersister(Object entity) { catch (MappingException ex) { throw new IllegalArgumentException( entityClass.getName() + " is not an entity", ex ); } - return persister.getIdentifier( entity, null ); + return persister.getIdentifier( entity, (SharedSessionContractImplementor) null ); } } diff --git a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/EntityIdentifierMapping.java b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/EntityIdentifierMapping.java index f6612255e8aa..92ea4430b7f8 100644 --- a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/EntityIdentifierMapping.java +++ b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/EntityIdentifierMapping.java @@ -10,6 +10,7 @@ import org.hibernate.engine.internal.ForeignKeys; import org.hibernate.engine.spi.IdentifierValue; import org.hibernate.engine.spi.SharedSessionContractImplementor; +import org.hibernate.event.spi.MergeContext; import jakarta.persistence.EmbeddedId; import jakarta.persistence.Id; @@ -68,6 +69,15 @@ default String getPartName() { */ Object getIdentifier(Object entity); + /** + * Extract the identifier from an instance of the entity + * + * It's supposed to be use during the merging process + */ + default Object getIdentifier(Object entity, MergeContext mergeContext){ + return getIdentifier( entity ); + } + /** * Return the identifier of the persistent or transient object, or throw * an exception if the instance is "unsaved" diff --git a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/InverseNonAggregatedIdentifierMapping.java b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/InverseNonAggregatedIdentifierMapping.java index 5e47f34d21a8..46589709d974 100644 --- a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/InverseNonAggregatedIdentifierMapping.java +++ b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/InverseNonAggregatedIdentifierMapping.java @@ -14,6 +14,7 @@ import org.hibernate.engine.spi.EntityKey; import org.hibernate.engine.spi.PersistenceContext; import org.hibernate.engine.spi.SharedSessionContractImplementor; +import org.hibernate.event.spi.MergeContext; import org.hibernate.internal.util.collections.CollectionHelper; import org.hibernate.metamodel.mapping.AttributeMapping; import org.hibernate.metamodel.mapping.EmbeddableMappingType; @@ -196,6 +197,11 @@ public SqlTuple toSqlExpression( @Override public Object getIdentifier(Object entity) { + return getIdentifier( entity, null ); + } + + @Override + public Object getIdentifier(Object entity, MergeContext mergeContext) { if ( hasContainingClass() ) { final Object id = identifierValueMapper.getRepresentationStrategy().getInstantiator().instantiate( null, @@ -218,16 +224,17 @@ public Object getIdentifier(Object entity) { //JPA 2 @MapsId + @IdClass points to the pk of the entity else if ( attributeMapping instanceof ToOneAttributeMapping && !( identifierValueMapper.getAttributeMapping( i ) instanceof ToOneAttributeMapping ) ) { + final Object toOne = getIfMerged( o, mergeContext ); final ToOneAttributeMapping toOneAttributeMapping = (ToOneAttributeMapping) attributeMapping; final ModelPart targetPart = toOneAttributeMapping.getForeignKeyDescriptor().getPart( toOneAttributeMapping.getSideNature().inverse() ); if ( targetPart.isEntityIdentifierMapping() ) { - propertyValues[i] = ( (EntityIdentifierMapping) targetPart ).getIdentifier( o ); + propertyValues[i] = ( (EntityIdentifierMapping) targetPart ) + .getIdentifier( toOne, mergeContext ); } else { - propertyValues[i] = o; - assert false; + propertyValues[i] = toOne; } } else { @@ -242,6 +249,16 @@ else if ( attributeMapping instanceof ToOneAttributeMapping } } + private static Object getIfMerged(Object o, MergeContext mergeContext) { + if ( mergeContext != null ) { + final Object merged = mergeContext.get( o ); + if ( merged != null ) { + return merged; + } + } + return o; + } + @Override public void setIdentifier(Object entity, Object id, SharedSessionContractImplementor session) { final Object[] propertyValues = new Object[identifierValueMapper.getNumberOfAttributeMappings()]; diff --git a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/NonAggregatedIdentifierMappingImpl.java b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/NonAggregatedIdentifierMappingImpl.java index 54871a77f99e..243fb06e6faa 100644 --- a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/NonAggregatedIdentifierMappingImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/NonAggregatedIdentifierMappingImpl.java @@ -15,6 +15,7 @@ import org.hibernate.engine.spi.EntityKey; import org.hibernate.engine.spi.PersistenceContext; import org.hibernate.engine.spi.SharedSessionContractImplementor; +import org.hibernate.event.spi.MergeContext; import org.hibernate.internal.util.collections.CollectionHelper; import org.hibernate.mapping.Component; import org.hibernate.mapping.RootClass; @@ -237,6 +238,11 @@ public String getAttributeName() { @Override public Object getIdentifier(Object entity) { + return getIdentifier( entity, null ); + } + + @Override + public Object getIdentifier(Object entity, MergeContext mergeContext) { if ( hasContainingClass() ) { final LazyInitializer lazyInitializer = HibernateProxy.extractLazyInitializer( entity ); if ( lazyInitializer != null ) { @@ -259,16 +265,16 @@ public Object getIdentifier(Object entity) { //JPA 2 @MapsId + @IdClass points to the pk of the entity else if ( attributeMapping instanceof ToOneAttributeMapping && !( identifierValueMapper.getAttributeMapping( i ) instanceof ToOneAttributeMapping ) ) { + final Object toOne = getIfMerged( o, mergeContext ); final ToOneAttributeMapping toOneAttributeMapping = (ToOneAttributeMapping) attributeMapping; final ModelPart targetPart = toOneAttributeMapping.getForeignKeyDescriptor().getPart( toOneAttributeMapping.getSideNature().inverse() ); if ( targetPart.isEntityIdentifierMapping() ) { - propertyValues[i] = ( (EntityIdentifierMapping) targetPart ).getIdentifier( o ); + propertyValues[i] = ( (EntityIdentifierMapping) targetPart ).getIdentifier( toOne, mergeContext ); } else { - propertyValues[i] = o; - assert false; + propertyValues[i] = toOne; } } else { @@ -285,6 +291,16 @@ else if ( attributeMapping instanceof ToOneAttributeMapping } } + private static Object getIfMerged(Object o, MergeContext mergeContext) { + if ( mergeContext != null ) { + final Object merged = mergeContext.get( o ); + if ( merged != null ) { + return merged; + } + } + return o; + } + @Override public void setIdentifier(Object entity, Object id, SharedSessionContractImplementor session) { final Object[] propertyValues = new Object[identifierValueMapper.getNumberOfAttributeMappings()]; diff --git a/hibernate-core/src/main/java/org/hibernate/persister/entity/AbstractEntityPersister.java b/hibernate-core/src/main/java/org/hibernate/persister/entity/AbstractEntityPersister.java index 5d5265d169ca..4e9a79f9ef2d 100644 --- a/hibernate-core/src/main/java/org/hibernate/persister/entity/AbstractEntityPersister.java +++ b/hibernate-core/src/main/java/org/hibernate/persister/entity/AbstractEntityPersister.java @@ -100,6 +100,7 @@ import org.hibernate.engine.spi.SharedSessionContractImplementor; import org.hibernate.event.spi.EventSource; import org.hibernate.event.spi.LoadEvent; +import org.hibernate.event.spi.MergeContext; import org.hibernate.generator.BeforeExecutionGenerator; import org.hibernate.generator.EventType; import org.hibernate.generator.Generator; @@ -4724,6 +4725,11 @@ public Object getIdentifier(Object entity, SharedSessionContractImplementor sess return identifierMapping.getIdentifier( entity ); } + @Override + public Object getIdentifier(Object entity, MergeContext mergeContext) { + return identifierMapping.getIdentifier( entity, mergeContext ); + } + @Override public void setIdentifier(Object entity, Object id, SharedSessionContractImplementor session) { identifierMapping.setIdentifier( entity, id, session ); diff --git a/hibernate-core/src/main/java/org/hibernate/persister/entity/EntityPersister.java b/hibernate-core/src/main/java/org/hibernate/persister/entity/EntityPersister.java index a86e5f133dc5..05011c3b0a7a 100644 --- a/hibernate-core/src/main/java/org/hibernate/persister/entity/EntityPersister.java +++ b/hibernate-core/src/main/java/org/hibernate/persister/entity/EntityPersister.java @@ -30,6 +30,7 @@ import org.hibernate.engine.spi.SessionImplementor; import org.hibernate.engine.spi.SharedSessionContractImplementor; import org.hibernate.event.spi.EventSource; +import org.hibernate.event.spi.MergeContext; import org.hibernate.generator.BeforeExecutionGenerator; import org.hibernate.generator.EventType; import org.hibernate.generator.Generator; @@ -1130,8 +1131,18 @@ default Object getValue(Object object, int i) { */ Object getIdentifier(Object entity, SharedSessionContractImplementor session); - /** - * Inject the identifier value into the given entity. + /** + * Get the identifier of an instance from the object's identifier property. + * Throw an exception if it has no identifier property. + * + * It's supposed to be use during the merging process + */ + default Object getIdentifier(Object entity, MergeContext mergeContext) { + return getIdentifier( entity, mergeContext.getEventSource() ); + } + + /** + * Inject the identifier value into the given entity. */ void setIdentifier(Object entity, Object id, SharedSessionContractImplementor session); diff --git a/hibernate-core/src/main/java/org/hibernate/query/derived/AnonymousTupleBasicEntityIdentifierMapping.java b/hibernate-core/src/main/java/org/hibernate/query/derived/AnonymousTupleBasicEntityIdentifierMapping.java index d1f65dc0b858..f2ee788ff6d7 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/derived/AnonymousTupleBasicEntityIdentifierMapping.java +++ b/hibernate-core/src/main/java/org/hibernate/query/derived/AnonymousTupleBasicEntityIdentifierMapping.java @@ -9,6 +9,7 @@ import org.hibernate.Incubating; import org.hibernate.engine.spi.IdentifierValue; import org.hibernate.engine.spi.SharedSessionContractImplementor; +import org.hibernate.event.spi.MergeContext; import org.hibernate.metamodel.mapping.BasicEntityIdentifierMapping; import org.hibernate.metamodel.mapping.JdbcMapping; import org.hibernate.metamodel.mapping.ManagedMappingType; @@ -51,6 +52,11 @@ public Object getIdentifier(Object entity) { return delegate.getIdentifier( entity ); } + @Override + public Object getIdentifier(Object entity, MergeContext mergeContext) { + return delegate.getIdentifier( entity, mergeContext ); + } + @Override public void setIdentifier(Object entity, Object id, SharedSessionContractImplementor session) { delegate.setIdentifier( entity, id, session ); diff --git a/hibernate-core/src/main/java/org/hibernate/query/derived/AnonymousTupleEmbeddedEntityIdentifierMapping.java b/hibernate-core/src/main/java/org/hibernate/query/derived/AnonymousTupleEmbeddedEntityIdentifierMapping.java index 64be6083d4d5..abd8f49da889 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/derived/AnonymousTupleEmbeddedEntityIdentifierMapping.java +++ b/hibernate-core/src/main/java/org/hibernate/query/derived/AnonymousTupleEmbeddedEntityIdentifierMapping.java @@ -12,6 +12,7 @@ import org.hibernate.Incubating; import org.hibernate.engine.spi.IdentifierValue; import org.hibernate.engine.spi.SharedSessionContractImplementor; +import org.hibernate.event.spi.MergeContext; import org.hibernate.metamodel.mapping.CompositeIdentifierMapping; import org.hibernate.metamodel.mapping.EmbeddableMappingType; import org.hibernate.metamodel.mapping.internal.SingleAttributeIdentifierMapping; @@ -70,6 +71,11 @@ public Object getIdentifier(Object entity) { return delegate.getIdentifier( entity ); } + @Override + public Object getIdentifier(Object entity, MergeContext mergeContext) { + return delegate.getIdentifier( entity, mergeContext ); + } + @Override public void setIdentifier(Object entity, Object id, SharedSessionContractImplementor session) { delegate.setIdentifier( entity, id, session ); diff --git a/hibernate-core/src/main/java/org/hibernate/query/derived/AnonymousTupleNonAggregatedEntityIdentifierMapping.java b/hibernate-core/src/main/java/org/hibernate/query/derived/AnonymousTupleNonAggregatedEntityIdentifierMapping.java index beee4fecd2a7..c2d7db73fb9d 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/derived/AnonymousTupleNonAggregatedEntityIdentifierMapping.java +++ b/hibernate-core/src/main/java/org/hibernate/query/derived/AnonymousTupleNonAggregatedEntityIdentifierMapping.java @@ -14,6 +14,7 @@ import org.hibernate.engine.FetchTiming; import org.hibernate.engine.spi.IdentifierValue; import org.hibernate.engine.spi.SharedSessionContractImplementor; +import org.hibernate.event.spi.MergeContext; import org.hibernate.metamodel.mapping.EmbeddableMappingType; import org.hibernate.metamodel.mapping.NonAggregatedIdentifierMapping; import org.hibernate.metamodel.mapping.internal.IdClassEmbeddable; @@ -79,6 +80,12 @@ public Object getIdentifier(Object entity) { return delegate.getIdentifier( entity ); } + + @Override + public Object getIdentifier(Object entity, MergeContext mergeContext) { + return delegate.getIdentifier( entity, mergeContext ); + } + @Override public void setIdentifier(Object entity, Object id, SharedSessionContractImplementor session) { delegate.setIdentifier( entity, id, session ); diff --git a/hibernate-core/src/main/java/org/hibernate/type/AnyType.java b/hibernate-core/src/main/java/org/hibernate/type/AnyType.java index 67fc1cb09c55..f508d441daea 100644 --- a/hibernate-core/src/main/java/org/hibernate/type/AnyType.java +++ b/hibernate-core/src/main/java/org/hibernate/type/AnyType.java @@ -149,7 +149,7 @@ private Object extractIdentifier(Object entity, SessionFactoryImplementor factor final EntityPersister concretePersister = guessEntityPersister( entity, factory ); return concretePersister == null ? null - : concretePersister.getIdentifier( entity, null ); + : concretePersister.getIdentifier( entity, (SharedSessionContractImplementor) null ); } private EntityPersister guessEntityPersister(Object object, SessionFactoryImplementor factory) { diff --git a/hibernate-core/src/main/java/org/hibernate/type/EntityType.java b/hibernate-core/src/main/java/org/hibernate/type/EntityType.java index bc6cec2cdda7..342616914091 100644 --- a/hibernate-core/src/main/java/org/hibernate/type/EntityType.java +++ b/hibernate-core/src/main/java/org/hibernate/type/EntityType.java @@ -284,7 +284,7 @@ private Object extractIdentifier(Object entity, SessionFactoryImplementor factor final EntityPersister concretePersister = getAssociatedEntityPersister( factory ); return concretePersister == null ? null - : concretePersister.getIdentifier( entity, null ); + : concretePersister.getIdentifier( entity, (SharedSessionContractImplementor) null ); } @Override @@ -353,7 +353,7 @@ public int getHashCode(Object x, SessionFactoryImplementor factory) { else { final Class mappedClass = persister.getMappedClass(); if ( mappedClass.isAssignableFrom( x.getClass() ) ) { - id = persister.getIdentifier( x, null ); + id = persister.getIdentifier( x, (SharedSessionContractImplementor) null ); } else { id = x; @@ -387,7 +387,7 @@ public boolean isEqual(Object x, Object y, SessionFactoryImplementor factory) { } else { if ( mappedClass.isAssignableFrom( x.getClass() ) ) { - xid = persister.getIdentifier( x, null ); + xid = persister.getIdentifier( x, (SharedSessionContractImplementor) null ); } else { //JPA 2 case where @IdClass contains the id and not the associated entity @@ -402,7 +402,7 @@ public boolean isEqual(Object x, Object y, SessionFactoryImplementor factory) { } else { if ( mappedClass.isAssignableFrom( y.getClass() ) ) { - yid = persister.getIdentifier( y, null ); + yid = persister.getIdentifier( y, (SharedSessionContractImplementor) null ); } else { //JPA 2 case where @IdClass contains the id and not the associated entity @@ -564,7 +564,7 @@ public String toLoggableString(Object value, SessionFactoryImplementor factory) id = lazyInitializer.getInternalIdentifier(); } else { - id = persister.getIdentifier( value, null ); + id = persister.getIdentifier( value, (SharedSessionContractImplementor) null ); } result.append( '#' ) diff --git a/hibernate-core/src/test/java/org/hibernate/engine/internal/StatisticalLoggingSessionEventListenerTest.java b/hibernate-core/src/test/java/org/hibernate/engine/internal/StatisticalLoggingSessionEventListenerTest.java index c87ec5b837d0..e8c9065c691c 100644 --- a/hibernate-core/src/test/java/org/hibernate/engine/internal/StatisticalLoggingSessionEventListenerTest.java +++ b/hibernate-core/src/test/java/org/hibernate/engine/internal/StatisticalLoggingSessionEventListenerTest.java @@ -1,16 +1,12 @@ /* - * 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 + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors */ package org.hibernate.engine.internal; import java.util.List; import java.util.regex.Matcher; import java.util.regex.Pattern; -import java.util.stream.Collectors; -import java.util.stream.Stream; import org.hibernate.cfg.AvailableSettings; import org.hibernate.internal.CoreMessageLogger; @@ -183,7 +179,7 @@ void testSessionMetricsLog(SessionFactoryScope scope) { // Number of lines assertThat( sessionMetricsLog.lines().count() ) .as( "The StatisticalLoggingSessionEventListener should write a line per metric (" - + numberOfMetrics + " lines) plus a header and a footer (2 lines)" ) + + numberOfMetrics + " lines) plus a header and a footer (2 lines)" ) .isEqualTo( numberOfMetrics + 2 ); // Total time long sumDuration = metricList.stream().map( SessionMetric::getDuration ).mapToLong( Long::longValue ).sum(); diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/cid/CompositeIdAndMergeTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/cid/CompositeIdAndMergeTest.java new file mode 100644 index 000000000000..9dc2d41e5bd2 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/cid/CompositeIdAndMergeTest.java @@ -0,0 +1,162 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.cid; + +import java.util.ArrayList; +import java.util.List; + +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.JiraKey; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.junit.jupiter.api.Test; + +import jakarta.persistence.CascadeType; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.IdClass; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.OneToMany; +import jakarta.persistence.Table; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; + +@DomainModel( + annotatedClasses = { + CompositeIdAndMergeTest.Order.class, + CompositeIdAndMergeTest.Invoice.class, + CompositeIdAndMergeTest.LineItem.class + } +) +@SessionFactory +@JiraKey("HHH-18131") +public class CompositeIdAndMergeTest { + + @Test + public void testMerge(SessionFactoryScope scope) { + Integer lineItemIndex = 2; + Order persistedOrder = scope.fromTransaction( + session -> { + Order order = new Order( "order" ); + session.persist( order ); + + Invoice invoice = new Invoice( "invoice" ); + LineItem lineItem = new LineItem( lineItemIndex ); + invoice.addLine( lineItem ); + order.setInvoice( invoice ); + + session.merge( order ); + return order; + } + ); + + scope.inTransaction( + session -> { + Order order = session.find( Order.class, persistedOrder.getId() ); + Invoice invoice = order.getInvoice(); + assertThat( invoice ).isNotNull(); + List lines = invoice.getLines(); + assertThat( lines.size() ).isEqualTo( 1 ); + assertThat( lines.get( 0 ).getIndex() ).isEqualTo( lineItemIndex ); + } + ); + } + + @Entity(name = "Order") + @Table(name = "order_table") + public static class Order { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false) + private String description; + + @ManyToOne(cascade = { CascadeType.ALL }) + private Invoice invoice; + + public Order() { + } + + public Order(String description) { + this.description = description; + } + + public Long getId() { + return id; + } + + public Invoice getInvoice() { + return invoice; + } + + public void setInvoice(Invoice invoice) { + this.invoice = invoice; + } + } + + @Entity(name = "Invoice") + public static class Invoice { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(name = "number_column") + private String number; + + @OneToMany(mappedBy = "invoice", cascade = { CascadeType.ALL }, orphanRemoval = true) + private List lines = new ArrayList<>(); + + public Invoice() { + } + + public Invoice(String number) { + this.number = number; + } + + public void addLine(LineItem line) { + lines.add( line ); + line.invoice = this; + } + + public List getLines() { + return lines; + } + } + + @Entity + @Table(name = "invoice_lines") + @IdClass(LineItemId.class) + public static class LineItem { + @Id + @ManyToOne + private Invoice invoice; + + @Id + @Column(name = "index_column") + private Integer index; + + public LineItem() { + } + + public LineItem(Integer index) { + this.index = index; + } + + public Integer getIndex() { + return index; + } + } + + public static class LineItemId { + private Long invoice; + private Integer index; + + } + +}