From c915168cf1326854e4c88819de59fd90aac7f27e Mon Sep 17 00:00:00 2001 From: Christian Beikov Date: Wed, 9 Dec 2020 11:46:43 +0100 Subject: [PATCH] HHH-14360 Test a fix for NPE due to non-existing previous state in bytecode enhanced dirty attribute resolving --- .../entity/AbstractEntityPersister.java | 3 +- .../dirty/DirtyTrackingPersistTest.java | 190 ++++++++++++++++++ 2 files changed, 192 insertions(+), 1 deletion(-) create mode 100644 hibernate-core/src/test/java/org/hibernate/test/bytecode/enhancement/dirty/DirtyTrackingPersistTest.java diff --git a/hibernate-core/src/main/java/org/hibernate/persister/entity/AbstractEntityPersister.java b/hibernate-core/src/main/java/org/hibernate/persister/entity/AbstractEntityPersister.java index f0c4f6b6a531..c222292001cd 100644 --- a/hibernate-core/src/main/java/org/hibernate/persister/entity/AbstractEntityPersister.java +++ b/hibernate-core/src/main/java/org/hibernate/persister/entity/AbstractEntityPersister.java @@ -2280,7 +2280,8 @@ public int[] resolveDirtyAttributeIndexes( mutablePropertiesIndexes.stream().forEach( i -> { // This is kindly borrowed from org.hibernate.type.TypeHelper.findDirty final boolean dirty = currentState[i] != LazyPropertyInitializer.UNFETCHED_PROPERTY && - ( previousState[i] == LazyPropertyInitializer.UNFETCHED_PROPERTY || + // Consider mutable properties as dirty if we don't have a previous state + ( previousState == null || previousState[i] == LazyPropertyInitializer.UNFETCHED_PROPERTY || ( propertyCheckability[i] && propertyTypes[i].isDirty( previousState[i], diff --git a/hibernate-core/src/test/java/org/hibernate/test/bytecode/enhancement/dirty/DirtyTrackingPersistTest.java b/hibernate-core/src/test/java/org/hibernate/test/bytecode/enhancement/dirty/DirtyTrackingPersistTest.java new file mode 100644 index 000000000000..fa615e2a22f0 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/test/bytecode/enhancement/dirty/DirtyTrackingPersistTest.java @@ -0,0 +1,190 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later. + * See the lgpl.txt file in the root directory or . + */ +package org.hibernate.test.bytecode.enhancement.dirty; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Date; +import java.util.List; +import java.util.Set; +import javax.persistence.Basic; +import javax.persistence.CascadeType; +import javax.persistence.ElementCollection; +import javax.persistence.Entity; +import javax.persistence.FetchType; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.JoinColumn; +import javax.persistence.ManyToOne; +import javax.persistence.OneToMany; +import javax.persistence.OrderBy; +import javax.persistence.OrderColumn; +import javax.persistence.Table; +import javax.persistence.Temporal; +import javax.persistence.TemporalType; + +import org.hibernate.boot.internal.SessionFactoryBuilderImpl; +import org.hibernate.boot.internal.SessionFactoryOptionsBuilder; +import org.hibernate.boot.spi.SessionFactoryBuilderService; +import org.hibernate.bytecode.enhance.spi.interceptor.BytecodeLazyAttributeInterceptor; +import org.hibernate.cfg.Configuration; +import org.hibernate.engine.spi.PersistentAttributeInterceptable; + +import org.hibernate.testing.TestForIssue; +import org.hibernate.testing.bytecode.enhancement.BytecodeEnhancerRunner; +import org.hibernate.testing.junit4.BaseCoreFunctionalTestCase; +import org.hibernate.testing.transaction.TransactionUtil; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +import static org.hibernate.testing.transaction.TransactionUtil.doInHibernate; +import static org.hibernate.testing.transaction.TransactionUtil.doInJPA; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +/** + * @author Christian Beikov + */ +@TestForIssue(jiraKey = "HHH-14360") +@RunWith(BytecodeEnhancerRunner.class) +public class DirtyTrackingPersistTest extends BaseCoreFunctionalTestCase { + + @Override + public Class[] getAnnotatedClasses() { + return new Class[] { HotherEntity.class, Hentity.class }; + } + + @Override + protected void configure(Configuration configuration) { + super.configure( configuration ); + configuration.getStandardServiceRegistryBuilder().addService( + SessionFactoryBuilderService.class, + (SessionFactoryBuilderService) (metadata, bootstrapContext) -> { + SessionFactoryOptionsBuilder optionsBuilder = new SessionFactoryOptionsBuilder( + metadata.getMetadataBuildingOptions().getServiceRegistry(), + bootstrapContext + ); + optionsBuilder.enableCollectionInDefaultFetchGroup( true ); + return new SessionFactoryBuilderImpl( metadata, optionsBuilder ); + } + ); + } + + @Test + public void test() { + Hentity hentity = new Hentity(); + HotherEntity hotherEntity = new HotherEntity(); + hentity.setLineItems( new ArrayList<>( Collections.singletonList( hotherEntity ) ) ); + hentity.setNextRevUNs( new ArrayList<>( Collections.singletonList( "something" ) ) ); + doInHibernate( this::sessionFactory, session -> { + session.persist( hentity ); + } ); + doInHibernate( this::sessionFactory, session -> { + hentity.bumpNumber(); + session.saveOrUpdate( hentity ); + } ); + } + + // --- // + + @Entity + public static class HotherEntity { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + @Basic + private Long clicId; + + public void setId(Long id) { + this.id = id; + } + + public Long getId() { + return id; + } + + public Long getClicId() { + return clicId; + } + + public void setClicId(Long clicId) { + this.clicId = clicId; + } + } + + @Entity + public static class Hentity { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ElementCollection + @OrderColumn(name = "nextRevUN_index") + private List nextRevUNs; + + @OneToMany(fetch = FetchType.LAZY, cascade = CascadeType.ALL) + @JoinColumn(name = "clicId") + @OrderBy("id asc") + protected List lineItems; + + @Basic + private Long number; + + @Temporal(value = TemporalType.TIMESTAMP) + private Date createDate; + + @Temporal(value = TemporalType.TIMESTAMP) + private Date deleteDate; + + public void setId(Long id) { + this.id = id; + } + + public Long getId() { + return id; + } + + public List getNextRevUNs() { + return nextRevUNs; + } + + public void setNextRevUNs(List nextRevUNs) { + this.nextRevUNs = nextRevUNs; + } + + public List getLineItems() { + return lineItems; + } + + public void setLineItems(List lineItems) { + this.lineItems = lineItems; + } + + public Date getCreateDate() { + return createDate; + } + + public void setCreateDate(Date createDate) { + this.createDate = createDate; + } + + public Date getDeleteDate() { + return deleteDate; + } + + public void setDeleteDate(Date deleteDate) { + this.deleteDate = deleteDate; + } + + public void bumpNumber() { + number = number == null ? 0 : number++; + } + } +} \ No newline at end of file