diff --git a/hibernate-core/src/main/java/org/hibernate/bytecode/enhance/spi/interceptor/EnhancementAsProxyLazinessInterceptor.java b/hibernate-core/src/main/java/org/hibernate/bytecode/enhance/spi/interceptor/EnhancementAsProxyLazinessInterceptor.java index 140ea0bae6..85f71995b8 100644 --- a/hibernate-core/src/main/java/org/hibernate/bytecode/enhance/spi/interceptor/EnhancementAsProxyLazinessInterceptor.java +++ b/hibernate-core/src/main/java/org/hibernate/bytecode/enhance/spi/interceptor/EnhancementAsProxyLazinessInterceptor.java @@ -74,8 +74,9 @@ public class EnhancementAsProxyLazinessInterceptor extends AbstractLazyLoadInter this.inLineDirtyChecking = SelfDirtinessTracker.class.isAssignableFrom( entityPersister.getMappedClass() ); // if self-dirty tracking is enabled but DynamicUpdate is not enabled then we need to initialise the entity - // because the pre-computed update statement contains even not dirty properties and so we need all the values - initializeBeforeWrite = !( inLineDirtyChecking && entityPersister.getEntityMetamodel().isDynamicUpdate() ); + // because the pre-computed update statement contains even not dirty properties and so we need all the values + // we have to initialise it even if it's versioned to fetch the current version + initializeBeforeWrite = !( inLineDirtyChecking && entityPersister.getEntityMetamodel().isDynamicUpdate() ) || entityPersister.isVersioned(); status = Status.UNINITIALIZED; } diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/version/VersionedEntityTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/version/VersionedEntityTest.java new file mode 100644 index 0000000000..6c9c379eec --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/version/VersionedEntityTest.java @@ -0,0 +1,241 @@ +package org.hibernate.orm.test.bytecode.enhancement.version; + +import org.hibernate.Hibernate; +import org.hibernate.testing.TestForIssue; +import org.hibernate.testing.bytecode.enhancement.BytecodeEnhancerRunner; +import org.hibernate.testing.junit4.BaseCoreFunctionalTestCase; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.io.Serializable; +import java.util.HashSet; +import java.util.Set; + +import jakarta.persistence.CascadeType; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.ForeignKey; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.MappedSuperclass; +import jakarta.persistence.OneToMany; +import jakarta.persistence.Version; + +import static org.hibernate.Hibernate.isInitialized; +import static org.hibernate.testing.transaction.TransactionUtil.doInJPA; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +@TestForIssue(jiraKey = "HHH-15134") +@RunWith(BytecodeEnhancerRunner.class) +public class VersionedEntityTest extends BaseCoreFunctionalTestCase { + private final Long parentID = 1L; + + @Override + public Class[] getAnnotatedClasses() { + return new Class[]{ FooEntity.class, BarEntity.class, BazEntity.class }; + } + + @Before + public void prepare() { + doInJPA(this::sessionFactory, em -> { + final FooEntity entity = FooEntity.of( parentID, "foo" ); + em.persist( entity ); + }); + } + + @Test + public void testUpdateVersionedEntity() { + doInJPA(this::sessionFactory, em -> { + final FooEntity entity = em.getReference( FooEntity.class, parentID ); + + assertFalse( isInitialized( entity ) ); + assertTrue( Hibernate.isPropertyInitialized( entity, "id" ) ); + assertFalse( Hibernate.isPropertyInitialized( entity, "name" ) ); + assertFalse( Hibernate.isPropertyInitialized( entity, "version" ) ); + assertFalse( Hibernate.isPropertyInitialized( entity, "bars" ) ); + assertFalse( Hibernate.isPropertyInitialized( entity, "bazzes" ) ); + + entity.setName( "bar" ); + }); + } + + @MappedSuperclass + public static abstract class AbstractEntity { + + public abstract T getId(); + + public abstract void setId(T id); + + @Override + public int hashCode() { + return getClass().hashCode(); + } + + @Override + public boolean equals(Object obj) { + if (obj == this) return true; + if (obj == null || obj.getClass() != getClass()) return false; + + final AbstractEntity other = (AbstractEntity) obj; + return getId() != null && getId().equals(other.getId()); + } + } + + @Entity(name = "FooEntity") + public static class FooEntity extends AbstractEntity { + + @Id + private long id; + @Version + private int version; + + private String name; + + @OneToMany(mappedBy = "foo", cascade = CascadeType.ALL, targetEntity = BarEntity.class, orphanRemoval = true) + public Set bars = new HashSet<>(); + + @OneToMany(mappedBy = "foo", cascade = CascadeType.ALL, targetEntity = BazEntity.class, orphanRemoval = true) + public Set bazzes = new HashSet<>(); + + public static FooEntity of(long id, String name) { + final FooEntity f = new FooEntity(); + f.id = id; + f.name = name; + return f; + } + + @Override + public Long getId() { + return id; + } + + @Override + public void setId(Long id) { + this.id = id; + } + + public int getVersion() { + return version; + } + + public void setVersion(int version) { + this.version = version; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public Set getBars() { + return bars; + } + + public void addBar(BarEntity bar) { + bars.add(bar); + bar.setFoo(this); + } + + public void removeBar(BarEntity bar) { + bars.remove(bar); + bar.setFoo(null); + } + + public Set getBazzes() { + return bazzes; + } + + public void addBaz(BazEntity baz) { + bazzes.add(baz); + baz.setFoo(this); + } + + public void removeBaz(BazEntity baz) { + bazzes.remove(baz); + baz.setFoo(null); + } + + @Override + public String toString() { + return String.format("FooEntity: id=%d, version=%d, name=%s", id, version, name); + } + } + + @Entity(name = "BazEntity") + public static class BazEntity extends AbstractEntity { + + @Id + @GeneratedValue + private long id; + + @ManyToOne(fetch = FetchType.LAZY, optional = false) + @JoinColumn(foreignKey = @ForeignKey(name = "fk_baz_foo"), nullable = false) + private FooEntity foo; + + @Override + public Long getId() { + return id; + } + + @Override + public void setId(Long id) { + this.id = id; + } + + public FooEntity getFoo() { + return foo; + } + + public void setFoo(FooEntity foo) { + this.foo = foo; + } + + @Override + public String toString() { + return String.format("BazEntity: id=%d", id); + } + } + + @Entity(name = "BarEntity") + public static class BarEntity extends AbstractEntity { + + @Id + @GeneratedValue + private long id; + + @ManyToOne(fetch = FetchType.LAZY, optional = false) + @JoinColumn(foreignKey = @ForeignKey(name = "fk_bar_foo"), nullable = false) + private FooEntity foo; + + @Override + public Long getId() { + return id; + } + + @Override + public void setId(Long id) { + this.id = id; + } + + public FooEntity getFoo() { + return foo; + } + + public void setFoo(FooEntity foo) { + this.foo = foo; + } + + @Override + public String toString() { + return String.format("BarEntity: id=%d", id); + } + } +} +