Skip to content

Commit ce7f77d

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

File tree

4 files changed

+247
-9
lines changed

4 files changed

+247
-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: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
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.orm.test.id;
8+
9+
import java.io.Serializable;
10+
11+
import org.hibernate.testing.TestForIssue;
12+
import org.hibernate.testing.orm.junit.EntityManagerFactoryScope;
13+
import org.hibernate.testing.orm.junit.Jpa;
14+
import org.junit.jupiter.api.Test;
15+
16+
import jakarta.persistence.Entity;
17+
import jakarta.persistence.GeneratedValue;
18+
import jakarta.persistence.Id;
19+
import jakarta.persistence.MappedSuperclass;
20+
import jakarta.persistence.metamodel.Attribute;
21+
import jakarta.persistence.metamodel.IdentifiableType;
22+
import jakarta.persistence.metamodel.Metamodel;
23+
import jakarta.persistence.metamodel.SingularAttribute;
24+
import org.hamcrest.MatcherAssert;
25+
26+
import static org.hamcrest.Matchers.is;
27+
import static org.hamcrest.Matchers.typeCompatibleWith;
28+
29+
/**
30+
* @author Nathan Xu
31+
*/
32+
@Jpa(annotatedClasses = {
33+
GenericsIdTypeTest.ConcreteEntityOne.class,
34+
GenericsIdTypeTest.ConcreteEntityTwo.class,
35+
})
36+
@TestForIssue( jiraKey = "HHH-15070" )
37+
class GenericsIdTypeTest {
38+
39+
@Test
40+
void test1(EntityManagerFactoryScope scope) {
41+
Metamodel metamodel = scope.getEntityManagerFactory().getMetamodel();
42+
assertIdentifierType(metamodel.entity(ConcreteEntityOne.class), Integer.class);
43+
}
44+
45+
@Test
46+
void test2(EntityManagerFactoryScope scope) {
47+
Metamodel metamodel = scope.getEntityManagerFactory().getMetamodel();
48+
assertIdentifierType(metamodel.entity(ConcreteEntityTwo.class), Integer.class);
49+
}
50+
51+
private static <T> void assertIdentifierType(IdentifiableType<T> entity, Class<?> idType) {
52+
53+
// Identifier type
54+
MatcherAssert.assertThat( entity.getIdType().getJavaType(), is( typeCompatibleWith( idType) ) );
55+
56+
// Identifier type by name
57+
Attribute<? super T, ?> attribute = entity.getAttribute( "id" );
58+
MatcherAssert.assertThat( attribute.getJavaType(), is( typeCompatibleWith(idType) ) );
59+
60+
// Identifier type by id attribute
61+
SingularAttribute<? super T, ?> idAttribute = entity.getId( entity.getIdType().getJavaType() );
62+
MatcherAssert.assertThat( idAttribute.getJavaType(), is( typeCompatibleWith(idType) ) );
63+
64+
}
65+
66+
@MappedSuperclass
67+
static abstract class AbstractEntity<PK extends Serializable> {
68+
private @Id
69+
@GeneratedValue PK id;
70+
}
71+
72+
@MappedSuperclass
73+
static abstract class AbstractIntermediate<T, PK extends Serializable> extends AbstractEntity<PK> {
74+
}
75+
76+
@Entity(name = "ConcreteEntityOne")
77+
static class ConcreteEntityOne extends AbstractEntity<Integer> {}
78+
79+
@Entity(name = "ConcreteEntityTwo")
80+
static class ConcreteEntityTwo extends AbstractIntermediate<ConcreteEntityTwo, Long> {}
81+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package org.hibernate.orm.test.onetoone.flush;
2+
3+
import org.hibernate.CallbackException;
4+
import org.hibernate.Interceptor;
5+
import org.hibernate.type.Type;
6+
7+
public class DirtyFlushInterceptor implements Interceptor {
8+
public static boolean dirtyFlushedForUser;
9+
10+
@Override
11+
public boolean onFlushDirty(
12+
Object entity,
13+
Object id,
14+
Object[] currentState,
15+
Object[] previousState,
16+
String[] propertyNames,
17+
Type[] types) throws CallbackException {
18+
19+
System.out.println( "onFlushDirty invoked on entity: " + entity.getClass().getSimpleName() );
20+
21+
dirtyFlushedForUser = entity instanceof DirtyFlushTest.User;
22+
23+
return false;
24+
}
25+
}
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
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+
93+
@Id
94+
int id;
95+
96+
int version;
97+
@OneToOne(mappedBy = "user")
98+
Profile profile;
99+
100+
}
101+
102+
@Entity(name = "Profile")
103+
static class Profile {
104+
105+
@Id
106+
int id;
107+
108+
@OneToOne // internally Hibernate will use `@ManyToOne` for this field
109+
User user;
110+
}
111+
112+
static class DirtyFlushInterceptor implements Interceptor {
113+
static boolean dirtyFlushedForUser;
114+
115+
@Override
116+
public boolean onFlushDirty(
117+
Object entity,
118+
Object id,
119+
Object[] currentState,
120+
Object[] previousState,
121+
String[] propertyNames,
122+
Type[] types) throws CallbackException {
123+
124+
System.out.println( "onFlushDirty invoked on entity: " + entity.getClass().getSimpleName() );
125+
126+
dirtyFlushedForUser = entity instanceof DirtyFlushTest.User;
127+
128+
return false;
129+
}
130+
}
131+
132+
}

0 commit comments

Comments
 (0)