[JIRA] (HHH-16673) Fail to get access lazy fetched field ( @ManyToOne ) which is part of a composite Id (using an @IdClass) when stored in L2 cache
by Erwan Moutymbo (JIRA)
Erwan Moutymbo ( https://hibernate.atlassian.net/secure/ViewProfile.jspa?accountId=640210c... ) *updated* an issue
Hibernate ORM ( https://hibernate.atlassian.net/browse/HHH?atlOrigin=eyJpIjoiODc4YjQ5YmI1... ) / Bug ( https://hibernate.atlassian.net/browse/HHH-16673?atlOrigin=eyJpIjoiODc4Yj... ) HHH-16673 ( https://hibernate.atlassian.net/browse/HHH-16673?atlOrigin=eyJpIjoiODc4Yj... ) Fail to get access lazy fetched field ( @ManyToOne ) which is part of a composite Id (using an @IdClass) when stored in L2 cache ( https://hibernate.atlassian.net/browse/HHH-16673?atlOrigin=eyJpIjoiODc4Yj... )
Change By: 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.
h3. Entities
{code:java}@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;
}
}{code}
{code:java}@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;
}
}{code}
h3. First Test
{code:java} @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);
}{code}
This test throw the following exception during {{Optional<Product> byId2 = productService.getProduct(id);}} execution:
{noformat}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{noformat}
h3. Second Test
{code:java} @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);
}{code}
This test raise the following exception. It seems that hibernate ignore that {{operator}} is a lazy fetched field.
{noformat}java.lang.NullPointerException: Cannot invoke "com.example.demo.local.Operator.getOperatorId()" because the return value of "com.example.demo.local.Product.getOperator()" is null{noformat}
Github repo with sources : [https://github.com/emouty/hibernate-L2-cache-issue|https://github.com/emo...]
( 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=EmailN... ) This message was sent by Atlassian Jira (v1001.0.0-SNAPSHOT#100225- sha1:e03cc87 )
2 years, 10 months