Skip to content

Commit 5582d1e

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

File tree

3 files changed

+152
-10
lines changed

3 files changed

+152
-10
lines changed

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -385,7 +385,7 @@ public boolean isEqual(Object x, Object y, SessionFactoryImplementor factory) {
385385
/**
386386
* Resolve an identifier or unique key value
387387
*/
388-
private Object resolve(Object value, SharedSessionContractImplementor session, Object owner) {
388+
protected Object resolve(Object value, SharedSessionContractImplementor session, Object owner) {
389389
if ( value != null && !isNull( owner, session ) ) {
390390
if ( isReferenceToPrimaryKey() ) {
391391
return resolveIdentifier( value, session, null );

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

Lines changed: 15 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
@@ -159,6 +154,12 @@ public Serializable disassemble(Object value, SharedSessionContractImplementor s
159154

160155
@Override
161156
public Object assemble(Serializable oid, SharedSessionContractImplementor session, Object owner) throws HibernateException {
157+
if ( oid == null ) {
158+
if ( uniqueKeyPropertyName != null ) {
159+
return resolve( session.getContextEntityIdentifier( owner ), session, owner );
160+
}
161+
return null;
162+
}
162163

163164
//the owner of the association is not the owner of the id
164165
Object id = getIdentifierType( session ).assemble( oid, session, null );
@@ -169,9 +170,14 @@ public Object assemble(Serializable oid, SharedSessionContractImplementor sessio
169170

170171
return resolveIdentifier( id, session );
171172
}
172-
173+
174+
/**
175+
* We don't need to dirty check one-to-one because of how
176+
* assemble/disassemble is implemented and because a one-to-one
177+
* association is never dirty
178+
*/
173179
@Override
174180
public boolean isAlwaysDirtyChecked() {
175-
return true;
181+
return false;
176182
}
177183
}
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
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.AfterEach;
18+
import org.junit.jupiter.api.Assertions;
19+
import org.junit.jupiter.api.BeforeEach;
20+
import org.junit.jupiter.api.Test;
21+
22+
import jakarta.persistence.Entity;
23+
import jakarta.persistence.EntityManagerFactory;
24+
import jakarta.persistence.Id;
25+
import jakarta.persistence.OneToOne;
26+
27+
/**
28+
* @author Nathan Xu
29+
*/
30+
@TestForIssue( jiraKey = "HHH-15045" )
31+
class DirtyFlushTest {
32+
33+
List<Class> getAnnotatedClasses() {
34+
return List.of( User.class, Profile.class );
35+
}
36+
37+
Map basicSettings() {
38+
return SettingsGenerator.generateSettings(
39+
AvailableSettings.HBM2DDL_AUTO, "create-drop",
40+
AvailableSettings.DIALECT, DialectContext.getDialect().getClass().getName(),
41+
AvailableSettings.LOADED_CLASSES, getAnnotatedClasses(),
42+
AvailableSettings.GLOBALLY_QUOTED_IDENTIFIERS, "true"
43+
);
44+
}
45+
46+
EntityManagerFactory buildEntityManagerFactory(Map settings) {
47+
return Bootstrap
48+
.getEntityManagerFactoryBuilder( new PersistenceUnitDescriptorAdapter(), settings )
49+
.build();
50+
}
51+
52+
EntityManagerFactory entityManagerFactory;
53+
54+
@BeforeEach
55+
void setUp() {
56+
DirtyFlushInterceptor.dirtyFlushedForUser = false;
57+
}
58+
59+
@Test
60+
void testDirtyFlushNotHappened() {
61+
var settings = basicSettings();
62+
settings.put( AvailableSettings.INTERCEPTOR, new DirtyFlushInterceptor() );
63+
entityManagerFactory = buildEntityManagerFactory( settings );
64+
var em = entityManagerFactory.createEntityManager();
65+
66+
TransactionUtil.inTransaction( em, entityManager -> {
67+
var user = new User();
68+
user.id = 1;
69+
entityManager.persist( user );
70+
} );
71+
72+
try {
73+
TransactionUtil.inTransaction( em, entityManager -> {
74+
var user = entityManager.find( User.class, 1 );
75+
var profile = new Profile();
76+
profile.id = 1;
77+
profile.user = user;
78+
user.profile = profile;
79+
entityManager.persist( profile );
80+
} );
81+
82+
Assertions.assertFalse( DirtyFlushInterceptor.dirtyFlushedForUser, "User should not be dirty-flushed when only Profile changes!" );
83+
84+
} finally {
85+
TransactionUtil.inTransaction( em, entityManager -> {
86+
entityManager.createQuery( "delete from Profile " ).executeUpdate();
87+
entityManager.createQuery( "delete from User" ).executeUpdate();
88+
} );
89+
}
90+
}
91+
92+
@AfterEach
93+
void releaseResources() {
94+
if ( entityManagerFactory != null ) {
95+
entityManagerFactory.close();
96+
}
97+
}
98+
99+
100+
@Entity(name = "User")
101+
static class User {
102+
@Id int id;
103+
104+
@OneToOne(mappedBy = "user")
105+
Profile profile;
106+
}
107+
108+
@Entity(name = "Profile")
109+
static class Profile {
110+
@Id int id;
111+
112+
@OneToOne // internally Hibernate will use `@ManyToOne` for this field
113+
User user;
114+
}
115+
116+
static class DirtyFlushInterceptor implements Interceptor {
117+
static boolean dirtyFlushedForUser;
118+
119+
@Override
120+
public boolean onFlushDirty(
121+
Object entity,
122+
Object id,
123+
Object[] currentState,
124+
Object[] previousState,
125+
String[] propertyNames,
126+
Type[] types) throws CallbackException {
127+
128+
System.out.println( "onFlushDirty invoked on entity: " + entity.getClass().getSimpleName() );
129+
130+
dirtyFlushedForUser = entity instanceof DirtyFlushTest.User;
131+
132+
return false;
133+
}
134+
}
135+
136+
}

0 commit comments

Comments
 (0)