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/emouty/hibernate-L2-cache-issue|smart-link] |
|