Skip to content

Commit 4612094

Browse files
HHH-11866 fix CustomEntityDirtinessStrategy#resetDirty is not called after entity initialization
1 parent 37c1f88 commit 4612094

File tree

3 files changed

+203
-26
lines changed

3 files changed

+203
-26
lines changed

hibernate-core/src/main/java/org/hibernate/persister/entity/AbstractEntityPersister.java

Lines changed: 30 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,29 @@
44
*/
55
package org.hibernate.persister.entity;
66

7-
import org.checkerframework.checker.nullness.qual.Nullable;
7+
import java.io.Serializable;
8+
import java.sql.PreparedStatement;
9+
import java.sql.ResultSet;
10+
import java.sql.SQLException;
11+
import java.util.ArrayList;
12+
import java.util.Arrays;
13+
import java.util.BitSet;
14+
import java.util.Collection;
15+
import java.util.HashMap;
16+
import java.util.HashSet;
17+
import java.util.LinkedHashMap;
18+
import java.util.List;
19+
import java.util.Locale;
20+
import java.util.Map;
21+
import java.util.Objects;
22+
import java.util.Set;
23+
import java.util.SortedMap;
24+
import java.util.TreeMap;
25+
import java.util.concurrent.ConcurrentHashMap;
26+
import java.util.function.BiConsumer;
27+
import java.util.function.Consumer;
28+
import java.util.function.Supplier;
29+
830
import org.hibernate.AssertionFailure;
931
import org.hibernate.FetchMode;
1032
import org.hibernate.Filter;
@@ -135,7 +157,6 @@
135157
import org.hibernate.metamodel.mapping.ForeignKeyDescriptor;
136158
import org.hibernate.metamodel.mapping.JdbcMapping;
137159
import org.hibernate.metamodel.mapping.ManagedMappingType;
138-
import org.hibernate.metamodel.mapping.internal.MappingModelHelper;
139160
import org.hibernate.metamodel.mapping.ModelPart;
140161
import org.hibernate.metamodel.mapping.NaturalIdMapping;
141162
import org.hibernate.metamodel.mapping.NonAggregatedIdentifierMapping;
@@ -158,6 +179,7 @@
158179
import org.hibernate.metamodel.mapping.internal.InFlightEntityMappingType;
159180
import org.hibernate.metamodel.mapping.internal.MappingModelCreationHelper;
160181
import org.hibernate.metamodel.mapping.internal.MappingModelCreationProcess;
182+
import org.hibernate.metamodel.mapping.internal.MappingModelHelper;
161183
import org.hibernate.metamodel.mapping.internal.SimpleAttributeMetadata;
162184
import org.hibernate.metamodel.mapping.internal.SimpleNaturalIdMapping;
163185
import org.hibernate.metamodel.mapping.internal.UnifiedAnyDiscriminatorConverter;
@@ -253,28 +275,7 @@
253275
import org.hibernate.type.descriptor.java.MutabilityPlan;
254276
import org.hibernate.type.spi.TypeConfiguration;
255277

256-
import java.io.Serializable;
257-
import java.sql.PreparedStatement;
258-
import java.sql.ResultSet;
259-
import java.sql.SQLException;
260-
import java.util.ArrayList;
261-
import java.util.Arrays;
262-
import java.util.BitSet;
263-
import java.util.Collection;
264-
import java.util.HashMap;
265-
import java.util.HashSet;
266-
import java.util.LinkedHashMap;
267-
import java.util.List;
268-
import java.util.Locale;
269-
import java.util.Map;
270-
import java.util.Objects;
271-
import java.util.Set;
272-
import java.util.SortedMap;
273-
import java.util.TreeMap;
274-
import java.util.concurrent.ConcurrentHashMap;
275-
import java.util.function.BiConsumer;
276-
import java.util.function.Consumer;
277-
import java.util.function.Supplier;
278+
import org.checkerframework.checker.nullness.qual.Nullable;
278279

279280
import static java.util.Collections.emptyList;
280281
import static java.util.Collections.emptyMap;
@@ -4066,6 +4067,11 @@ public void afterInitialize(Object entity, SharedSessionContractImplementor sess
40664067
// clear the fields that are marked as dirty in the dirtiness tracker
40674068
processIfSelfDirtinessTracker( entity, AbstractEntityPersister::clearDirtyAttributes );
40684069
processIfManagedEntity( entity, AbstractEntityPersister::useTracker );
4070+
4071+
if ( session instanceof SessionImplementor sessionImplementor ) {
4072+
session.getFactory().getCustomEntityDirtinessStrategy()
4073+
.resetDirty( entity, this, sessionImplementor );
4074+
}
40694075
}
40704076

