Skip to content

Commit 8b1c8ce

Browse files
HHH-11866 fix CustomEntityDirtinessStrategy#resetDirty is not called after entity initialization
1 parent 8d5d589 commit 8b1c8ce

File tree

3 files changed

+179
-2
lines changed

3 files changed

+179
-2
lines changed

hibernate-core/src/main/java/org/hibernate/engine/internal/EntityEntryImpl.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -310,6 +310,14 @@ public void postUpdate(Object entity, Object[] updatedState, Object nextVersion)
310310
public void postLoad(Object entity) {
311311
processIfSelfDirtinessTracker( entity, EntityEntryImpl::clearDirtyAttributes );
312312
processIfManagedEntity( entity, EntityEntryImpl::useTracker );
313+
314+
if ( persister.isMutable() ) {
315+
final SharedSessionContractImplementor session = persistenceContext.getSession();
316+
if ( session instanceof SessionImplementor sessionImplementor ) {
317+
session.getFactory().getCustomEntityDirtinessStrategy()
318+
.resetDirty( entity, persister, sessionImplementor );
319+
}
320+
}
313321
}
314322

315323
private static void clearDirtyAttributes(final SelfDirtinessTracker entity) {

hibernate-core/src/test/java/org/hibernate/orm/test/dirtiness/CustomDirtinessStrategyTest.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ public void testOnlyCustomStrategy() {
6060

6161
assertEquals( 1, Strategy.INSTANCE.canDirtyCheckCount );
6262
assertEquals( 1, Strategy.INSTANCE.isDirtyCount );
63-
assertEquals( 1, Strategy.INSTANCE.resetDirtyCount );
63+
assertEquals( 2, Strategy.INSTANCE.resetDirtyCount );
6464
assertEquals( 1, Strategy.INSTANCE.findDirtyCount );
6565

6666
session = openSession();
@@ -94,7 +94,7 @@ public void testCustomStrategyWithFlushInterceptor() {
9494
// As we used an interceptor, the custom strategy should have been called twice to find dirty properties
9595
assertEquals( 1, Strategy.INSTANCE.canDirtyCheckCount );
9696
assertEquals( 1, Strategy.INSTANCE.isDirtyCount );
97-
assertEquals( 1, Strategy.INSTANCE.resetDirtyCount );
97+
assertEquals( 2, Strategy.INSTANCE.resetDirtyCount );
9898
assertEquals( 2, Strategy.INSTANCE.findDirtyCount );
9999

100100
session = openSession();
Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
/*
2+
* SPDX-License-Identifier: Apache-2.0
3+
* Copyright Red Hat Inc. and Hibernate Authors
4+
*/
5+
package org.hibernate.orm.test.dirtiness;
6+
7+
import jakarta.persistence.Access;
8+
import jakarta.persistence.AccessType;
9+
import jakarta.persistence.Entity;
10+
import jakarta.persistence.GeneratedValue;
11+
import jakarta.persistence.Id;
12+
import jakarta.persistence.Transient;
13+
import org.hibernate.CustomEntityDirtinessStrategy;
14+
import org.hibernate.Session;
15+
import org.hibernate.cfg.AvailableSettings;
16+
import org.hibernate.persister.entity.EntityPersister;
17+
import org.hibernate.query.MutationQuery;
18+
import org.hibernate.testing.orm.junit.DomainModel;
19+
import org.hibernate.testing.orm.junit.JiraKey;
20+
import org.hibernate.testing.orm.junit.ServiceRegistry;
21+
import org.hibernate.testing.orm.junit.SessionFactory;
22+
import org.hibernate.testing.orm.junit.SessionFactoryScope;
23+
import org.hibernate.testing.orm.junit.Setting;
24+
import org.junit.jupiter.api.Test;
25+
26+
import java.beans.BeanInfo;
27+
import java.beans.IntrospectionException;
28+
import java.beans.Introspector;
29+
import java.beans.PropertyDescriptor;
30+
import java.lang.reflect.Method;
31+
import java.util.HashMap;
32+
import java.util.LinkedHashSet;
33+
import java.util.Map;
34+
import java.util.Set;
35+
36+
import static org.junit.jupiter.api.Assertions.assertEquals;
37+
import static org.junit.jupiter.api.Assertions.assertNotNull;
38+
39+
@JiraKey(value = "HHH-11866")
40+
@DomainModel(
41+
annotatedClasses = {HHH11866Test.Document.class})
42+
@ServiceRegistry(
43+
settings = {
44+
@Setting(name = AvailableSettings.GENERATE_STATISTICS, value = "true"),
45+
@Setting(name = AvailableSettings.CUSTOM_ENTITY_DIRTINESS_STRATEGY,
46+
value = "org.hibernate.orm.test.dirtiness.HHH11866Test$EntityDirtinessStrategy")
47+
}
48+
)
49+
@SessionFactory
50+
public class HHH11866Test {
51+
52+
@Test
53+
void hhh11866Test(SessionFactoryScope scope) {
54+
55+
// prepare document
56+
scope.inTransaction( session -> {
57+
58+
MutationQuery nativeMutationQuery = session.createNativeMutationQuery(
59+
"insert into Document (id,name) values (1,'title')" );
60+
nativeMutationQuery.executeUpdate();
61+
62+
} );
63+
64+
// assert document
65+
scope.inTransaction( session -> {
66+
67+
final Document document = session.createQuery( "select d from Document d", Document.class )
68+
.getSingleResult();
69+
assertNotNull( document );
70+
assertEquals( "title", document.getName() );
71+
72+
// check that flush doesn't trigger an update
73+
assertEquals( 0, scope.getSessionFactory().getStatistics().getEntityUpdateCount() );
74+
session.flush();
75+
assertEquals( 0, scope.getSessionFactory().getStatistics().getEntityUpdateCount() );
76+
} );
77+
}
78+
79+
@Entity(name = "Document")
80+
public static class Document extends SelfDirtyCheckingEntity {
81+
82+
@Id
83+
@GeneratedValue
84+
Long id;
85+
86+
// we need AccessType.PROPERTY to ensure that markDirtyProperty() is called
87+
@Access(AccessType.PROPERTY)
88+
private String name;
89+
90+
public String getName() {
91+
return name;
92+
}
93+
94+
public void setName(String name) {
95+
this.name = name;
96+
markDirtyProperty();
97+
}
98+
}
99+
100+
public static class EntityDirtinessStrategy implements CustomEntityDirtinessStrategy {
101+
102+
@Override
103+
public boolean canDirtyCheck(Object entity, EntityPersister persister, Session session) {
104+
return entity instanceof SelfDirtyCheckingEntity;
105+
}
106+
107+
@Override
108+
public boolean isDirty(Object entity, EntityPersister persister, Session session) {
109+
return !cast( entity ).getDirtyProperties().isEmpty();
110+
}
111+
112+
@Override
113+
public void resetDirty(Object entity, EntityPersister persister, Session session) {
114+
cast( entity ).clearDirtyProperties();
115+
}
116+
117+
@Override
118+
public void findDirty(Object entity, EntityPersister persister, Session session, DirtyCheckContext dirtyCheckContext) {
119+
final SelfDirtyCheckingEntity dirtyAware = cast( entity );
120+
dirtyCheckContext.doDirtyChecking(
121+
attributeInformation -> {
122+
String propertyName = attributeInformation.getName();
123+
return dirtyAware.getDirtyProperties().contains( propertyName );
124+
}
125+
);
126+
}
127+
128+
private SelfDirtyCheckingEntity cast(Object entity) {
129+
return (SelfDirtyCheckingEntity) entity;
130+
}
131+
}
132+
133+
public static abstract class SelfDirtyCheckingEntity {
134+
135+
private final Map<String, String> setterToPropertyMap = new HashMap<>();
136+
137+
@Transient
138+
private final Set<String> dirtyProperties = new LinkedHashSet<>();
139+
140+
public SelfDirtyCheckingEntity() {
141+
try {
142+
BeanInfo beanInfo = Introspector.getBeanInfo( getClass() );
143+
PropertyDescriptor[] descriptors = beanInfo.getPropertyDescriptors();
144+
for ( PropertyDescriptor descriptor : descriptors ) {
145+
Method setter = descriptor.getWriteMethod();
146+
if ( setter != null ) {
147+
setterToPropertyMap.put( setter.getName(), descriptor.getName() );
148+
}
149+
}
150+
}
151+
catch (IntrospectionException e) {
152+
throw new IllegalStateException( e );
153+
}
154+
}
155+
156+
public Set<String> getDirtyProperties() {
157+
return dirtyProperties;
158+
}
159+
160+
public void clearDirtyProperties() {
161+
dirtyProperties.clear();
162+
}
163+
164+
protected void markDirtyProperty() {
165+
String methodName = Thread.currentThread().getStackTrace()[2].getMethodName();
166+
dirtyProperties.add( setterToPropertyMap.get( methodName ) );
167+
}
168+
}
169+
}

0 commit comments

Comments
 (0)