Composite primary key is altered when part of it is from a lazy non optional ManyToOne

Description

Hello, I'm migrating from hibernate 5.6.15 to hibernate 6.2 and I have noticed some issues. it seems that I can’t use a composite primary key when part of it is from a lazy non optional ManyToOne, as it gives me a error saying primary key was altered.

 

Entities

note

I had to use tiny ints for my enums due to https://hibernate.atlassian.net/browse/HHH-17020

Product

@Getter @IdClass(ProductPK.class) @EqualsAndHashCode(onlyExplicitlyIncluded = true) @ToString(onlyExplicitlyIncluded = true) @NoArgsConstructor(access = PROTECTED) @Entity @OptimisticLocking(type = OptimisticLockType.DIRTY) @DynamicUpdate @Cacheable @Cache(usage = READ_WRITE) @Table(name = "PRODUCTS") public class Product { public Product(String productId, Operator operator) { this.productId = productId; this.operator = operator; } public Product(String productId, Operator operator, Benefits benefits) { this.productId = productId; this.operator = operator; this.benefits = benefits; } @EqualsAndHashCode.Include @ToString.Include @Id @Column(name = "PRODUCT_ID", nullable = false) private String productId; @Id @EqualsAndHashCode.Include @ToString.Include @Getter @Setter @Cache(usage = READ_WRITE) @ManyToOne(fetch = LAZY, optional = false) @JoinColumn(name = "OPERATOR_ID", nullable = false) @JoinColumn(name = "COUNTRY", nullable = false) private Operator operator; @Column(name = "DESCRIPTION") @Setter private String description; @Embedded private Benefits benefits; @EqualsAndHashCode @ToString @Embeddable @NoArgsConstructor(access = PROTECTED) public static class ProductPK implements Serializable { private String productId; @Embedded @AttributeOverride(name = "operatorId", column = @Column(name = "OPERATOR_ID", nullable = false)) @AttributeOverride(name = "country", column = @Column(name = "COUNTRY", nullable = false)) private Operator.OperatorPK operator; public ProductPK(String productId, Operator.OperatorPK operator) { this.productId = productId; this.operator = operator; } public ProductPK(String productId, String operatorID, Country country) { this.productId = productId; this.operator = new Operator.OperatorPK(operatorID, country); } } @Embeddable @Value @AllArgsConstructor @NoArgsConstructor(access = PROTECTED) public static class Benefits { @Embedded @NonFinal @Setter TypeOneBenefit credit; @Embedded @NonFinal @Setter TypeTwoBenefit data; } @Embeddable @Value @AllArgsConstructor @NoArgsConstructor(access = PROTECTED) public static class TypeOneBenefit { @NonFinal @Column(name = "BENEFIT_ONE_BASE_AMOUNT") BigDecimal baseAmount; } @Embeddable @Value @AllArgsConstructor @NoArgsConstructor(access = PROTECTED) public static class TypeTwoBenefit { @NonFinal @Column(name = "BENEFIT_TWO_BASE_AMOUNT") String baseAmount; } }

 

Operator

@Getter @Entity @ToString(onlyExplicitlyIncluded = true) @EqualsAndHashCode(onlyExplicitlyIncluded = true) @NoArgsConstructor(access = PROTECTED) @Table(name = "OPERATORS") @IdClass(Operator.OperatorPK.class) @OptimisticLocking(type = OptimisticLockType.DIRTY) @DynamicUpdate @Cacheable @Cache(usage = READ_WRITE) public class Operator { @EqualsAndHashCode.Include @ToString.Include @Id @Column(name = "COUNTRY", nullable = false) private Country country; @EqualsAndHashCode.Include @ToString.Include @Id @Column(name = "OPERATOR_ID", nullable = false) private String operatorId; @ManyToOne @JoinColumn(name = "meta_operator_id", referencedColumnName = "ID") private MetaOperator metaOperator; @OneToMany(mappedBy = "operator", cascade = { PERSIST, MERGE, REMOVE }, orphanRemoval = true, fetch = FetchType.LAZY) private List<Product> products = new ArrayList<>(); public Operator(String operatorId) { this.operatorId = operatorId; this.country = USA; } public void setMetaOperator(MetaOperator metaOperator) { this.metaOperator = metaOperator; } public void setProducts(List<Product> products) { this.products = products; } @Embeddable @Value @AllArgsConstructor @NoArgsConstructor(access = PROTECTED) public static class OperatorPK implements Serializable { @NonFinal String operatorId; @NonFinal Country country; } }

 

Country

public enum Country { USA, FRA; }

 

Tests

should delete product

@Test void shouldDeleteProduct() { // Given String string = "ID2"; String operatorID = "operatorID2"; String test = "test"; Operator operator = new Operator(operatorID); operatorService.addOperator(operator); Product product = new Product(string, operator); product.setDescription(test); productService.addProduct(product); // When ProductPK productPK = new ProductPK(string, operatorID, USA); productService.deleteProduct(productPK); // Then Optional<Product> byId2 = productService.getProduct(productPK); assertThat(byId2).isEmpty(); }

 

an exception occurs when product is being deleted :

org.springframework.orm.jpa.JpaSystemException: identifier of an instance of com.example.demo.local.Product was altered from Product.ProductPK(productId=ID2, operator=Operator.OperatorPK(operatorId=null, country=null)) to Product.ProductPK(productId=ID2, operator=Operator.OperatorPK(operatorId=operatorID2, country=USA)) at org.springframework.orm.jpa.vendor.HibernateJpaDialect.convertHibernateAccessException(HibernateJpaDialect.java:320) at org.springframework.orm.jpa.vendor.HibernateJpaDialect.translateExceptionIfPossible(HibernateJpaDialect.java:229) at org.springframework.orm.jpa.JpaTransactionManager.doCommit(JpaTransactionManager.java:565) at org.springframework.transaction.support.AbstractPlatformTransactionManager.processCommit(AbstractPlatformTransactionManager.java:743) at org.springframework.transaction.support.AbstractPlatformTransactionManager.commit(AbstractPlatformTransactionManager.java:711) at org.springframework.transaction.interceptor.TransactionAspectSupport.commitTransactionAfterReturning(TransactionAspectSupport.java:660) at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:410) at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:119) at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:184) at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:750) at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:702) at com.example.demo.service.ProductService$$SpringCGLIB$$0.deleteProduct(<generated>) at com.example.demo.service.ProductServiceTest.shouldDeleteProduct(ProductServiceTest.java:107) ...

 

All tests on ProductServiceWithCacheTest and ProductServiceTest fails except the test where the operator is delete so it cascade delete the product (shouldDeleteProductsWithBenefitsFromOperator)

 

Sources

 

sources can be found in pk_altered_when_part_is_from_lazy_association : https://github.com/emouty/hibernate-issues/tree/pk_altered_when_part_is_from_lazy_association

Activity

Show:

Andrea Boriero November 15, 2023 at 10:45 AM

Hi

I’m going to close this!

Thanks!

Erwan Moutymbo September 19, 2023 at 11:52 AM

updating to 6.3.1.Final fixed the issue.

Out of Date

Details

Assignee

Reporter

Worked in

Components

Affects versions

Priority

Created August 1, 2023 at 4:20 PM
Updated December 3, 2024 at 9:20 AM
Resolved November 15, 2023 at 10:45 AM

Flag notifications