Skip to content

Commit 892e4e5

Browse files
committed
Guard against Hibernate exposing non-embeddables as embeddables on versions > 5.4.21.
Hibernate 5.4.22 started including types managed by custom user types in the embeddables exposed via JPA's Metamodel API. This causes those to be considered types to explicitly map (read: unfold) in Spring Data REST. We now exposed a cleaned up view on this via a tweak in JpaPersistentPropertyImpl (ultimately in JpaMetamodel) looking for @embeddable annotation on types allegedly considered embeddable by Hibernate. Fixes #2421.
1 parent 881bfbd commit 892e4e5

File tree

3 files changed

+95
-1
lines changed

3 files changed

+95
-1
lines changed

spring-data-jpa/src/main/java/org/springframework/data/jpa/mapping/JpaPersistentPropertyImpl.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@ public JpaPersistentPropertyImpl(JpaMetamodel metamodel, Property property,
109109

110110
this.isIdProperty = Lazy.of(() -> ID_ANNOTATIONS.stream().anyMatch(it -> isAnnotationPresent(it)) //
111111
|| metamodel.isSingleIdAttribute(getOwner().getType(), getName(), getType()));
112-
this.isEntity = Lazy.of(() -> metamodel.isJpaManaged(getActualType()));
112+
this.isEntity = Lazy.of(() -> metamodel.isMappedType(getActualType()));
113113
}
114114

115115
@Override

spring-data-jpa/src/main/java/org/springframework/data/jpa/util/JpaMetamodel.java

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,15 +16,20 @@
1616
package org.springframework.data.jpa.util;
1717

1818
import java.util.Collection;
19+
import java.util.EnumSet;
1920
import java.util.Map;
2021
import java.util.Optional;
22+
import java.util.Set;
2123
import java.util.concurrent.ConcurrentHashMap;
2224

25+
import jakarta.persistence.Embeddable;
2326
import jakarta.persistence.metamodel.EntityType;
2427
import jakarta.persistence.metamodel.ManagedType;
2528
import jakarta.persistence.metamodel.Metamodel;
2629
import jakarta.persistence.metamodel.SingularAttribute;
30+
import jakarta.persistence.metamodel.Type.PersistenceType;
2731

