Skip to content

Commit 6be7d76

Browse files
committed
HHH-16383 - NaturalIdClass
HHH-7287 - Problem in caching proper natural-id-values when obtaining result by naturalIdQuery
1 parent 9424258 commit 6be7d76

File tree

2 files changed

+305
-2
lines changed

2 files changed

+305
-2
lines changed

hibernate-core/src/main/java/org/hibernate/engine/internal/NaturalIdResolutionsImpl.java

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@
3131
import static org.hibernate.engine.internal.CacheHelper.fromSharedCache;
3232
import static org.hibernate.engine.internal.NaturalIdLogging.NATURAL_ID_LOGGER;
3333

34-
class NaturalIdResolutionsImpl implements NaturalIdResolutions, Serializable {
34+
public class NaturalIdResolutionsImpl implements NaturalIdResolutions, Serializable {
3535

3636
private final StatefulPersistenceContext persistenceContext;
3737
private final ConcurrentHashMap<EntityMappingType, EntityResolutions> resolutionsByEntity = new ConcurrentHashMap<>();
@@ -54,6 +54,14 @@ private SharedSessionContractImplementor session() {
5454
return persistenceContext.getSession();
5555
}
5656

57+
public EntityResolutions getEntityResolutions(EntityMappingType entityMappingType) {
58+
return resolutionsByEntity.get( entityMappingType );
59+
}
60+
61+
public EntityResolutions getEntityResolutions(Class<?> entityType) {
62+
return getEntityResolutions( session().getFactory().getMappingMetamodel().getEntityDescriptor( entityType ) );
63+
}
64+
5765
@Override
5866
public boolean cacheResolution(Object id, Object naturalId, EntityMappingType entityDescriptor) {
5967
validateNaturalId( entityDescriptor, naturalId );
@@ -709,7 +717,7 @@ public void clear() {
709717
/**
710718
* Represents the entity-specific cross-reference cache.
711719
*/
712-
private static class EntityResolutions implements Serializable {
720+
public static class EntityResolutions implements Serializable {
713721
private final PersistenceContext persistenceContext;
714722

715723
private final EntityMappingType entityDescriptor;
@@ -732,6 +740,25 @@ public EntityPersister getPersister() {
732740
return getEntityDescriptor().getEntityPersister();
733741
}
734742

743+
/**
744+
* Used for testing.
745+
*/
746+
public Resolution getResolutionByPk(Object pk) {
747+
return pkToNaturalIdMap.get( pk );
748+
}
749+
750+
/**
751+
* Used for testing.
752+
*/
753+
public Object getIdResolutionByNaturalId(Object naturalId) {
754+
for ( var entry : pkToNaturalIdMap.entrySet() ) {
755+
if ( entry.getValue().getNaturalIdValue().equals( naturalId ) ) {
756+
return entry.getKey();
757+
}
758+
}
759+
return null;
760+
}
761+
735762
public boolean sameAsCached(Object pk, Object naturalIdValues) {
736763
if ( pk == null ) {
737764
return false;
Lines changed: 276 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,276 @@
1+
/*
2+
* SPDX-License-Identifier: Apache-2.0
3+
* Copyright Red Hat Inc. and Hibernate Authors
4+
*/
5+
package org.hibernate.orm.test.mapping.naturalid;
6+
7+
import jakarta.persistence.Entity;
8+
import jakarta.persistence.Id;
9+
import jakarta.persistence.Table;
10+
import org.hibernate.annotations.NaturalId;
11+
import org.hibernate.annotations.NaturalIdCache;
12+
import org.hibernate.cache.internal.NaturalIdCacheKey;
13+
import org.hibernate.cache.spi.CacheImplementor;
14+
import org.hibernate.cache.spi.support.AbstractReadWriteAccess;
15+
import org.hibernate.cache.spi.support.DomainDataRegionTemplate;
16+
import org.hibernate.cfg.CacheSettings;
17+
import org.hibernate.engine.internal.NaturalIdResolutionsImpl;
18+
import org.hibernate.engine.spi.SessionImplementor;
19+
import org.hibernate.testing.jdbc.SQLStatementInspector;
20+
import org.hibernate.testing.orm.junit.DomainModel;
21+
import org.hibernate.testing.orm.junit.ServiceRegistry;
22+
import org.hibernate.testing.orm.junit.SessionFactory;
23+
import org.hibernate.testing.orm.junit.SessionFactoryScope;
24+
import org.hibernate.testing.orm.junit.Setting;
25+
import org.junit.jupiter.api.AfterEach;
26+
import org.junit.jupiter.api.BeforeEach;
27+
import org.junit.jupiter.api.Test;
28+
29+
import static org.assertj.core.api.Assertions.assertThat;
30+
import static org.hibernate.KeyType.NATURAL;
31+
32+
/// Tests for [org.hibernate.engine.spi.NaturalIdResolutions]
33+
///
34+
/// @author Steve Ebersole
35+
@SuppressWarnings("JUnitMalformedDeclaration")
36+
@ServiceRegistry(settings = @Setting( name = CacheSettings.USE_SECOND_LEVEL_CACHE, value = "true" ) )
37+
@DomainModel(annotatedClasses = {XRefTests.Book.class,XRefTests.Bookmark.class,XRefTests.Pen.class})
38+
@SessionFactory(useCollectingStatementInspector = true)
39+
public class XRefTests {
40+
public static final String BOOK_ISBN = "123-4567-890";
41+
public static final String BOOKMARK_SKU = "98-abc-7654-def";
42+
public static final String PEN_SKU = "98-xyz-7234";
43+
44+
@Test
45+
void testLocalResolution(SessionFactoryScope factoryScope) {
46+
final SQLStatementInspector sqlCollector = factoryScope.getCollectingStatementInspector();
47+
sqlCollector.clear();
48+
49+
factoryScope.inTransaction( (session) -> {
50+
final NaturalIdResolutionsImpl naturalIdResolutions = (NaturalIdResolutionsImpl) session.getPersistenceContext().getNaturalIdResolutions();
51+
assertThat( naturalIdResolutions.getEntityResolutions( Book.class ) ).isNull();
52+
53+
session.find( XRefTests.Book.class, BOOK_ISBN, NATURAL );
54+
assertThat( sqlCollector.getSqlQueries() ).hasSize( 1 );
55+
verifyLocalResolution( Book.class, BOOK_ISBN, 1, session );
56+
57+
sqlCollector.clear();
58+
59+
session.find( XRefTests.Book.class, BOOK_ISBN, NATURAL );
60+
assertThat( sqlCollector.getSqlQueries() ).isEmpty();
61+
verifyLocalResolution( Book.class, BOOK_ISBN, 1, session );
62+
} );
63+
}
64+
65+
@Test
66+
void testLocalResolutionWithCache(SessionFactoryScope factoryScope) {
67+
final SQLStatementInspector sqlCollector = factoryScope.getCollectingStatementInspector();
68+
sqlCollector.clear();
69+
70+
factoryScope.getSessionFactory().getCache().evictAllRegions();
71+
72+
factoryScope.inTransaction( (session) -> {
73+
verifyEmptyLocalResolution( Bookmark.class, session );
74+
verifyEmptyCacheResolution( Bookmark.class, BOOKMARK_SKU, session );
75+
76+
session.find( Bookmark.class, BOOKMARK_SKU, NATURAL );
77+
assertThat( sqlCollector.getSqlQueries() ).hasSize( 1 );
78+
verifyLocalResolution( Bookmark.class, BOOKMARK_SKU, 1, session );
79+
verifyCacheResolution( Bookmark.class, BOOKMARK_SKU, 1, session );
80+
81+
sqlCollector.clear();
82+
83+
session.find( Bookmark.class, BOOKMARK_SKU, NATURAL );
84+
assertThat( sqlCollector.getSqlQueries() ).isEmpty();
85+
verifyLocalResolution( Bookmark.class, BOOKMARK_SKU, 1, session );
86+
verifyCacheResolution( Bookmark.class, BOOKMARK_SKU, 1, session );
87+
} );
88+
89+
factoryScope.inTransaction( (session) -> {
90+
verifyCacheResolution( Bookmark.class, BOOKMARK_SKU, 1, session );
91+
verifyEmptyLocalResolution( Bookmark.class, session );
92+
} );
93+
}
94+
95+
@Test
96+
void testLocalResolutionWithMutableCache(SessionFactoryScope factoryScope) {
97+
final SQLStatementInspector sqlCollector = factoryScope.getCollectingStatementInspector();
98+
sqlCollector.clear();
99+
100+
factoryScope.getSessionFactory().getCache().evictAllRegions();
101+
102+
factoryScope.inTransaction( (session) -> {
103+
verifyEmptyLocalResolution( Pen.class, session );
104+
verifyEmptyCacheResolution( Pen.class, PEN_SKU, session );
105+
106+
session.find( XRefTests.Pen.class, PEN_SKU, NATURAL );
107+
assertThat( sqlCollector.getSqlQueries() ).hasSize( 1 );
108+
verifyLocalResolution( Pen.class, PEN_SKU, 1, session );
109+
verifyCacheResolution( Pen.class, PEN_SKU, 1, session );
110+
111+
sqlCollector.clear();
112+
113+
session.find( XRefTests.Pen.class, PEN_SKU, NATURAL );
114+
assertThat( sqlCollector.getSqlQueries() ).isEmpty();
115+
verifyLocalResolution( Pen.class, PEN_SKU, 1, session );
116+
verifyCacheResolution( Pen.class, PEN_SKU, 1, session );
117+
} );
118+
119+
factoryScope.inTransaction( (session) -> {
120+
verifyCacheResolution( Pen.class, PEN_SKU, 1, session );
121+
verifyEmptyLocalResolution( Pen.class, session );
122+
} );
123+
}
124+
125+
@Test
126+
void testCrossRefManagementWithMutation(SessionFactoryScope factoryScope) {
127+
final SQLStatementInspector sqlCollector = factoryScope.getCollectingStatementInspector();
128+
sqlCollector.clear();
129+
130+
final String updatedSku = "987-123-654";
131+
factoryScope.inTransaction( (session) -> {
132+
verifyCacheResolution( Pen.class, PEN_SKU, 1, session );
133+
verifyEmptyLocalResolution( Pen.class, session );
134+
135+
session.find( Pen.class, PEN_SKU, NATURAL );
136+
verifyLocalResolution( Pen.class, PEN_SKU, 1, session );
137+
138+
factoryScope.inTransaction( (session2) -> {
139+
session2.find( Pen.class, 1 ).sku = updatedSku;
140+
} );
141+
142+
// the point here -
143+
// 1. The local (Session) cache maintains repeatable-read
144+
// 2. The shared (L2C) cache sees the updated xref
145+
verifyLocalResolution( Pen.class, PEN_SKU, 1, session );
146+
verifyCacheResolution( Pen.class, updatedSku, 1, session );
147+
148+
sqlCollector.clear();
149+
150+
session.find( Pen.class, PEN_SKU, NATURAL );
151+
assertThat( sqlCollector.getSqlQueries() ).isEmpty();
152+
verifyLocalResolution( Pen.class, PEN_SKU, 1, session );
153+
verifyCacheResolution( Pen.class, updatedSku, 1, session );
154+
} );
155+
156+
factoryScope.inTransaction( (session) -> {
157+
var nonExistent = session.find( Pen.class, PEN_SKU, NATURAL );
158+
assertThat( nonExistent ).isNull();
159+
160+
var updated = session.find( Pen.class, updatedSku, NATURAL );
161+
assertThat( updated ).isNotNull();
162+
163+
verifyLocalResolution( Pen.class, updatedSku, 1, session );
164+
verifyCacheResolution( Pen.class, updatedSku, 1, session );
165+
} );
166+
}
167+
168+
private void verifyEmptyLocalResolution(Class<?> entityType, SessionImplementor session) {
169+
final NaturalIdResolutionsImpl naturalIdResolutions = (NaturalIdResolutionsImpl) session.getPersistenceContext().getNaturalIdResolutions();
170+
assertThat( naturalIdResolutions.getEntityResolutions( entityType ) ).isNull();
171+
}
172+
173+
private void verifyLocalResolution(Class<?> entityType, String naturalId, int id, SessionImplementor session) {
174+
final NaturalIdResolutionsImpl naturalIdResolutions = (NaturalIdResolutionsImpl) session.getPersistenceContext().getNaturalIdResolutions();
175+
176+
var entityResolutions = naturalIdResolutions.getEntityResolutions( entityType );
177+
assertThat( entityResolutions ).isNotNull();
178+
assertThat( entityResolutions.getResolutionByPk( id ).getNaturalIdValue() ).isEqualTo( naturalId );
179+
assertThat( entityResolutions.getIdResolutionByNaturalId( naturalId ) ).isEqualTo( id );
180+
}
181+
182+
private void verifyEmptyCacheResolution(Class<?> entityType, Object naturalId, SessionImplementor session) {
183+
final CacheImplementor cache = session.getSessionFactory().getCache();
184+
final DomainDataRegionTemplate region = (DomainDataRegionTemplate) cache.getRegion( entityType.getName() + "##NaturalId" );
185+
// region should get created on bootstrap
186+
assertThat( region ).isNotNull();
187+
// however, we should have no cached resolutions
188+
assertThat( region.getCacheStorageAccess().contains( naturalId ) ).isFalse();
189+
}
190+
191+
private void verifyCacheResolution(Class<?> entityType, String naturalId, int id, SessionImplementor session) {
192+
final CacheImplementor cache = session.getSessionFactory().getCache();
193+
final DomainDataRegionTemplate region = (DomainDataRegionTemplate) cache.getRegion( entityType.getName() + "##NaturalId" );
194+
var storage = region.getCacheStorageAccess();
195+
var entityDescriptor = session.getFactory().getMappingMetamodel().getEntityDescriptor( entityType.getName() );
196+
var resolutionKey = NaturalIdCacheKey.from( naturalId, entityDescriptor, entityDescriptor.getEntityName(), session );
197+
var resolution = (AbstractReadWriteAccess.Item) storage.getFromCache( resolutionKey, session );
198+
assertThat( resolution.getValue() ).isEqualTo( id );
199+
}
200+
201+
@BeforeEach
202+
void createTestData(SessionFactoryScope factoryScope) {
203+
factoryScope.inTransaction( (session) -> {
204+
session.persist( new Book( 1, "The Grapes of Wrath", BOOK_ISBN ) );
205+
session.persist( new Bookmark( 1, "The Keeper", BOOKMARK_SKU ) );
206+
session.persist( new Pen( 1, "G2", PEN_SKU ) );
207+
} );
208+
}
209+
210+
@AfterEach
211+
void dropTestData(SessionFactoryScope factoryScope) {
212+
factoryScope.dropData();
213+
}
214+
215+
@SuppressWarnings({"FieldCanBeLocal", "unused"})
216+
@Entity(name="Book")
217+
@Table(name="books")
218+
public static class Book {
219+
@Id
220+
private Integer id;
221+
private String title;
222+
@NaturalId
223+
private String isbn;
224+
225+
public Book() {
226+
}
227+
228+
public Book(Integer id, String title, String isbn) {
229+
this.id = id;
230+
this.title = title;
231+
this.isbn = isbn;
232+
}
233+
}
234+
235+
@SuppressWarnings({"FieldCanBeLocal", "unused"})
236+
@Entity(name="Bookmark")
237+
@Table(name="bookmarks")
238+
@NaturalIdCache
239+
public static class Bookmark {
240+
@Id
241+
private Integer id;
242+
private String manufacturer;
243+
@NaturalId
244+
private String sku;
245+
246+
public Bookmark() {
247+
}
248+
249+
public Bookmark(Integer id, String manufacturer, String sku) {
250+
this.id = id;
251+
this.manufacturer = manufacturer;
252+
this.sku = sku;
253+
}
254+
}
255+
256+
@SuppressWarnings({"FieldCanBeLocal", "unused"})
257+
@Entity(name="Pen")
258+
@Table(name="pens")
259+
@NaturalIdCache
260+
public static class Pen {
261+
@Id
262+
private Integer id;
263+
private String manufacturer;
264+
@NaturalId(mutable = true)
265+
private String sku;
266+
267+
public Pen() {
268+
}
269+
270+
public Pen(Integer id, String manufacturer, String sku) {
271+
this.id = id;
272+
this.manufacturer = manufacturer;
273+
this.sku = sku;
274+
}
275+
}
276+
}

0 commit comments

Comments
 (0)