40714077
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: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
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.SHOW_SQL, value = "true"),
45+
@Setting(name = AvailableSettings.FORMAT_SQL, value = "true"),
46+
@Setting(name = AvailableSettings.GENERATE_STATISTICS, value = "true"),
47+
@Setting(name = AvailableSettings.CUSTOM_ENTITY_DIRTINESS_STRATEGY,
48+
value = "org.hibernate.orm.test.dirtiness.HHH11866Test$EntityDirtinessStrategy")
49+
}
50+
)
51+
@SessionFactory
52+
public class HHH11866Test {
53+
54+
@Test
55+
void hhh11866Test(SessionFactoryScope scope) {
56+
57+
// prepare document
58+
scope.inTransaction( session -> {
59+
60+
MutationQuery nativeMutationQuery = session.createNativeMutationQuery(
61+
"insert into Document (id,name) values (1,'title')" );
62+
nativeMutationQuery.executeUpdate();
63+
64+
} );
65+
66+
// assert document
67+
scope.inTransaction( session -> {
68+
69+
final Document document = session.createQuery( "select d from Document d", Document.class )
70+
.getSingleResult();
71+
assertNotNull( document );
72+
assertEquals( "title", document.getName() );
73+
74+
// check that flush doesn't trigger an update
75+
assertEquals( 0, scope.getSessionFactory().getStatistics().getEntityUpdateCount() );
76+
session.flush();
77+
assertEquals( 0, scope.getSessionFactory().getStatistics().getEntityUpdateCount() );
78+
} );
79+
}
80+
81+
@Entity(name = "Document")
82+
public static class Document extends SelfDirtyCheckingEntity {
83+
84+
@Id
85+
@GeneratedValue
86+
Long id;
87+
88+
// we need AccessType.PROPERTY to ensure that markDirtyProperty() is called
89+
@Access(AccessType.PROPERTY)
90+
private String name;
91+
92+
public String getName() {
93+
return name;
94+
}
95+
96+
public void setName(String name) {
97+
this.name = name;
98+
markDirtyProperty();
99+
}
100+
}
101+
102+
public static class EntityDirtinessStrategy implements CustomEntityDirtinessStrategy {
103+
104+
@Override
105+
public boolean canDirtyCheck(Object entity, EntityPersister persister, Session session) {
106+
return entity instanceof SelfDirtyCheckingEntity;
107+
}
108+
109+
@Override
110+
public boolean isDirty(Object entity, EntityPersister persister, Session session) {
111+
return !cast( entity ).getDirtyProperties().isEmpty();
112+
}
113+
114+
@Override
115+
public void resetDirty(Object entity, EntityPersister persister, Session session) {
116+
cast( entity ).clearDirtyProperties();
117+
}
118+
119+
@Override
120+
public void findDirty(Object entity, EntityPersister persister, Session session, DirtyCheckContext dirtyCheckContext) {
121+
final SelfDirtyCheckingEntity dirtyAware = cast( entity );
122+
dirtyCheckContext.doDirtyChecking(
123+
attributeInformation -> {
124+
String propertyName = attributeInformation.getName();
125+
return dirtyAware.getDirtyProperties().contains( propertyName );
126+
}
127+
);
128+
}
129+
130+
private SelfDirtyCheckingEntity cast(Object entity) {
131+
return (SelfDirtyCheckingEntity) entity;
132+
}
133+
}
134+
135+
public static abstract class SelfDirtyCheckingEntity {
136+
137+
private final Map<String, String> setterToPropertyMap = new HashMap<>();
138+
139+
@Transient
140+
private final Set<String> dirtyProperties = new LinkedHashSet<>();
141+
142+
public SelfDirtyCheckingEntity() {
143+
try {
144+
BeanInfo beanInfo = Introspector.getBeanInfo( getClass() );
145+
PropertyDescriptor[] descriptors = beanInfo.getPropertyDescriptors();
146+
for ( PropertyDescriptor descriptor : descriptors ) {
147+
Method setter = descriptor.getWriteMethod();
148+
if ( setter != null ) {
149+
setterToPropertyMap.put( setter.getName(), descriptor.getName() );
150+
}
151+
}
152+
}
153+
catch (IntrospectionException e) {
154+
throw new IllegalStateException( e );
155+
}
156+
}
157+
158+
public Set<String> getDirtyProperties() {
159+
return dirtyProperties;
160+
}
161+
162+
public void clearDirtyProperties() {
163+
dirtyProperties.clear();
164+
}
165+
166+
protected void markDirtyProperty() {
167+
String methodName = Thread.currentThread().getStackTrace()[2].getMethodName();
168+
dirtyProperties.add( setterToPropertyMap.get( methodName ) );
169+
}
170+
}
171+
}

0 commit comments

Comments
 (0)