Skip to content

Commit b03b25e

Browse files
committed
HHH-7180 Test and fix unnecessary collection cache eviction due to wrong key comparison
1 parent 75e7803 commit b03b25e

File tree

2 files changed

+152
-1
lines changed

2 files changed

+152
-1
lines changed

hibernate-core/src/main/java/org/hibernate/cache/internal/CollectionCacheInvalidator.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,7 @@ private void evictCache(Object entity, EntityPersister persister, EventSource se
126126
Object id = getIdentifier( session, ref );
127127

128128
// only evict if the related entity has changed
129-
if ( ( id != null && !id.equals( oldId ) ) || ( oldId != null && !oldId.equals( id ) ) ) {
129+
if ( ( id != null || oldId != null ) && !collectionPersister.getKeyType().isEqual( oldId, id ) ) {
130130
if ( id != null ) {
131131
evict( id, collectionPersister, session );
132132
}
Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
/*
2+
* SPDX-License-Identifier: LGPL-2.1-or-later
3+
* Copyright Red Hat Inc. and Hibernate Authors
4+
*/
5+
package org.hibernate.orm.test.cache;
6+
7+
import jakarta.persistence.CascadeType;
8+
import jakarta.persistence.Entity;
9+
import jakarta.persistence.FetchType;
10+
import jakarta.persistence.Id;
11+
import jakarta.persistence.ManyToOne;
12+
import jakarta.persistence.OneToMany;
13+
import org.hibernate.annotations.Cache;
14+
import org.hibernate.annotations.CacheConcurrencyStrategy;
15+
import org.hibernate.cfg.AvailableSettings;
16+
import org.hibernate.community.dialect.InformixDialect;
17+
import org.hibernate.dialect.MySQLDialect;
18+
import org.hibernate.dialect.OracleDialect;
19+
import org.hibernate.stat.CollectionStatistics;
20+
import org.hibernate.stat.spi.StatisticsImplementor;
21+
import org.hibernate.testing.orm.junit.DomainModel;
22+
import org.hibernate.testing.orm.junit.JiraKey;
23+
import org.hibernate.testing.orm.junit.ServiceRegistry;
24+
import org.hibernate.testing.orm.junit.SessionFactory;
25+
import org.hibernate.testing.orm.junit.SessionFactoryScope;
26+
import org.hibernate.testing.orm.junit.Setting;
27+
import org.hibernate.testing.orm.junit.SkipForDialect;
28+
import org.junit.jupiter.api.AfterEach;
29+
import org.junit.jupiter.api.BeforeEach;
30+
import org.junit.jupiter.api.Test;
31+
32+
import java.util.HashSet;
33+
import java.util.Set;
34+
35+
import static org.junit.jupiter.api.Assertions.assertEquals;
36+
37+
38+
@SkipForDialect(dialectClass = MySQLDialect.class, majorVersion = 5, reason = "BLOB/TEXT column 'id' used in key specification without a key length")
39+
@SkipForDialect(dialectClass = OracleDialect.class, matchSubTypes = true, reason = "ORA-02329: column of datatype LOB cannot be unique or a primary key")
40+
@SkipForDialect(dialectClass = InformixDialect.class, reason = "Informix does not support unique / primary constraints on binary columns")
41+
@DomainModel(
42+
annotatedClasses = {
43+
CollectionCacheEvictionComplexIdTest.Parent.class,
44+
CollectionCacheEvictionComplexIdTest.Child.class
45+
}
46+
)
47+
@SessionFactory
48+
@ServiceRegistry(
49+
settings = {
50+
@Setting(name = AvailableSettings.AUTO_EVICT_COLLECTION_CACHE, value = "true"),
51+
@Setting(name = AvailableSettings.USE_SECOND_LEVEL_CACHE, value = "true"),
52+
@Setting(name = AvailableSettings.USE_QUERY_CACHE, value = "true")
53+
}
54+
)
55+
public class CollectionCacheEvictionComplexIdTest {
56+
57+
@BeforeEach
58+
public void prepare(SessionFactoryScope scope) {
59+
scope.inTransaction(
60+
session -> {
61+
Parent entity = new Parent();
62+
entity.id = new byte[] {
63+
(byte) ( 1 ),
64+
(byte) ( 2 ),
65+
(byte) ( 3 ),
66+
(byte) ( 4 )
67+
};
68+
entity.name = "Simple name";
69+
70+
for ( int j = 1; j <= 2; j++ ) {
71+
Child child = new Child();
72+
child.id = j;
73+
entity.name = "Child name " + j;
74+
child.parent = entity;
75+
entity.children.add(child);
76+
}
77+
session.persist( entity );
78+
}
79+
);
80+
}
81+
82+
@AfterEach
83+
public void cleanup(SessionFactoryScope scope) {
84+
scope.inTransaction(
85+
session -> {
86+
session.createQuery( "delete from Child" ).executeUpdate();
87+
session.createQuery( "delete from Parent" ).executeUpdate();
88+
}
89+
);
90+
}
91+
92+
@Test
93+
@JiraKey(value = "HHH-7180")
94+
public void testEvict(SessionFactoryScope scope) {
95+
final StatisticsImplementor statistics = scope.getSessionFactory().getStatistics();
96+
statistics.setStatisticsEnabled( true );
97+
98+
Parent parent = scope.fromTransaction(
99+
session -> session.createQuery( "from Parent p", Parent.class ).getSingleResult()
100+
);
101+
scope.inTransaction(
102+
session -> {
103+
Parent p = session.createQuery( "from Parent p", Parent.class ).getSingleResult();
104+
Child child1 = p.children.iterator().next();
105+
child1.name = "Updated child";
106+
child1.parent = parent;
107+
}
108+
);
109+
110+
statistics.clear();
111+
scope.inTransaction(
112+
session -> {
113+
Parent p = session.createQuery( "from Parent p", Parent.class ).getSingleResult();
114+
final CollectionStatistics collectionStatistics = statistics.getCollectionStatistics(
115+
Parent.class.getName() + ".children" );
116+
assertEquals( 1, collectionStatistics.getCacheHitCount() );
117+
assertEquals( 0, collectionStatistics.getCacheMissCount() );
118+
}
119+
);
120+
121+
}
122+
123+
@Entity(name = "Parent")
124+
@Cache(usage = CacheConcurrencyStrategy.READ_WRITE)
125+
public static class Parent {
126+
@Id
127+
public byte[] id;
128+
public String name;
129+
@OneToMany(mappedBy = "parent", fetch = FetchType.EAGER, cascade = CascadeType.ALL)
130+
@Cache(usage = CacheConcurrencyStrategy.READ_WRITE)
131+
public Set<Child> children = new HashSet<>();
132+
}
133+
134+
@Entity(name = "Child")
135+
@Cache(usage = CacheConcurrencyStrategy.READ_WRITE)
136+
public static class Child {
137+
@Id
138+
public Integer id;
139+
public String name;
140+
@ManyToOne(fetch = FetchType.LAZY)
141+
public Parent parent;
142+
143+
public Child() {
144+
}
145+
146+
public Child(Integer id, Parent parent) {
147+
this.id = id;
148+
this.parent = parent;
149+
}
150+
}
151+
}

0 commit comments

Comments
 (0)