Skip to content

Commit f2afee7

Browse files
committed
HHH-2003 - Collections which are fetched AND restricted should not be written to second-level cache
1 parent ccba3f6 commit f2afee7

File tree

7 files changed

+362
-6
lines changed

7 files changed

+362
-6
lines changed

hibernate-core/src/main/java/org/hibernate/cache/spi/support/AbstractCachedDomainDataAccess.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
*/
77
package org.hibernate.cache.spi.support;
88

9+
import org.hibernate.Internal;
910
import org.hibernate.cache.spi.DomainDataRegion;
1011
import org.hibernate.cache.spi.access.CachedDomainDataAccess;
1112
import org.hibernate.cache.spi.access.SoftLock;
@@ -34,7 +35,8 @@ public DomainDataRegion getRegion() {
3435
return region;
3536
}
3637

37-
protected DomainDataStorageAccess getStorageAccess() {
38+
@Internal
39+
public DomainDataStorageAccess getStorageAccess() {
3840
return storageAccess;
3941
}
4042

Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
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.querycache;
8+
9+
import java.io.Serializable;
10+
11+
import org.hibernate.CacheMode;
12+
import org.hibernate.cache.internal.BasicCacheKeyImplementation;
13+
import org.hibernate.cache.spi.CacheImplementor;
14+
import org.hibernate.cache.spi.access.CollectionDataAccess;
15+
import org.hibernate.cache.spi.entry.CollectionCacheEntry;
16+
import org.hibernate.cache.spi.support.AbstractReadWriteAccess;
17+
import org.hibernate.cache.spi.support.CollectionReadWriteAccess;
18+
import org.hibernate.cache.spi.support.DomainDataStorageAccess;
19+
import org.hibernate.cfg.AvailableSettings;
20+
import org.hibernate.metamodel.model.domain.NavigableRole;
21+
22+
import org.hibernate.testing.cache.MapStorageAccessImpl;
23+
import org.hibernate.testing.orm.domain.StandardDomainModel;
24+
import org.hibernate.testing.orm.domain.library.Book;
25+
import org.hibernate.testing.orm.domain.library.Person;
26+
import org.hibernate.testing.orm.junit.DomainModel;
27+
import org.hibernate.testing.orm.junit.FailureExpected;
28+
import org.hibernate.testing.orm.junit.Jira;
29+
import org.hibernate.testing.orm.junit.ServiceRegistry;
30+
import org.hibernate.testing.orm.junit.SessionFactory;
31+
import org.hibernate.testing.orm.junit.SessionFactoryScope;
32+
import org.hibernate.testing.orm.junit.Setting;
33+
import org.junit.jupiter.api.AfterEach;
34+
import org.junit.jupiter.api.BeforeEach;
35+
import org.junit.jupiter.api.Test;
36+
37+
import jakarta.persistence.CacheStoreMode;
38+
import jakarta.persistence.SharedCacheMode;
39+
40+
import static org.assertj.core.api.Assertions.assertThat;
41+
import static org.assertj.core.api.Assertions.fail;
42+
43+
/**
44+
* Assertions that collections which are join fetched and restricted in a query do not get put into the
45+
* second level cache with the filtered state
46+
*
47+
* @author Steve Ebersole
48+
*/
49+
@SuppressWarnings("JUnitMalformedDeclaration")
50+
@Jira( "https://hibernate.atlassian.net/browse/HHH-2003" )
51+
@ServiceRegistry(settings = {
52+
@Setting( name= AvailableSettings.USE_SECOND_LEVEL_CACHE, value = "true"),
53+
@Setting( name= AvailableSettings.USE_QUERY_CACHE, value = "true"),
54+
})
55+
@DomainModel(standardModels = StandardDomainModel.LIBRARY, sharedCacheMode = SharedCacheMode.ALL)
56+
@SessionFactory
57+
public class QueryRestrictedCollectionCachingTests {
58+
public static final String AUTHORS_ROLE = Book.class.getName() + ".authors";
59+
60+
@Test
61+
void testSimpleFetch(SessionFactoryScope sessions) {
62+
final CacheImplementor cache = sessions.getSessionFactory().getCache();
63+
cache.evictAllRegions();
64+
65+
sessions.inTransaction( (session) -> {
66+
final Book book = session.createSelectionQuery( "from Book b left join fetch b.authors a", Book.class ).getSingleResult();
67+
assertThat( book ).isNotNull();
68+
assertThat( book.getAuthors() ).hasSize( 2 );
69+
} );
70+
71+
assertThat( cache.containsCollection( AUTHORS_ROLE, 1 ) ).isTrue();
72+
assertThat( extractCachedCollectionKeys( cache, AUTHORS_ROLE, 1 ) ).hasSize( 2 );
73+
}
74+
75+
@Test
76+
void testSimpleFetch2(SessionFactoryScope sessions) {
77+
final CacheImplementor cache = sessions.getSessionFactory().getCache();
78+
cache.evictAllRegions();
79+
80+
sessions.inTransaction( (session) -> {
81+
final Book book = session.createSelectionQuery(
82+
"from Book b left join fetch b.authors a where b.id = 1",
83+
Book.class
84+
).getSingleResult();
85+
assertThat( book ).isNotNull();
86+
assertThat( book.getAuthors() ).hasSize( 2 );
87+
} );
88+
89+
assertThat( cache.containsCollection( AUTHORS_ROLE, 1 ) ).isTrue();
90+
assertThat( extractCachedCollectionKeys( cache, AUTHORS_ROLE, 1 ) ).hasSize( 2 );
91+
}
92+
93+
@Test
94+
void testRestrictedFetchWithCacheIgnored(SessionFactoryScope sessions) {
95+
final CacheImplementor cache = sessions.getSessionFactory().getCache();
96+
cache.evictAllRegions();
97+
98+
sessions.inTransaction( (session) -> {
99+
final Book book = session
100+
.createSelectionQuery( "from Book b left join fetch b.authors a where a.id = 1", Book.class )
101+
.setCacheMode( CacheMode.IGNORE )
102+
.getSingleResult();
103+
assertThat( book ).isNotNull();
104+
assertThat( book.getAuthors() ).hasSize( 1 );
105+
} );
106+
107+
// we ignored the cache explicitly
108+
assertThat( cache.containsCollection( AUTHORS_ROLE, 1 ) ).isFalse();
109+
}
110+
111+
@Test
112+
@FailureExpected
113+
void testRestrictedFetch(SessionFactoryScope sessions) {
114+
final CacheImplementor cache = sessions.getSessionFactory().getCache();
115+
cache.evictAllRegions();
116+
117+
sessions.inTransaction( (session) -> {
118+
final Book book = session
119+
.createSelectionQuery( "from Book b left join fetch b.authors a where a.id = 1", Book.class )
120+
.getSingleResult();
121+
assertThat( book ).isNotNull();
122+
assertThat( book.getAuthors() ).hasSize( 1 );
123+
} );
124+
125+
// This is the crux of HHH-2003.
126+
// At the moment we put the filtered collection into the cache
127+
assertThat( cache.containsCollection( AUTHORS_ROLE, 1 ) ).isTrue();
128+
// this is just some deeper checks to show that the data is "corrupt"
129+
assertThat( extractCachedCollectionKeys( cache, AUTHORS_ROLE, 1 ) ).hasSize( 1 );
130+
131+
fail( "Really, HHH-2003 the collection to not be cached here" );
132+
}
133+
134+
private static Serializable[] extractCachedCollectionKeys(CacheImplementor cache, String role, Integer ownerKey) {
135+
final NavigableRole navigableRole = new NavigableRole( role );
136+
final CollectionReadWriteAccess authorsRegionAccess = (CollectionReadWriteAccess) cache.getCollectionRegionAccess( navigableRole );
137+
138+
final MapStorageAccessImpl storageAccess = (MapStorageAccessImpl) authorsRegionAccess.getStorageAccess();
139+
final BasicCacheKeyImplementation cacheKey = new BasicCacheKeyImplementation( ownerKey, role, ownerKey );
140+
final AbstractReadWriteAccess.Item cacheItem = (AbstractReadWriteAccess.Item) storageAccess.getFromData( cacheKey );
141+
assertThat( cacheItem ).isNotNull();
142+
143+
final CollectionCacheEntry cacheEntry = (CollectionCacheEntry) cacheItem.getValue();
144+
return cacheEntry.getState();
145+
}
146+
147+
@BeforeEach
148+
void createTestData(SessionFactoryScope sessions) {
149+
sessions.inTransaction( (session) -> {
150+
final Person poe = new Person( 1, "John Poe" );
151+
session.persist( poe );
152+
153+
final Person schmidt = new Person( 2, "Jacob Schmidt" );
154+
session.persist( schmidt );
155+
156+
final Person king = new Person( 3, "David King" );
157+
session.persist( king );
158+
159+
final Book nightsEdge = new Book( 1, "A Night's Edge" );
160+
nightsEdge.addAuthor( poe );
161+
nightsEdge.addAuthor( king );
162+
nightsEdge.addEditor( schmidt );
163+
session.persist( nightsEdge );
164+
} );
165+
}
166+
167+
@AfterEach
168+
void dropTestData(SessionFactoryScope sessions) {
169+
sessions.inTransaction( (session) -> {
170+
// session.createNativeMutationQuery( "delete book_authors" ).executeUpdate();
171+
// session.createNativeMutationQuery( "delete book_editors" ).executeUpdate();
172+
session.createMutationQuery( "delete Book" ).executeUpdate();
173+
session.createMutationQuery( "delete Person" ).executeUpdate();
174+
} );
175+
}
176+
}

hibernate-testing/src/main/java/org/hibernate/testing/cache/MapStorageAccessImpl.java

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import java.util.concurrent.ConcurrentHashMap;
1010
import java.util.concurrent.ConcurrentMap;
1111

12+
import org.hibernate.Internal;
1213
import org.hibernate.cache.spi.support.DomainDataStorageAccess;
1314
import org.hibernate.engine.spi.SharedSessionContractImplementor;
1415

@@ -20,17 +21,22 @@
2021
public class MapStorageAccessImpl implements DomainDataStorageAccess {
2122
private ConcurrentMap data;
2223

24+
@Internal
25+
public Object getFromData(Object key) {
26+
if ( data == null ) {
27+
return null;
28+
}
29+
return data.get( key );
30+
}
31+
2332
@Override
2433
public boolean contains(Object key) {
2534
return data != null && data.containsKey( key );
2635
}
2736

2837
@Override
2938
public Object getFromCache(Object key, SharedSessionContractImplementor session) {
30-
if ( data == null ) {
31-
return null;
32-
}
33-
return data.get( key );
39+
return getFromData( key );
3440
}
3541

3642
@Override

hibernate-testing/src/main/java/org/hibernate/testing/orm/domain/StandardDomainModel.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import org.hibernate.testing.orm.domain.contacts.ContactsDomainModel;
1111
import org.hibernate.testing.orm.domain.gambit.GambitDomainModel;
1212
import org.hibernate.testing.orm.domain.helpdesk.HelpDeskDomainModel;
13+
import org.hibernate.testing.orm.domain.library.LibraryDomainModel;
1314
import org.hibernate.testing.orm.domain.retail.RetailDomainModel;
1415
import org.hibernate.testing.orm.domain.userguide.UserguideDomainModel;
1516

@@ -22,7 +23,8 @@ public enum StandardDomainModel {
2223
GAMBIT( GambitDomainModel.INSTANCE ),
2324
HELPDESK( HelpDeskDomainModel.INSTANCE ),
2425
RETAIL( RetailDomainModel.INSTANCE ),
25-
USERGUIDE( UserguideDomainModel.INSTANCE );
26+
USERGUIDE( UserguideDomainModel.INSTANCE ),
27+
LIBRARY( LibraryDomainModel.INSTANCE );
2628

2729
private final DomainModelDescriptor domainModel;
2830

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
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.testing.orm.domain.library;
8+
9+
import java.util.HashSet;
10+
import java.util.Set;
11+
12+
import org.hibernate.annotations.Cache;
13+
import org.hibernate.annotations.CacheConcurrencyStrategy;
14+
15+
import jakarta.persistence.CollectionTable;
16+
import jakarta.persistence.Entity;
17+
import jakarta.persistence.Id;
18+
import jakarta.persistence.Basic;
19+
import jakarta.persistence.JoinColumn;
20+
import jakarta.persistence.JoinTable;
21+
import jakarta.persistence.ManyToMany;
22+
23+
/**
24+
* @author Steve Ebersole
25+
*/
26+
@Entity
27+
public class Book {
28+
@Id
29+
private Integer id;
30+
private String name;
31+
private String isbn;
32+
33+
@ManyToMany
34+
@JoinTable( name = "book_authors",
35+
joinColumns = @JoinColumn(name = "book_fk"),
36+
inverseJoinColumns = @JoinColumn(name = "author_fk")
37+
)
38+
@Cache( usage = CacheConcurrencyStrategy.READ_WRITE)
39+
Set<Person> authors;
40+
41+
@ManyToMany
42+
@JoinTable( name = "book_editors",
43+
joinColumns = @JoinColumn(name = "book_fk"),
44+
inverseJoinColumns = @JoinColumn(name = "editor_fk")
45+
)
46+
@Cache( usage = CacheConcurrencyStrategy.READ_WRITE)
47+
Set<Person> editors;
48+
49+
protected Book() {
50+
// for Hibernate use
51+
}
52+
53+
public Book(Integer id, String name) {
54+
this.id = id;
55+
this.name = name;
56+
}
57+
58+
public Integer getId() {
59+
return id;
60+
}
61+
62+
public String getName() {
63+
return name;
64+
}
65+
66+
public void setName(String name) {
67+
this.name = name;
68+
}
69+
70+
public String getIsbn() {
71+
return isbn;
72+
}
73+
74+
public void setIsbn(String isbn) {
75+
this.isbn = isbn;
76+
}
77+
78+
public Set<Person> getAuthors() {
79+
return authors;
80+
}
81+
82+
public void setAuthors(Set<Person> authors) {
83+
this.authors = authors;
84+
}
85+
86+
public void addAuthor(Person author) {
87+
if ( authors == null ) {
88+
authors = new HashSet<>();
89+
}
90+
authors.add( author );
91+
}
92+
93+
public Set<Person> getEditors() {
94+
return editors;
95+
}
96+
97+
public void setEditors(Set<Person> editors) {
98+
this.editors = editors;
99+
}
100+
101+
public void addEditor(Person editor) {
102+
if ( editors == null ) {
103+
editors = new HashSet<>();
104+
}
105+
editors.add( editor );
106+
}
107+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
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.testing.orm.domain.library;
8+
9+
import org.hibernate.testing.orm.domain.AbstractDomainModelDescriptor;
10+
11+
/**
12+
* @author Steve Ebersole
13+
*/
14+
public class LibraryDomainModel extends AbstractDomainModelDescriptor {
15+
public static final LibraryDomainModel INSTANCE = new LibraryDomainModel();
16+
17+
public LibraryDomainModel() {
18+
super( Book.class, Person.class );
19+
}
20+
}

0 commit comments

Comments
 (0)