32+
import org.springframework.core.annotation.AnnotatedElementUtils;
2833
import org.springframework.data.util.Lazy;
2934
import org.springframework.data.util.StreamUtils;
3035
import org.springframework.util.Assert;
@@ -39,10 +44,13 @@
3944
public class JpaMetamodel {
4045

4146
private static final Map<Metamodel, JpaMetamodel> CACHE = new ConcurrentHashMap<>(4);
47+
private static final Set<PersistenceType> ENTITY_OR_MAPPED_SUPERCLASS = EnumSet.of(PersistenceType.ENTITY,
48+
PersistenceType.MAPPED_SUPERCLASS);
4249

4350
private final Metamodel metamodel;
4451

4552
private Lazy<Collection<Class<?>>> managedTypes;
53+
private Lazy<Collection<Class<?>>> jpaEmbeddables;
4654

4755
/**
4856
* Creates a new {@link JpaMetamodel} for the given JPA {@link Metamodel}.
@@ -54,10 +62,17 @@ private JpaMetamodel(Metamodel metamodel) {
5462
Assert.notNull(metamodel, "Metamodel must not be null!");
5563

5664
this.metamodel = metamodel;
65+
5766
this.managedTypes = Lazy.of(() -> metamodel.getManagedTypes().stream() //
5867
.map(ManagedType::getJavaType) //
5968
.filter(it -> it != null) //
6069
.collect(StreamUtils.toUnmodifiableSet()));
70+
71+
this.jpaEmbeddables = Lazy.of(() -> metamodel.getEmbeddables().stream() //
72+
.map(ManagedType::getJavaType)
73+
.filter(it -> it != null)
74+
.filter(it -> AnnotatedElementUtils.isAnnotated(it, Embeddable.class))
75+
.collect(StreamUtils.toUnmodifiableSet()));
6176
}
6277

6378
public static JpaMetamodel of(Metamodel metamodel) {
@@ -96,6 +111,27 @@ public boolean isSingleIdAttribute(Class<?> entity, String name, Class<?> attrib
96111
.orElse(false);
97112
}
98113

114+
/**
115+
* Returns whether the given type is considered a mapped type, i.e. an actually JPA persisted entity, mapped
116+
* superclass or native JPA embeddable.
117+
*
118+
* @param entity must not be {@literal null}.
119+
* @return
120+
*/
121+
public boolean isMappedType(Class<?> entity) {
122+
123+
Assert.notNull(entity, "Type must not be null!");
124+
125+
if (!isJpaManaged(entity)) {
126+
return false;
127+
}
128+
129+
ManagedType<?> managedType = metamodel.managedType(entity);
130+
131+
return !managedType.getPersistenceType().equals(PersistenceType.EMBEDDABLE)
132+
|| jpaEmbeddables.get().contains(entity);
133+
}
134+
99135
/**
100136
* Wipes the static cache of {@link Metamodel} to {@link JpaMetamodel}.
101137
*/

spring-data-jpa/src/test/java/org/springframework/data/jpa/util/JpaMetamodelUnitTests.java

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,18 @@
1818
import static org.assertj.core.api.Assertions.*;
1919
import static org.mockito.Mockito.*;
2020

21+
import java.util.Arrays;
2122
import java.util.Collections;
23+
import java.util.HashSet;
2224

2325
import jakarta.persistence.metamodel.EntityType;
26+
import jakarta.persistence.Embeddable;
27+
import jakarta.persistence.Entity;
28+
import jakarta.persistence.metamodel.EmbeddableType;
29+
import jakarta.persistence.metamodel.EntityType;
30+
import jakarta.persistence.metamodel.ManagedType;
2431
import jakarta.persistence.metamodel.Metamodel;
32+
import jakarta.persistence.metamodel.Type.PersistenceType;
2533

2634
import org.junit.jupiter.api.Test;
2735
import org.junit.jupiter.api.extension.ExtendWith;
@@ -60,4 +68,54 @@ void cacheIsEffectiveUnlessCleared() {
6068
JpaMetamodel.clear();
6169
assertThat(model).isNotEqualTo(JpaMetamodel.of(metamodel));
6270
}
71+
72+
@Test // #2421
73+
void doesNotConsiderNonNativeEmbeddablesJpaManaged() {
74+
75+
JpaMetamodel model = JpaMetamodel.of(metamodel);
76+
77+
ManagedType<?> entity = getEntity(Wrapper.class);
78+
ManagedType<?> embeddable = getEmbeddable(ExplicitEmbeddable.class);
79+
ManagedType<?> inner = getEmbeddable(Inner.class);
80+
81+
doReturn(new HashSet<>(Arrays.asList(entity, embeddable, inner))).when(metamodel).getManagedTypes();
82+
doReturn(new HashSet<>(Arrays.asList(embeddable, inner))).when(metamodel).getEmbeddables();
83+
84+
assertThat(model.isMappedType(Wrapper.class)).isTrue();
85+
assertThat(model.isMappedType(ExplicitEmbeddable.class)).isTrue();
86+
assertThat(model.isMappedType(Inner.class)).isFalse();
87+
}
88+
89+
private EmbeddableType<?> getEmbeddable(Class<?> type) {
90+
91+
EmbeddableType<?> managedType = getManagedType(type, EmbeddableType.class);
92+
doReturn(PersistenceType.EMBEDDABLE).when(managedType).getPersistenceType();
93+
94+
return managedType;
95+
}
96+
97+
private EntityType<?> getEntity(Class<?> type) {
98+
99+
EntityType<?> managedType = getManagedType(type, EntityType.class);
100+
doReturn(PersistenceType.ENTITY).when(managedType).getPersistenceType();
101+
102+
return managedType;
103+
}
104+
105+
private <T extends ManagedType<?>> T getManagedType(Class<?> type, Class<T> baseType) {
106+
107+
T managedType = mock(baseType);
108+
doReturn(type).when(managedType).getJavaType();
109+
doReturn(managedType).when(metamodel).managedType(type);
110+
111+
return managedType;
112+
}
113+
114+
@Entity
115+
static class Wrapper {}
116+
117+
@Embeddable
118+
static class ExplicitEmbeddable {}
119+
120+
static class Inner {}
63121
}

0 commit comments

Comments
 (0)