diff --git a/hibernate-core/src/main/java/org/hibernate/event/internal/DefaultFlushEntityEventListener.java b/hibernate-core/src/main/java/org/hibernate/event/internal/DefaultFlushEntityEventListener.java index b684a51693..d28de244bf 100644 --- a/hibernate-core/src/main/java/org/hibernate/event/internal/DefaultFlushEntityEventListener.java +++ b/hibernate-core/src/main/java/org/hibernate/event/internal/DefaultFlushEntityEventListener.java @@ -480,10 +480,20 @@ public class DefaultFlushEntityEventListener implements FlushEntityEventListener if ( dirtyProperties == null ) { if ( entity instanceof SelfDirtinessTracker ) { if ( ( (SelfDirtinessTracker) entity ).$$_hibernate_hasDirtyAttributes() ) { - dirtyProperties = persister.resolveAttributeIndexes( ( (SelfDirtinessTracker) entity ).$$_hibernate_getDirtyAttributes() ); + int[] dirty = persister.resolveAttributeIndexes( ( (SelfDirtinessTracker) entity ).$$_hibernate_getDirtyAttributes() ); + + // HHH-12051 - filter non-updatable attributes + // TODO: add Updateability to EnhancementContext and skip dirty tracking of those attributes + int count = 0; + for ( int i : dirty ) { + if ( persister.getPropertyUpdateability()[i] ) { + dirty[count++] = i; + } + } + dirtyProperties = count == 0 ? ArrayHelper.EMPTY_INT_ARRAY : count == dirty.length ? dirty : Arrays.copyOf( dirty, count ); } else { - dirtyProperties = new int[0]; + dirtyProperties = ArrayHelper.EMPTY_INT_ARRAY; } } else { diff --git a/hibernate-core/src/test/java/org/hibernate/test/bytecode/enhancement/dirty/DirtyTrackingNonUpdateableTest.java b/hibernate-core/src/test/java/org/hibernate/test/bytecode/enhancement/dirty/DirtyTrackingNonUpdateableTest.java new file mode 100644 index 0000000000..d9fed08001 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/test/bytecode/enhancement/dirty/DirtyTrackingNonUpdateableTest.java @@ -0,0 +1,68 @@ +/* + * 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 org.hibernate.testing.TestForIssue; +import org.hibernate.testing.bytecode.enhancement.BytecodeEnhancerRunner; +import org.hibernate.testing.junit4.BaseCoreFunctionalTestCase; +import org.junit.Test; +import org.junit.runner.RunWith; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.Table; +import javax.persistence.Version; + +import static org.hibernate.testing.transaction.TransactionUtil.doInJPA; + +/** + * @author Luis Barreiro + */ +@TestForIssue( jiraKey = "HHH-12051" ) +@RunWith( BytecodeEnhancerRunner.class ) +public class DirtyTrackingNonUpdateableTest extends BaseCoreFunctionalTestCase { + + @Override + public Class[] getAnnotatedClasses() { + return new Class[]{Thing.class}; + } + + @Test + public void test() { + doInJPA( this::sessionFactory, entityManager -> { + Thing thing = new Thing(); + entityManager.persist( thing ); + + entityManager + .createQuery( "update thing set special = :s, version = version + 1" ) + .setParameter( "s", "new" ) + .executeUpdate(); + + thing.special = "If I'm flush to the DB you get an OptimisticLockException"; + } ); + } + + // --- // + + @Entity( name = "thing" ) + @Table( name = "THING_ENTITY" ) + public class Thing { + + @Id + @GeneratedValue( strategy = GenerationType.IDENTITY ) + long id; + + @Version + long version; + + @Column( updatable = false ) + String special; + } +}