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 4ccea081149f..dd1bd662065f 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 @@ -162,7 +162,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 4384a1d45f83..7189b8173fcb 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 @@ -364,4 +364,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 8eda904731a5..22d37d15b99e 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 @@ -51,7 +51,7 @@ public String toString(String entityName, Object entity) throws HibernateExcepti result.put( entityPersister.getIdentifierPropertyName(), entityPersister.getIdentifierType().toLoggableString( - entityPersister.getIdentifier( entity, null ), + entityPersister.getIdentifier( entity ), 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 e199675ad7f0..d3b41555f572 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 @@ -159,7 +159,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 ); } private Object getVersionFromPersister(Object entity) { 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 90af592a26dc..045481c3abc8 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 @@ -8,6 +8,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; @@ -66,6 +67,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 decc148cf8e2..bb0b0c74d52a 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 @@ -12,6 +12,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; @@ -194,6 +195,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, @@ -214,18 +220,18 @@ public Object getIdentifier(Object entity) { } } //JPA 2 @MapsId + @IdClass points to the pk of the entity - else if ( attributeMapping instanceof ToOneAttributeMapping + else if ( attributeMapping instanceof ToOneAttributeMapping toOneAttributeMapping && !( identifierValueMapper.getAttributeMapping( i ) instanceof ToOneAttributeMapping ) ) { - final ToOneAttributeMapping toOneAttributeMapping = (ToOneAttributeMapping) attributeMapping; + final Object toOne = getIfMerged( o, mergeContext ); 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 { @@ -240,6 +246,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 3861a42f2e8c..be2544f816ca 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 @@ -13,6 +13,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; @@ -235,6 +236,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 ) { @@ -255,18 +261,17 @@ public Object getIdentifier(Object entity) { } } //JPA 2 @MapsId + @IdClass points to the pk of the entity - else if ( attributeMapping instanceof ToOneAttributeMapping + else if ( attributeMapping instanceof ToOneAttributeMapping toOneAttributeMapping && !( identifierValueMapper.getAttributeMapping( i ) instanceof ToOneAttributeMapping ) ) { - final ToOneAttributeMapping toOneAttributeMapping = (ToOneAttributeMapping) attributeMapping; + final Object toOne = getIfMerged( o, mergeContext ); 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 { @@ -283,6 +288,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 97c05015cd3c..145fec1c2416 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 @@ -94,6 +94,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; @@ -4297,6 +4298,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 cc475efd9662..8e3576039fa5 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 @@ -28,6 +28,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; @@ -1129,6 +1130,24 @@ default Object getValue(Object object, int i) { */ Object getIdentifier(Object entity, SharedSessionContractImplementor session); + /** + * 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() ); + } + + /** + * Get the identifier of an instance from the object's identifier property. + * Throw an exception if it has no identifier property. + */ + default Object getIdentifier(Object entity) { + return getIdentifier( entity, (SharedSessionContractImplementor) null ); + } + /** * Inject the identifier value into the given entity. */ 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 cfb47b739b0c..04bf252cdd13 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 @@ -7,6 +7,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; @@ -49,6 +50,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 f7efd82cb2c7..9065873a8068 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 @@ -10,6 +10,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; @@ -68,6 +69,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 7dff0953369e..a88a2cdfe0ab 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 @@ -12,6 +12,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; @@ -77,6 +78,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 113214c01fc5..86c695d58568 100644 --- a/hibernate-core/src/main/java/org/hibernate/type/AnyType.java +++ b/hibernate-core/src/main/java/org/hibernate/type/AnyType.java @@ -146,7 +146,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 ); } 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 9d792325b691..f588895e3674 100644 --- a/hibernate-core/src/main/java/org/hibernate/type/EntityType.java +++ b/hibernate-core/src/main/java/org/hibernate/type/EntityType.java @@ -281,7 +281,7 @@ private Object extractIdentifier(Object entity, SessionFactoryImplementor factor final EntityPersister concretePersister = getAssociatedEntityPersister( factory ); return concretePersister == null ? null - : concretePersister.getIdentifier( entity, null ); + : concretePersister.getIdentifier( entity ); } @Override @@ -350,7 +350,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 ); } else { id = x; @@ -384,7 +384,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 ); } else { //JPA 2 case where @IdClass contains the id and not the associated entity @@ -399,7 +399,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 ); } else { //JPA 2 case where @IdClass contains the id and not the associated entity @@ -561,7 +561,7 @@ public String toLoggableString(Object value, SessionFactoryImplementor factory) id = lazyInitializer.getInternalIdentifier(); } else { - id = persister.getIdentifier( value, null ); + id = persister.getIdentifier( value ); } 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; + + } + +}