diff --git a/hibernate-core/src/main/java/org/hibernate/event/internal/DefaultDirtyCheckEventListener.java b/hibernate-core/src/main/java/org/hibernate/event/internal/DefaultDirtyCheckEventListener.java index aa007cdb3e0e..eddb40ab850a 100644 --- a/hibernate-core/src/main/java/org/hibernate/event/internal/DefaultDirtyCheckEventListener.java +++ b/hibernate-core/src/main/java/org/hibernate/event/internal/DefaultDirtyCheckEventListener.java @@ -4,6 +4,7 @@ */ package org.hibernate.event.internal; +import org.hibernate.AssertionFailure; import org.hibernate.HibernateException; import org.hibernate.collection.spi.PersistentCollection; import org.hibernate.engine.spi.EntityEntry; @@ -42,11 +43,7 @@ public void onDirtyCheck(DirtyCheckEvent event) throws HibernateException { final var holdersByKey = persistenceContext.getEntityHoldersByKey(); if ( holdersByKey != null ) { for ( var entry : holdersByKey.entrySet() ) { - final EntityHolder holder = entry.getValue(); - final EntityEntry entityEntry = holder.getEntityEntry(); - final Status status = entityEntry.getStatus(); - if ( status != Status.MANAGED && status != Status.GONE - || isEntityDirty( holder.getManagedObject(), holder.getDescriptor(), entityEntry, session ) ) { + if ( isEntityDirty( entry.getValue(), session ) ) { event.setDirty( true ); return; } @@ -63,7 +60,18 @@ public void onDirtyCheck(DirtyCheckEvent event) throws HibernateException { } } - private static boolean isEntityDirty( + private static boolean isEntityDirty(EntityHolder holder, EventSource session) { + final EntityEntry entityEntry = holder.getEntityEntry(); + final Status status = entityEntry.getStatus(); + return switch ( status ) { + case GONE, READ_ONLY -> false; + case DELETED -> true; + case MANAGED -> isManagedEntityDirty( holder.getManagedObject(), holder.getDescriptor(), entityEntry, session ); + case SAVING, LOADING -> throw new AssertionFailure( "Unexpected status: " + status ); + }; + } + + private static boolean isManagedEntityDirty( Object entity, EntityPersister descriptor, EntityEntry entityEntry, EventSource session) { if ( entityEntry.requiresDirtyCheck( entity ) ) { // takes into account CustomEntityDirtinessStrategy final Object[] propertyValues = diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/IsDirtyImmutableTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/IsDirtyImmutableTest.java new file mode 100644 index 000000000000..c1ce47665139 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/IsDirtyImmutableTest.java @@ -0,0 +1,48 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test; + +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import org.hibernate.annotations.Immutable; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertFalse; + +@SessionFactory +@DomainModel(annotatedClasses = {IsDirtyImmutableTest.Mutable.class, IsDirtyImmutableTest.NotMutable.class}) +class IsDirtyImmutableTest { + @Test + void test(SessionFactoryScope scope) { + scope.inTransaction( s -> s.persist( new Mutable() ) ); + scope.inTransaction( s -> { + var entity = s.find( NotMutable.class, 1L ); + assertFalse( s.isDirty() ); + entity.description = "new description"; + assertFalse( s.isDirty() ); + } ); + } + + @Entity + @Table(name = "TheTable") + static class Mutable { + @Id + Long id = 1L; + String description = "old description"; + } + + @Immutable + @Entity + @Table(name = "TheTable") + static class NotMutable { + @Id + Long id; + String description; + } +}