diff --git a/hibernate-entitymanager/hibernate-entitymanager.gradle b/hibernate-entitymanager/hibernate-entitymanager.gradle index bf151386cb..f4ceb2a6fc 100755 --- a/hibernate-entitymanager/hibernate-entitymanager.gradle +++ b/hibernate-entitymanager/hibernate-entitymanager.gradle @@ -112,3 +112,17 @@ task testJar(type: Jar, dependsOn: testClasses) { artifacts { tests testJar } + +task instrument(dependsOn: compileTestJava) << { + ant.taskdef(name: 'hibInstrument', + classname: 'org.hibernate.tool.instrument.javassist.InstrumentTask', + classpath: configurations.testCompile.asPath) + ant.hibInstrument(verbose: 'true'){ + fileset(dir:"$buildDir/classes/test/org/hibernate/jpa/test/callbacks/"){ + include(name: "EntityWithLazyProperty.class") + } + } + println("hibernate instrumentation") +} + +test.dependsOn instrument diff --git a/hibernate-entitymanager/src/main/java/org/hibernate/jpa/event/internal/core/JpaFlushEntityEventListener.java b/hibernate-entitymanager/src/main/java/org/hibernate/jpa/event/internal/core/JpaFlushEntityEventListener.java index 928f0085db..083e06abda 100644 --- a/hibernate-entitymanager/src/main/java/org/hibernate/jpa/event/internal/core/JpaFlushEntityEventListener.java +++ b/hibernate-entitymanager/src/main/java/org/hibernate/jpa/event/internal/core/JpaFlushEntityEventListener.java @@ -7,6 +7,7 @@ package org.hibernate.jpa.event.internal.core; import org.hibernate.SessionFactory; +import org.hibernate.bytecode.instrumentation.spi.LazyPropertyInitializer; import org.hibernate.engine.spi.EntityEntry; import org.hibernate.engine.spi.SessionImplementor; import org.hibernate.engine.spi.Status; @@ -61,7 +62,8 @@ public class JpaFlushEntityEventListener extends DefaultFlushEntityEventListener int size = newState.length; boolean isDirty = false; for ( int index = 0; index < size ; index++ ) { - if ( !types[index].isEqual( state[index], newState[index] ) ) { + if (state[index] == LazyPropertyInitializer.UNFETCHED_PROPERTY && newState[index] != LazyPropertyInitializer.UNFETCHED_PROPERTY + || state[index] != newState[index] && !types[index].isEqual(state[index], newState[index])) { isDirty = true; state[index] = newState[index]; } diff --git a/hibernate-entitymanager/src/test/java/org/hibernate/jpa/test/callbacks/CallbacksTest.java b/hibernate-entitymanager/src/test/java/org/hibernate/jpa/test/callbacks/CallbacksTest.java index ec7571e416..315d27eb8e 100644 --- a/hibernate-entitymanager/src/test/java/org/hibernate/jpa/test/callbacks/CallbacksTest.java +++ b/hibernate-entitymanager/src/test/java/org/hibernate/jpa/test/callbacks/CallbacksTest.java @@ -251,7 +251,76 @@ public class CallbacksTest extends BaseEntityManagerFunctionalTestCase { RemoteControl.class, Rythm.class, Plant.class, - Kitten.class + Kitten.class, + EntityWithLazyProperty.class }; } + + /** + * Test for HHH-7573. + * Load some test data into an entity which has a lazy property and a @PreUpdate callback, then reload and update a + * non lazy field which will trigger the PreUpdate lifecycle callback. + * @throws Exception + */ + @Test + public void testJpaFlushEntityEventListener() throws Exception { + EntityWithLazyProperty entity; + EntityManager em = getOrCreateEntityManager(); + + byte[] testArray = new byte[]{0x2A}; + + //persist the test entity. + em.getTransaction().begin(); + entity = new EntityWithLazyProperty(); + entity.setSomeField("TEST"); + entity.setLazyData(testArray); + em.persist(entity); + em.getTransaction().commit(); + checkLazyField(entity, em, testArray); + + /** + * Set a non lazy field, therefore the lazyData field will be LazyPropertyInitializer.UNFETCHED_PROPERTY + * for both state and newState so the field should not change. This should no longer cause a ClassCastException. + */ + em.getTransaction().begin(); + entity = em.find(EntityWithLazyProperty.class, entity.getId()); + entity.setSomeField("TEST1"); + em.getTransaction().commit(); + checkLazyField(entity, em, testArray); + + /** + * Set the updateLazyFieldInPreUpdate flag so that the lazy field is updated from within the + * PreUpdate annotated callback method. So state == LazyPropertyInitializer.UNFETCHED_PROPERTY and + * newState == EntityWithLazyProperty.PRE_UPDATE_VALUE. This should no longer cause a ClassCastException. + */ + em.getTransaction().begin(); + entity = em.find(EntityWithLazyProperty.class, entity.getId()); + entity.setUpdateLazyFieldInPreUpdate(true); + entity.setSomeField("TEST2"); + em.getTransaction().commit(); + checkLazyField(entity, em, EntityWithLazyProperty.PRE_UPDATE_VALUE); + + /** + * Set the updateLazyFieldInPreUpdate flag so that the lazy field is updated from within the + * PreUpdate annotated callback method and also set the lazyData field directly to testArray1. When we reload we + * should get EntityWithLazyProperty.PRE_UPDATE_VALUE. + */ + em.getTransaction().begin(); + entity = em.find(EntityWithLazyProperty.class, entity.getId()); + entity.setUpdateLazyFieldInPreUpdate(true); + entity.setLazyData(testArray); + entity.setSomeField("TEST3"); + em.getTransaction().commit(); + checkLazyField(entity, em, EntityWithLazyProperty.PRE_UPDATE_VALUE); + + em.close(); + } + + private void checkLazyField(EntityWithLazyProperty entity, EntityManager em, byte[] expected) { + // reload the entity and check the lazy value matches what we expect. + em.getTransaction().begin(); + entity = em.find(EntityWithLazyProperty.class, entity.getId()); + assertEquals(expected, entity.getLazyData()); + em.getTransaction().commit(); + } } diff --git a/hibernate-entitymanager/src/test/java/org/hibernate/jpa/test/callbacks/EntityWithLazyProperty.java b/hibernate-entitymanager/src/test/java/org/hibernate/jpa/test/callbacks/EntityWithLazyProperty.java new file mode 100644 index 0000000000..f38af8b130 --- /dev/null +++ b/hibernate-entitymanager/src/test/java/org/hibernate/jpa/test/callbacks/EntityWithLazyProperty.java @@ -0,0 +1,63 @@ +package org.hibernate.jpa.test.callbacks; + +import javax.persistence.*; + +/** + * Test entity with a lazy property which requires build time instrumentation. + */ +@Entity +public class EntityWithLazyProperty { + + public static final byte[] PRE_UPDATE_VALUE = new byte[]{0x2A, 0x2A, 0x2A, 0x2A}; + + @Id + @GeneratedValue + private Long id; + + @Basic(fetch = FetchType.LAZY) + private byte[] lazyData; + + private String someField; + + private boolean updateLazyFieldInPreUpdate; + + public Long getId() { + return id; + } + + public void setId(final Long id) { + this.id = id; + } + + public byte[] getLazyData() { + return lazyData; + } + + public void setLazyData(final byte[] lazyData) { + this.lazyData = lazyData; + } + + public String getSomeField() { + return someField; + } + + public void setSomeField(String someField) { + this.someField = someField; + } + + public boolean isUpdateLazyFieldInPreUpdate() { + return updateLazyFieldInPreUpdate; + } + + public void setUpdateLazyFieldInPreUpdate(boolean updateLazyFieldInPreUpdate) { + this.updateLazyFieldInPreUpdate = updateLazyFieldInPreUpdate; + } + + @PreUpdate + public void onPreUpdate() { + //Allow the update of the lazy field from within the pre update to check that this does not break things. + if(isUpdateLazyFieldInPreUpdate()) { + this.setLazyData(PRE_UPDATE_VALUE); + } + } +}