Skip to content

Commit 60b538a

Browse files
HHH-15045 avoid dirty-flush for OneToOne type
1 parent 7b2e93f commit 60b538a

File tree

2 files changed

+135
-9
lines changed

2 files changed

+135
-9
lines changed

hibernate-core/src/main/java/org/hibernate/type/OneToOneType.java

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -106,17 +106,12 @@ public boolean isOneToOne() {
106106

107107
@Override
108108
public boolean isDirty(Object old, Object current, SharedSessionContractImplementor session) {
109-
if ( isSame( old, current ) ) {
110-
return false;
111-
}
112-
113-
return getIdentifierType( session )
114-
.isDirty( getIdentifier( old, session ), getIdentifier( current, session ), session );
109+
return false;
115110
}
116111

117112
@Override
118113
public boolean isDirty(Object old, Object current, boolean[] checkable, SharedSessionContractImplementor session) {
119-
return isDirty(old, current, session);
114+
return false;
120115
}
121116

122117
@Override
@@ -169,9 +164,14 @@ public Object assemble(Serializable oid, SharedSessionContractImplementor sessio
169164

170165
return resolveIdentifier( id, session );
171166
}
172-
167+
168+
/**
169+
* We don't need to dirty check one-to-one because of how
170+
* assemble/disassemble is implemented and because a one-to-one
171+
* association is never dirty
172+
*/
173173
@Override
174174
public boolean isAlwaysDirtyChecked() {
175-
return true;
175+
return false;
176176
}
177177
}
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
package org.hibernate.orm.test.onetoone.flush;
2+
3+
import java.util.List;
4+
import java.util.Map;
5+
6+
import org.hibernate.CallbackException;
7+
import org.hibernate.Interceptor;
8+
import org.hibernate.cfg.AvailableSettings;
9+
import org.hibernate.jpa.boot.spi.Bootstrap;
10+
import org.hibernate.orm.test.jpa.SettingsGenerator;
11+
import org.hibernate.type.Type;
12+
13+
import org.hibernate.testing.TestForIssue;
14+
import org.hibernate.testing.orm.jpa.PersistenceUnitDescriptorAdapter;
15+
import org.hibernate.testing.orm.junit.DialectContext;
16+
import org.hibernate.testing.orm.transaction.TransactionUtil;
17+
import org.junit.jupiter.api.Assertions;
18+
import org.junit.jupiter.api.BeforeEach;
19+
import org.junit.jupiter.api.Test;
20+
21+
import jakarta.persistence.Entity;
22+
import jakarta.persistence.EntityManagerFactory;
23+
import jakarta.persistence.Id;
24+
import jakarta.persistence.OneToOne;
25+
26+
/**
27+
* @author Nathan Xu
28+
*/
29+
@TestForIssue( jiraKey = "HHH-15045" )
30+
class DirtyFlushTest {
31+
32+
static List<Class> getAnnotatedClasses() {
33+
return List.of( User.class, Profile.class );
34+
}
35+
36+
static Map basicSettings() {
37+
return SettingsGenerator.generateSettings(
38+
AvailableSettings.HBM2DDL_AUTO, "create-drop",
39+
AvailableSettings.DIALECT, DialectContext.getDialect().getClass().getName(),
40+
AvailableSettings.LOADED_CLASSES, getAnnotatedClasses(),
41+
AvailableSettings.KEYWORD_AUTO_QUOTING_ENABLED, "true"
42+
);
43+
}
44+
45+
static EntityManagerFactory buildEntityManagerFactory(Map settings) {
46+
return Bootstrap
47+
.getEntityManagerFactoryBuilder( new PersistenceUnitDescriptorAdapter(), settings )
48+
.build();
49+
}
50+
51+
@BeforeEach
52+
void setUp() {
53+
DirtyFlushInterceptor.dirtyFlushedForUser = false;
54+
}
55+
56+
@Test
57+
void testDirtyFlushNotHappened() {
58+
var settings = basicSettings();
59+
settings.put( AvailableSettings.INTERCEPTOR, new DirtyFlushInterceptor() );
60+
var entityManagerFactory = buildEntityManagerFactory( settings );
61+
var em = entityManagerFactory.createEntityManager();
62+
63+
TransactionUtil.inTransaction( em, entityManager -> {
64+
var user = new User();
65+
user.id = 1;
66+
entityManager.persist( user );
67+
} );
68+
69+
try {
70+
TransactionUtil.inTransaction( em, entityManager -> {
71+
var user = entityManager.find( User.class, 1 );
72+
var profile = new Profile();
73+
profile.id = 1;
74+
profile.user = user;
75+
user.profile = profile;
76+
entityManager.persist( profile );
77+
} );
78+
79+
Assertions.assertFalse( DirtyFlushInterceptor.dirtyFlushedForUser, "User should not be dirty-flushed when only Profile changes!" );
80+
81+
} finally {
82+
TransactionUtil.inTransaction( em, entityManager -> {
83+
entityManager.createQuery( "delete from Profile " ).executeUpdate();
84+
entityManager.createQuery( "delete from User" ).executeUpdate();
85+
} );
86+
}
87+
}
88+
89+
90+
@Entity(name = "User")
91+
static class User {
92+
@Id int id;
93+
94+
@OneToOne(mappedBy = "user")
95+
Profile profile;
96+
}
97+
98+
@Entity(name = "Profile")
99+
static class Profile {
100+
@Id int id;
101+
102+
@OneToOne // internally Hibernate will use `@ManyToOne` for this field
103+
User user;
104+
}
105+
106+
static class DirtyFlushInterceptor implements Interceptor {
107+
static boolean dirtyFlushedForUser;
108+
109+
@Override
110+
public boolean onFlushDirty(
111+
Object entity,
112+
Object id,
113+
Object[] currentState,
114+
Object[] previousState,
115+
String[] propertyNames,
116+
Type[] types) throws CallbackException {
117+
118+
System.out.println( "onFlushDirty invoked on entity: " + entity.getClass().getSimpleName() );
119+
120+
dirtyFlushedForUser = entity instanceof DirtyFlushTest.User;
121+
122+
return false;
123+
}
124+
}
125+
126+
}

0 commit comments

Comments
 (0)