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 c538e7309ca8..166cc40af96c 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 @@ -2434,7 +2434,8 @@ public int[] resolveDirtyAttributeIndexes( attributeNames == null ? 0 : attributeNames.length + mutablePropertiesIndexes.cardinality(); - final List fields = new ArrayList<>( estimatedSize ); + final BitSet fields = new BitSet(); + if ( estimatedSize == 0 ) { return EMPTY_INT_ARRAY; } @@ -2446,7 +2447,7 @@ public int[] resolveDirtyAttributeIndexes( i = mutablePropertiesIndexes.nextSetBit(i + 1) ) { // This is kindly borrowed from org.hibernate.type.TypeHelper.findDirty if ( isDirty( currentState, previousState, propertyTypes, propertyCheckability, i, session ) ) { - fields.add( i ); + fields.set( i ); } } } @@ -2479,8 +2480,8 @@ class ChildEntity extends SuperEntity { final String attributeName = attributeMapping.getAttributeName(); if ( isPrefix( attributeMapping, attributeNames[index] ) ) { final int position = attributeMapping.getStateArrayPosition(); - if ( propertyUpdateability[position] && !fields.contains( position ) ) { - fields.add( position ); + if ( propertyUpdateability[position] && !fields.get( position ) ) { + fields.set( position ); } index++; if ( index < attributeNames.length ) { @@ -2503,16 +2504,36 @@ class ChildEntity extends SuperEntity { else { for ( String attributeName : attributeNames ) { final Integer index = getPropertyIndexOrNull( attributeName ); - if ( index != null && propertyUpdateability[index] && !fields.contains( index ) ) { - fields.add( index ); + if ( index != null && propertyUpdateability[index] && !fields.get( index ) ) { + fields.set( index ); } } } } - return toIntArray( fields ); + return bitSetToIntArray( fields ); } + /** + * Converts a BitSet to an int array containing the indices of all set bits. + * The resulting array is naturally sorted since we iterate through set bits in order. + * + * @param bitSet the BitSet to convert + * @return an int array containing the indices of set bits, or EMPTY_INT_ARRAY if none + */ + private static int[] bitSetToIntArray(final BitSet bitSet) { + final int cardinality = bitSet.cardinality(); + if ( cardinality == 0 ) { + return EMPTY_INT_ARRAY; + } + + final int[] result = new int[cardinality]; + int arrayIndex = 0; + for ( int i = bitSet.nextSetBit(0); i >= 0; i = bitSet.nextSetBit(i + 1) ) { + result[arrayIndex++] = i; + } + return result; + } private boolean isDirty( Object[] currentState, Object[] previousState, diff --git a/hibernate-core/src/test/java/org/hibernate/persister/entity/AbstractEntityPersisterTest.java b/hibernate-core/src/test/java/org/hibernate/persister/entity/AbstractEntityPersisterTest.java new file mode 100644 index 000000000000..c0f143dee80a --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/persister/entity/AbstractEntityPersisterTest.java @@ -0,0 +1,170 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.persister.entity; + +import org.hibernate.engine.spi.SessionImplementor; +import org.hibernate.testing.orm.junit.JiraKey; +import org.hibernate.testing.junit4.BaseCoreFunctionalTestCase; +import org.junit.Before; +import org.junit.Test; + +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; +import jakarta.persistence.Table; + +import java.util.Arrays; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author El Mehdi KZADRI + */ +@JiraKey("HHH-19850") +public class AbstractEntityPersisterTest extends BaseCoreFunctionalTestCase { + + private AbstractEntityPersister persister; + + @Override + protected Class[] getAnnotatedClasses() { + return new Class[] { TestEntity.class }; + } + + @Before + @SuppressWarnings("resource") + public void setUp() { + persister = (AbstractEntityPersister) sessionFactory() + .getMappingMetamodel() + .getEntityDescriptor(TestEntity.class.getName()); + } + + @Test + public void testResolveDirtyAttributeIndexes_EmptyAttributeNames() { + inTransaction( entityManager -> { + SessionImplementor session = entityManager.unwrap(SessionImplementor.class); + + Object[] currentState = new Object[5]; + Object[] previousState = new Object[5]; + String[] attributeNames = new String[0]; + + int[] result = persister.resolveDirtyAttributeIndexes( + currentState, previousState, attributeNames, session + ); + + assertThat(result).isNotNull(); + assertThat(result.length).isEqualTo(0); + }); + } + + @Test + public void testResolveDirtyAttributeIndexes_WithAttributes() { + inTransaction( entityManager -> { + SessionImplementor session = entityManager.unwrap(SessionImplementor.class); + + Object[] currentState = new Object[] { 1L, "name1", "value1" }; + Object[] previousState = new Object[] { 1L, "name2", "value2" }; + String[] attributeNames = {"name", "value"}; + + int[] result = persister.resolveDirtyAttributeIndexes( + currentState, previousState, attributeNames, session + ); + + assertThat(result).isNotNull(); + // Verify no duplicates + assertThat(result.length).isEqualTo(Arrays.stream(result).distinct().count()); + // Verify sorted order + int[] sorted = result.clone(); + Arrays.sort(sorted); + assertThat(sorted).isEqualTo(result); + }); + } + + @Test + public void testResolveDirtyAttributeIndexes_DuplicateAttributeNames() { + inTransaction( entityManager -> { + SessionImplementor session = entityManager.unwrap(SessionImplementor.class); + + Object[] currentState = new Object[] { 1L, "name1", "value1" }; + Object[] previousState = new Object[] { 1L, "name2", "value2" }; + String[] attributeNames = {"name", "name", "value", "value"}; + + int[] result = persister.resolveDirtyAttributeIndexes( + currentState, previousState, attributeNames, session + ); + + // Should not contain duplicate indices + assertThat(result.length).isEqualTo(Arrays.stream(result).distinct().count()); + }); + } + + @Test + public void testResolveDirtyAttributeIndexes_NoDirtyFields() { + inTransaction( entityManager -> { + SessionImplementor session = entityManager.unwrap(SessionImplementor.class); + + Object[] state = new Object[] { 1L, "name1", "value1" }; + String[] attributeNames = {"name"}; + + int[] result = persister.resolveDirtyAttributeIndexes( + state, state, attributeNames, session + ); + + assertThat(result).isNotNull(); + }); + } + + @Test + public void testResolveDirtyAttributeIndexes_NonExistentAttribute() { + inTransaction( entityManager -> { + SessionImplementor session = entityManager.unwrap(SessionImplementor.class); + + Object[] currentState = new Object[] { 1L, "name1", "value1" }; + Object[] previousState = new Object[] { 1L, "name2", "value2" }; + String[] attributeNames = {"nonExistent"}; + + int[] result = persister.resolveDirtyAttributeIndexes( + currentState, previousState, attributeNames, session + ); + + assertThat(result).isNotNull(); + }); + } + + @Entity + @Table(name = "TEST_ENTITY") + public static class TestEntity { + @Id + @GeneratedValue + private Long id; + + private String name; + + private String value; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } + } +}