[JIRA] (HHH-16959) Fail to batch delete entities with nested embeddeds
by Erwan Moutymbo (JIRA)
Erwan Moutymbo ( https://hibernate.atlassian.net/secure/ViewProfile.jspa?accountId=640210c... ) *created* an issue
Hibernate ORM ( https://hibernate.atlassian.net/browse/HHH?atlOrigin=eyJpIjoiODQzNjEwNmMx... ) / Bug ( https://hibernate.atlassian.net/browse/HHH-16959?atlOrigin=eyJpIjoiODQzNj... ) HHH-16959 ( https://hibernate.atlassian.net/browse/HHH-16959?atlOrigin=eyJpIjoiODQzNj... ) Fail to batch delete entities with nested embeddeds ( https://hibernate.atlassian.net/browse/HHH-16959?atlOrigin=eyJpIjoiODQzNj... )
Issue Type: Bug Affects Versions: 6.3.0, 6.2.7 Assignee: Unassigned Components: hibernate-core Created: 20/Jul/2023 06:20 AM Environment: Hibernate: 6.2.7-SNAPSHOT
initially found with Postgresql: 14.5
reproduced with h2 2.1.214
Spring Data JPA: 3.0.6
Spring Boot: 3.0.7
ehcache 3.10.8
JDK: OpenJDK 64-Bit Server VM Temurin-17.0.6
OS: Fedora 38 Priority: Major Reporter: Erwan Moutymbo ( https://hibernate.atlassian.net/secure/ViewProfile.jspa?accountId=640210c... )
Hello, I'm migrating from hibernate 5.6.15 to hibernate 6.2 and I have noticed some issues while I perform a batch delete of some of my entities
--------
Entities
--------
Operator
@Getter
@Entity
@ToString(onlyExplicitlyIncluded = true )
@EqualsAndHashCode(onlyExplicitlyIncluded = true )
@NoArgsConstructor(access = PROTECTED)
@Table(name = "OPERATORS" )
@OptimisticLocking(type = OptimisticLockType.DIRTY)
@DynamicUpdate
@Cacheable
@Cache(usage = READ_WRITE)
public class Operator {
public Operator( String operatorId) {
this.operatorId = operatorId;
}
@EqualsAndHashCode.Include
@ToString.Include
@Id
@Column(name = "OPERATOR_ID" , nullable = false )
private String operatorId;
@OneToMany(mappedBy = " operator " , cascade = {CascadeType.ALL}, orphanRemoval = true , fetch = FetchType.LAZY)
private List<Product> products = new ArrayList<>();
public void setProducts(List<Product> products) {
this.products = products;
}
}
Product
@Getter
@IdClass(ProductPK.class)
@EqualsAndHashCode(onlyExplicitlyIncluded = true )
@ToString(onlyExplicitlyIncluded = true )
@NoArgsConstructor(access = PROTECTED)
@Entity
@OptimisticLocking(type = OptimisticLockType.ALL)
@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(nullable = false )
private Operator operator ;
@Column(name = "DESCRIPTION" )
@Setter
private String description;
@Embedded
private Benefits benefits;
@AllArgsConstructor
@EqualsAndHashCode
@ToString
@NoArgsConstructor(access = PUBLIC)
public static class ProductPK implements Serializable {
private String productId;
private String operator ;
}
@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;
}
}
-----
Tests
-----
Batch delete
------------
@Test
void shouldDeleteAllProductsWithBenefits() {
// Given
String productId1 = "ID1" ;
String productId2 = "ID2" ;
String operatorID = "operatorID2" ;
String test = "test" ;
Operator operator = new Operator(operatorID);
operatorService.addOperator( operator );
TypeOneBenefit typeOneBenefit = new TypeOneBenefit(BigDecimal.TEN);
Benefits benefits1 = new Benefits(typeOneBenefit, null );
Product product = new Product(productId1, operator , benefits1);
product.setDescription(test);
productService.addProduct(product);
TypeTwoBenefit typeTwoBenefit = new TypeTwoBenefit(BigDecimal.ONE.toString());
Benefits benefits2 = new Benefits( null , typeTwoBenefit);
Product product2 = new Product(productId2, operator , benefits2);
productService.addProduct(product2);
// When
productService.deleteAllProducts();
// Then
ProductPK productPK = new ProductPK(productId1, operatorID);
Optional<Product> byId2 = productService.getProduct(productPK);
assertThat(byId2).isEmpty();
}
When I run this test, I get the following error :
[main] DEBUG org.hibernate.SQL [correlationid=] - select p1_0.operator_operator_id,p1_0.product_id,p1_0.benefit_one_base_amount,p1_0.benefit_two_base_amount,p1_0.description from products p1_0
[main] DEBUG org.hibernate.SQL [correlationid=] - delete from products where operator_operator_id=? and product_id=? and benefit_one_base_amount=? and description=?
[main] TRACE org.hibernate.orm.jdbc.bind [correlationid=] - binding parameter (1:VARCHAR) <- [operatorID2]
[main] TRACE org.hibernate.orm.jdbc.bind [correlationid=] - binding parameter (2:VARCHAR) <- [ID1]
[main] TRACE org.hibernate.orm.jdbc.bind [correlationid=] - binding parameter (3:NUMERIC) <- [10.00]
[main] TRACE org.hibernate.orm.jdbc.bind [correlationid=] - binding parameter (4:VARCHAR) <- [test]
[main] DEBUG org.hibernate.SQL [correlationid=] - delete from products where operator_operator_id=? and product_id=? and benefit_one_base_amount=? and description=?
[main] TRACE org.hibernate.orm.jdbc.bind [correlationid=] - binding parameter (1:VARCHAR) <- [operatorID2]
[main] TRACE org.hibernate.orm.jdbc.bind [correlationid=] - binding parameter (2:VARCHAR) <- [ID2]
[main] TRACE org.hibernate.orm.jdbc.bind [correlationid=] - binding parameter (3:VARCHAR) <- [1]
[main] INFO org.hibernate.orm.jdbc.batch [correlationid=] - HHH100503: On release of batch it still contained JDBC statements
[main] ERROR org.hibernate.orm.jdbc.batch [correlationid=] - HHH100501: Exception executing batch [org.hibernate.StaleStateException: Batch update returned unexpected row count from update [1]; actual row count: 0; expected: 1; statement executed: delete from products where operator_operator_id=? and product_id=? and benefit_one_base_amount=? and description=?], SQL: delete from products where operator_operator_id=? and product_id=? and benefit_one_base_amount=? and description=?
...
org.springframework.orm.ObjectOptimisticLockingFailureException: Batch update returned unexpected row count from update [1]; actual row count: 0; expected: 1; statement executed: delete from products where operator_operator_id=? and product_id=? and benefit_one_base_amount=? and description=?
at org.springframework.orm.jpa.vendor.HibernateJpaDialect.convertHibernateAccessException(HibernateJpaDialect.java:307)
at org.springframework.orm.jpa.vendor.HibernateJpaDialect.translateExceptionIfPossible(HibernateJpaDialect.java:232)
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.deleteAllProducts(<generated>)
at com.example.demo.service.ProductServiceTest.shouldDeleteAllProductsWithBenefits(ProductServiceTest.java:165)
...
It seems that the query used to remove the first product is reused for the second even though their benefits are not the same. The correct query for the second product should have been :
delete from products where operator_operator_id=? and product_id=? and benefit_two_base_amount=? and description=?
Cascade delete
--------------
We get the same error when we delete the operator which cascade delete all of its products
@Test
void shouldDeleteProductsWithBenefitsFromOperator() {
// Given
String productId1 = "ID1" ;
String productId2 = "ID2" ;
String operatorID = "operatorID2" ;
String test = "test" ;
Operator operator = new Operator(operatorID);
operatorService.addOperator( operator );
TypeOneBenefit typeOneBenefit = new TypeOneBenefit(BigDecimal.TEN);
Benefits benefits1 = new Benefits(typeOneBenefit, null );
Product product = new Product(productId1, operator , benefits1);
product.setDescription(test);
productService.addProduct(product);
TypeTwoBenefit typeTwoBenefit = new TypeTwoBenefit(BigDecimal.ONE.toString());
Benefits benefits2 = new Benefits( null , typeTwoBenefit);
Product product2 = new Product(productId2, operator , benefits2);
productService.addProduct(product2);
// When
ProductPK productPK = new ProductPK(productId1, operatorID);
operatorService.deleteOperator(operatorID);
// Then
Optional<Product> byId2 = productService.getProduct(productPK);
assertThat(byId2).isEmpty();
}
sources can be found in branch product_with_benefits : https://github.com/emouty/hibernate-issues/tree/product_with_benefits
( https://hibernate.atlassian.net/browse/HHH-16959#add-comment?atlOrigin=ey... ) Add Comment ( https://hibernate.atlassian.net/browse/HHH-16959#add-comment?atlOrigin=ey... )
Get Jira notifications on your phone! Download the Jira Cloud app for Android ( https://play.google.com/store/apps/details?id=com.atlassian.android.jira.... ) or iOS ( https://itunes.apple.com/app/apple-store/id1006972087?pt=696495&ct=EmailN... ) This message was sent by Atlassian Jira (v1001.0.0-SNAPSHOT#100232- sha1:1e691fe )
2 years, 8 months