Erwan Moutymbo (
https://hibernate.atlassian.net/secure/ViewProfile.jspa?accountId=640210c...
) *created* an issue
Hibernate ORM (
https://hibernate.atlassian.net/browse/HHH?atlOrigin=eyJpIjoiMTFiY2U1M2Y1...
) / Bug (
https://hibernate.atlassian.net/browse/HHH-16673?atlOrigin=eyJpIjoiMTFiY2...
) HHH-16673 (
https://hibernate.atlassian.net/browse/HHH-16673?atlOrigin=eyJpIjoiMTFiY2...
) Fail to get access lazy fetched field ( @ManyToOne ) wich is part of a composite Id
(using an @IdClass) when stored in L2 cache (
https://hibernate.atlassian.net/browse/HHH-16673?atlOrigin=eyJpIjoiMTFiY2...
)
Issue Type: Bug Affects Versions: 6.1.7, 6.2.0, 6.2.1, 6.2.3 Assignee: Unassigned
Components: hibernate-jcache Created: 23/May/2023 01:54 AM Environment: Hibernate: tried
6.1.7.Final to 6.2.3.Final
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: Oracle OpenJDK 17.0.2
OS: Fedora 38 Priority: Major Reporter: Erwan Moutymbo (
https://hibernate.atlassian.net/secure/ViewProfile.jspa?accountId=640210c...
)
I'm migrating from hibernate 5.6.15 to hibernate 6.2.3 and I have noticed some issues
with the L2 cache when an entity has a composite Id with one of its field mapped from a
many to one association.
Entities
--------
@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 ;
}
@EqualsAndHashCode.Include
@ToString.Include
@Id
@Column(name = "PRODUCT_ID" , nullable = false )
private String productId;
@Id
@EqualsAndHashCode.Include
@ToString.Include
@Getter
@Setter
@ManyToOne(fetch = LAZY)
@JoinColumn
private Operator operator ;
@Column(name = "DESCRIPTION" )
@Setter
private String description;
@AllArgsConstructor
@EqualsAndHashCode
@Getter
@Setter
@NoArgsConstructor(access = PRIVATE)
public static class ProductPK implements Serializable {
private String productId;
private String 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 )
private List<Product> products = new ArrayList<>();
public void setProducts(List<Product> products) {
this.products = products;
}
}
First Test
----------
@Test
void addProductAndReadFromCacheTest() {
String string = "ID" ;
String operatorID = "operatorID" ;
ProductPK id = new ProductPK(string, operatorID);
String test = "test" ;
Operator operator = new Operator(operatorID);
operatorDao.save( operator );
Product product = new Product(string, operator );
product.setDescription(test);
productService.addProduct(product);
// getProduct has @Transactional(propagation = REQUIRES_NEW)
// First read is made from DB
Optional<Product> byId = productService.getProduct(id);
assertThat(byId.orElseThrow().getOperator().getOperatorId()).isEqualTo(operatorID);
// Second read is from cache
Optional<Product> byId2 = productService.getProduct(id);
assertThat(byId2.orElseThrow().getOperator().getOperatorId()).isEqualTo(operatorID);
}
This test throw the following exception during Optional<Product> byId2 =
productService.getProduct(id); execution:
Caused by: org.hibernate.HibernateException: identifier of an instance of
com.example.demo.local.Product was altered from
com.example.demo.local.Product$ProductPK@226fd to
com.example.demo.local.Product$ProductPK@44bf91
at
org.hibernate.event.internal.DefaultFlushEntityEventListener.checkId(DefaultFlushEntityEventListener.java:93)
at
org.hibernate.event.internal.DefaultFlushEntityEventListener.getValues(DefaultFlushEntityEventListener.java:175)
at
org.hibernate.event.internal.DefaultFlushEntityEventListener.onFlushEntity(DefaultFlushEntityEventListener.java:134)
at
org.hibernate.event.service.internal.EventListenerGroupImpl.fireEventOnEachListener(EventListenerGroupImpl.java:127)
at
org.hibernate.event.internal.AbstractFlushingEventListener.flushEntities(AbstractFlushingEventListener.java:221)
at
org.hibernate.event.internal.AbstractFlushingEventListener.flushEverythingToExecutions(AbstractFlushingEventListener.java:90)
at
org.hibernate.event.internal.DefaultFlushEventListener.onFlush(DefaultFlushEventListener.java:38)
at
org.hibernate.event.service.internal.EventListenerGroupImpl.fireEventOnEachListener(EventListenerGroupImpl.java:127)
at org.hibernate.internal.SessionImpl.doFlush(SessionImpl.java:1412)
at org.hibernate.internal.SessionImpl.managedFlush(SessionImpl.java:485)
at
org.hibernate.internal.SessionImpl.flushBeforeTransactionCompletion(SessionImpl.java:2296)
at org.hibernate.internal.SessionImpl.beforeTransactionCompletion(SessionImpl.java:1961)
at
org.hibernate.engine.jdbc.internal.JdbcCoordinatorImpl.beforeTransactionCompletion(JdbcCoordinatorImpl.java:439)
at
org.hibernate.resource.transaction.backend.jdbc.internal.JdbcResourceLocalTransactionCoordinatorImpl.beforeCompletionCallback(JdbcResourceLocalTransactionCoordinatorImpl.java:169)
at
org.hibernate.resource.transaction.backend.jdbc.internal.JdbcResourceLocalTransactionCoordinatorImpl$TransactionDriverControlImpl.commit(JdbcResourceLocalTransactionCoordinatorImpl.java:267)
at
org.hibernate.engine.transaction.internal.TransactionImpl.commit(TransactionImpl.java:101)
at
org.springframework.orm.jpa.JpaTransactionManager.doCommit(JpaTransactionManager.java:561)
... 80
Second Test
-----------
@Test
void addProductAndReadFromCacheReadOnlyTest() {
String string = "ID" ;
String operatorID = "operatorID" ;
ProductPK id = new ProductPK(string, operatorID);
String test = "test" ;
Operator operator = new Operator(operatorID);
operatorDao.save( operator );
Product product = new Product(string, operator );
product.setDescription(test);
productService.addProduct(product);
// getProduct has @Transactional(propagation = REQUIRES_NEW) annotation
// First read is made from DB
Optional<Product> byId = productService.getProduct(id);
assertThat(byId.orElseThrow().getOperator().getOperatorId()).isEqualTo(operatorID);
// readProduct has @Transactional(readOnly = true ) annotation
// Second read is from cache
Optional<Product> byId2 = productService.readProduct(id);
assertThat(byId2.orElseThrow().getOperator().getOperatorId()).isEqualTo(operatorID);
}
This test raise the following exception. It seems that hibernate ignore that operator is a
lazy fetched field.
java.lang.NullPointerException: Cannot invoke
"com.example.demo.local.Operator.getOperatorId()" because the return value of
"com.example.demo.local.Product.getOperator()" is null
(
https://hibernate.atlassian.net/browse/HHH-16673#add-comment?atlOrigin=ey...
) Add Comment (
https://hibernate.atlassian.net/browse/HHH-16673#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=Em...
) This message was sent by Atlassian Jira (v1001.0.0-SNAPSHOT#100225- sha1:bbd69a1 )