Skip to content

Commit c3c322d

Browse files
bestmeatgsmet
authored andcommitted
HHH-12718 - Entity changes in @PreUpdate callback are not persisted when lazy loading is active for more than one field
1 parent 523b29a commit c3c322d

File tree

4 files changed

+473
-6
lines changed

4 files changed

+473
-6
lines changed

hibernate-core/src/main/java/org/hibernate/event/internal/DefaultFlushEntityEventListener.java

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -332,15 +332,20 @@ protected boolean handleInterception(FlushEntityEvent event) {
332332
final boolean intercepted = invokeInterceptor( session, entity, entry, values, persister );
333333

334334
//now we might need to recalculate the dirtyProperties array
335-
if ( intercepted && event.isDirtyCheckPossible() && !event.isDirtyCheckHandledByInterceptor() ) {
336-
int[] dirtyProperties;
337-
if ( event.hasDatabaseSnapshot() ) {
338-
dirtyProperties = persister.findModified( event.getDatabaseSnapshot(), values, entity, session );
335+
if ( intercepted && event.isDirtyCheckPossible() ) {
336+
if ( !event.isDirtyCheckHandledByInterceptor() ) {
337+
int[] dirtyProperties;
338+
if ( event.hasDatabaseSnapshot() ) {
339+
dirtyProperties = persister.findModified( event.getDatabaseSnapshot(), values, entity, session );
340+
}
341+
else {
342+
dirtyProperties = persister.findDirty( values, entry.getLoadedState(), entity, session );
343+
}
344+
event.setDirtyProperties( dirtyProperties );
339345
}
340346
else {
341-
dirtyProperties = persister.findDirty( values, entry.getLoadedState(), entity, session );
347+
dirtyCheck( event );
342348
}
343-
event.setDirtyProperties( dirtyProperties );
344349
}
345350

346351
return intercepted;
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
/*
2+
* Hibernate, Relational Persistence for Idiomatic Java
3+
*
4+
* License: GNU Lesser General Public License (LGPL), version 2.1 or later.
5+
* See the lgpl.txt file in the root directory or <http://www.gnu.org/licenses/lgpl-2.1.html>.
6+
*/
7+
package org.hibernate.jpa.test.callbacks;
8+
9+
import java.nio.ByteBuffer;
10+
import java.time.Instant;
11+
import java.util.List;
12+
import java.util.Map;
13+
import javax.persistence.Basic;
14+
import javax.persistence.ElementCollection;
15+
import javax.persistence.Entity;
16+
import javax.persistence.FetchType;
17+
import javax.persistence.GeneratedValue;
18+
import javax.persistence.Id;
19+
import javax.persistence.Lob;
20+
import javax.persistence.PrePersist;
21+
import javax.persistence.PreUpdate;
22+
23+
import org.hibernate.cfg.AvailableSettings;
24+
import org.hibernate.jpa.test.BaseEntityManagerFunctionalTestCase;
25+
26+
import org.hibernate.testing.TestForIssue;
27+
import org.hibernate.testing.bytecode.enhancement.BytecodeEnhancerRunner;
28+
import org.junit.Test;
29+
import org.junit.runner.RunWith;
30+
31+
import static org.hibernate.testing.transaction.TransactionUtil.doInJPA;
32+
import static org.junit.Assert.assertNotNull;
33+
import static org.junit.Assert.assertNull;
34+
35+
@TestForIssue(jiraKey = "HHH-12718")
36+
@RunWith(BytecodeEnhancerRunner.class)
37+
public class PreUpdateBytecodeEnhancementTest extends BaseEntityManagerFunctionalTestCase {
38+
39+
@Override
40+
protected Class<?>[] getAnnotatedClasses() {
41+
return new Class<?>[] { Person.class };
42+
}
43+
44+
@Override
45+
protected void addConfigOptions(Map options) {
46+
options.put( AvailableSettings.CLASSLOADERS, getClass().getClassLoader() );
47+
options.put( AvailableSettings.ENHANCER_ENABLE_LAZY_INITIALIZATION, "true" );
48+
options.put( AvailableSettings.ENHANCER_ENABLE_DIRTY_TRACKING, "true" );
49+
}
50+
51+
@Test
52+
public void testPreUpdateModifications() {
53+
Person person = new Person();
54+
55+
doInJPA( this::entityManagerFactory, entityManager -> {
56+
entityManager.persist( person );
57+
} );
58+
59+
doInJPA( this::entityManagerFactory, entityManager -> {
60+
Person p = entityManager.find( Person.class, person.id );
61+
assertNotNull( p );
62+
assertNotNull( p.createdAt );
63+
assertNull( p.lastUpdatedAt );
64+
65+
p.setName( "Changed Name" );
66+
} );
67+
68+
doInJPA( this::entityManagerFactory, entityManager -> {
69+
Person p = entityManager.find( Person.class, person.id );
70+
assertNotNull( p.lastUpdatedAt );
71+
} );
72+
}
73+
74+
@Entity(name = "Person")
75+
private static class Person {
76+
@Id
77+
@GeneratedValue
78+
private int id;
79+
80+
private String name;
81+
82+
public String getName() {
83+
return name;
84+
}
85+
86+
public void setName(String name) {
87+
this.name = name;
88+
}
89+
90+
private Instant createdAt;
91+
92+
public Instant getCreatedAt() {
93+
return createdAt;
94+
}
95+
96+
public void setCreatedAt(Instant createdAt) {
97+
this.createdAt = createdAt;
98+
}
99+
100+
private Instant lastUpdatedAt;
101+
102+
public Instant getLastUpdatedAt() {
103+
return lastUpdatedAt;
104+
}
105+
106+
public void setLastUpdatedAt(Instant lastUpdatedAt) {
107+
this.lastUpdatedAt = lastUpdatedAt;
108+
}
109+
110+
@ElementCollection
111+
private List<String> tags;
112+
113+
@Lob
114+
@Basic(fetch = FetchType.LAZY)
115+
private ByteBuffer image;
116+
117+
@PrePersist
118+
void beforeCreate() {
119+
this.setCreatedAt( Instant.now() );
120+
}
121+
122+
@PreUpdate
123+
void beforeUpdate() {
124+
this.setLastUpdatedAt( Instant.now() );
125+
}
126+
}
127+
}
Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
/*
2+
* Hibernate, Relational Persistence for Idiomatic Java
3+
*
4+
* License: GNU Lesser General Public License (LGPL), version 2.1 or later.
5+
* See the lgpl.txt file in the root directory or <http://www.gnu.org/licenses/lgpl-2.1.html>.
6+
*/
7+
package org.hibernate.jpa.test.callbacks;
8+
9+
import java.nio.ByteBuffer;
10+
import java.time.Instant;
11+
import java.util.List;
12+
import java.util.Map;
13+
import javax.persistence.Basic;
14+
import javax.persistence.ElementCollection;
15+
import javax.persistence.Entity;
16+
import javax.persistence.FetchType;
17+
import javax.persistence.GeneratedValue;
18+
import javax.persistence.Id;
19+
import javax.persistence.Lob;
20+
import javax.persistence.PrePersist;
21+
import javax.persistence.PreUpdate;
22+
23+
import org.hibernate.CustomEntityDirtinessStrategy;
24+
import org.hibernate.Session;
25+
import org.hibernate.annotations.DynamicUpdate;
26+
import org.hibernate.cfg.AvailableSettings;
27+
import org.hibernate.persister.entity.EntityPersister;
28+
29+
import org.hibernate.testing.TestForIssue;
30+
import org.hibernate.testing.junit4.BaseNonConfigCoreFunctionalTestCase;
31+
import org.junit.Test;
32+
33+
import static org.hibernate.testing.transaction.TransactionUtil.doInHibernate;
34+
import static org.junit.Assert.assertNotNull;
35+
import static org.junit.Assert.assertNull;
36+
import static org.junit.Assert.assertTrue;
37+
38+
@TestForIssue(jiraKey = "HHH-12718")
39+
public class PreUpdateCustomEntityDirtinessStrategyTest
40+
extends BaseNonConfigCoreFunctionalTestCase {
41+
42+
@Override
43+
protected Class<?>[] getAnnotatedClasses() {
44+
return new Class<?>[] { Person.class };
45+
}
46+
47+
@Test
48+
public void testPreUpdateModifications() {
49+
Person person = new Person();
50+
51+
doInHibernate( this::sessionFactory, session -> {
52+
session.persist( person );
53+
} );
54+
55+
doInHibernate( this::sessionFactory, session -> {
56+
Person p = session.find( Person.class, person.id );
57+
assertNotNull( p );
58+
assertNotNull( p.createdAt );
59+
assertNull( p.lastUpdatedAt );
60+
61+
p.setName( "Changed Name" );
62+
} );
63+
64+
doInHibernate( this::sessionFactory, session -> {
65+
Person p = session.find( Person.class, person.id );
66+
assertNotNull( p.lastUpdatedAt );
67+
} );
68+
69+
assertTrue( DefaultCustomEntityDirtinessStrategy.INSTANCE.isPersonNameChanged() );
70+
assertTrue( DefaultCustomEntityDirtinessStrategy.INSTANCE.isPersonLastUpdatedAtChanged() );
71+
}
72+
73+
@Override
74+
protected void addSettings(Map settings) {
75+
settings.put( AvailableSettings.CUSTOM_ENTITY_DIRTINESS_STRATEGY, DefaultCustomEntityDirtinessStrategy.INSTANCE );
76+
}
77+
78+
public static class DefaultCustomEntityDirtinessStrategy
79+
implements CustomEntityDirtinessStrategy {
80+
private static final DefaultCustomEntityDirtinessStrategy INSTANCE =
81+
new DefaultCustomEntityDirtinessStrategy();
82+
83+
private boolean personNameChanged = false;
84+
private boolean personLastUpdatedAtChanged = false;
85+
86+
@Override
87+
public boolean canDirtyCheck(Object entity, EntityPersister persister, Session session) {
88+
return true;
89+
}
90+
91+
@Override
92+
public boolean isDirty(Object entity, EntityPersister persister, Session session) {
93+
Person person = (Person) entity;
94+
if ( !personNameChanged ) {
95+
personNameChanged = person.getName() != null;
96+
return personNameChanged;
97+
}
98+
if ( !personLastUpdatedAtChanged ) {
99+
personLastUpdatedAtChanged = person.getLastUpdatedAt() != null;
100+
return personLastUpdatedAtChanged;
101+
}
102+
return false;
103+
}
104+
105+
@Override
106+
public void resetDirty(Object entity, EntityPersister persister, Session session) {
107+
}
108+
109+
@Override
110+
public void findDirty(
111+
Object entity,
112+
EntityPersister persister,
113+
Session session,
114+
DirtyCheckContext dirtyCheckContext) {
115+
}
116+
117+
public boolean isPersonNameChanged() {
118+
return personNameChanged;
119+
}
120+
121+
public boolean isPersonLastUpdatedAtChanged() {
122+
return personLastUpdatedAtChanged;
123+
}
124+
}
125+
126+
@Entity(name = "Person")
127+
@DynamicUpdate
128+
private static class Person {
129+
@Id
130+
@GeneratedValue
131+
private int id;
132+
133+
private String name;
134+
135+
public String getName() {
136+
return name;
137+
}
138+
139+
public void setName(String name) {
140+
this.name = name;
141+
}
142+
143+
private Instant createdAt;
144+
145+
public Instant getCreatedAt() {
146+
return createdAt;
147+
}
148+
149+
public void setCreatedAt(Instant createdAt) {
150+
this.createdAt = createdAt;
151+
}
152+
153+
private Instant lastUpdatedAt;
154+
155+
public Instant getLastUpdatedAt() {
156+
return lastUpdatedAt;
157+
}
158+
159+
public void setLastUpdatedAt(Instant lastUpdatedAt) {
160+
this.lastUpdatedAt = lastUpdatedAt;
161+
}
162+
163+
@ElementCollection
164+
private List<String> tags;
165+
166+
@Lob
167+
@Basic(fetch = FetchType.LAZY)
168+
private ByteBuffer image;
169+
170+
@PrePersist
171+
void beforeCreate() {
172+
this.setCreatedAt( Instant.now() );
173+
}
174+
175+
@PreUpdate
176+
void beforeUpdate() {
177+
this.setLastUpdatedAt( Instant.now() );
178+
}
179+
}
180+
}

0 commit comments

Comments
 (0)