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.
This commit is contained in:
Oliver Drotbohm
2022-01-26 22:52:33 +01:00
parent 47f36f6ba2
commit 3cd3f151b5
3 changed files with 94 additions and 1 deletions

View File

@@ -109,7 +109,7 @@ class JpaPersistentPropertyImpl extends AnnotationBasedPersistentProperty<JpaPer
this.isIdProperty = Lazy.of(() -> ID_ANNOTATIONS.stream().anyMatch(it -> isAnnotationPresent(it)) //
|| metamodel.isSingleIdAttribute(getOwner().getType(), getName(), getType()));
this.isEntity = Lazy.of(() -> metamodel.isJpaManaged(getActualType()));
this.isEntity = Lazy.of(() -> metamodel.isMappedType(getActualType()));
}
/*

View File

@@ -16,15 +16,20 @@
package org.springframework.data.jpa.util;
import java.util.Collection;
import java.util.EnumSet;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import javax.persistence.Embeddable;
import javax.persistence.metamodel.EntityType;
import javax.persistence.metamodel.ManagedType;
import javax.persistence.metamodel.Metamodel;
import javax.persistence.metamodel.SingularAttribute;
import javax.persistence.metamodel.Type.PersistenceType;
import org.springframework.core.annotation.AnnotatedElementUtils;
import org.springframework.data.util.Lazy;
import org.springframework.data.util.StreamUtils;
import org.springframework.util.Assert;
@@ -39,10 +44,13 @@ import org.springframework.util.Assert;
public class JpaMetamodel {
private static final Map<Metamodel, JpaMetamodel> CACHE = new ConcurrentHashMap<>(4);
private static final Set<PersistenceType> ENTITY_OR_MAPPED_SUPERCLASS = EnumSet.of(PersistenceType.ENTITY,
PersistenceType.MAPPED_SUPERCLASS);
private final Metamodel metamodel;
private Lazy<Collection<Class<?>>> managedTypes;
private Lazy<Collection<Class<?>>> jpaEmbeddables;
/**
* Creates a new {@link JpaMetamodel} for the given JPA {@link Metamodel}.
@@ -54,10 +62,17 @@ public class JpaMetamodel {
Assert.notNull(metamodel, "Metamodel must not be null!");
this.metamodel = metamodel;
this.managedTypes = Lazy.of(() -> metamodel.getManagedTypes().stream() //
.map(ManagedType::getJavaType) //
.filter(it -> it != null) //
.collect(StreamUtils.toUnmodifiableSet()));
this.jpaEmbeddables = Lazy.of(() -> metamodel.getEmbeddables().stream() //
.map(ManagedType::getJavaType)
.filter(it -> it != null)
.filter(it -> AnnotatedElementUtils.isAnnotated(it, Embeddable.class))
.collect(StreamUtils.toUnmodifiableSet()));
}
public static JpaMetamodel of(Metamodel metamodel) {
@@ -96,6 +111,27 @@ public class JpaMetamodel {
.orElse(false);
}
/**
* Returns whether the given type is considered a mapped type, i.e. an actually JPA persisted entity, mapped
* superclass or native JPA embeddable.
*
* @param entity must not be {@literal null}.
* @return
*/
public boolean isMappedType(Class<?> entity) {
Assert.notNull(entity, "Type must not be null!");
if (!isJpaManaged(entity)) {
return false;
}
ManagedType<?> managedType = metamodel.managedType(entity);
return !managedType.getPersistenceType().equals(PersistenceType.EMBEDDABLE)
|| jpaEmbeddables.get().contains(entity);
}
/**
* Wipes the static cache of {@link Metamodel} to {@link JpaMetamodel}.
*/

View File

@@ -18,10 +18,17 @@ package org.springframework.data.jpa.util;
import static org.assertj.core.api.Assertions.*;
import static org.mockito.Mockito.*;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import javax.persistence.Embeddable;
import javax.persistence.Entity;
import javax.persistence.metamodel.EmbeddableType;
import javax.persistence.metamodel.EntityType;
import javax.persistence.metamodel.ManagedType;
import javax.persistence.metamodel.Metamodel;
import javax.persistence.metamodel.Type.PersistenceType;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
@@ -60,4 +67,54 @@ class JpaMetamodelUnitTests {
JpaMetamodel.clear();
assertThat(model).isNotEqualTo(JpaMetamodel.of(metamodel));
}
@Test // #2421
void doesNotConsiderNonNativeEmbeddablesJpaManaged() {
JpaMetamodel model = JpaMetamodel.of(metamodel);
ManagedType<?> entity = getEntity(Wrapper.class);
ManagedType<?> embeddable = getEmbeddable(ExplicitEmbeddable.class);
ManagedType<?> inner = getEmbeddable(Inner.class);
doReturn(new HashSet<>(Arrays.asList(entity, embeddable, inner))).when(metamodel).getManagedTypes();
doReturn(new HashSet<>(Arrays.asList(embeddable, inner))).when(metamodel).getEmbeddables();
assertThat(model.isMappedType(Wrapper.class)).isTrue();
assertThat(model.isMappedType(ExplicitEmbeddable.class)).isTrue();
assertThat(model.isMappedType(Inner.class)).isFalse();
}
private EmbeddableType<?> getEmbeddable(Class<?> type) {
EmbeddableType<?> managedType = getManagedType(type, EmbeddableType.class);
doReturn(PersistenceType.EMBEDDABLE).when(managedType).getPersistenceType();
return managedType;
}
private EntityType<?> getEntity(Class<?> type) {
EntityType<?> managedType = getManagedType(type, EntityType.class);
doReturn(PersistenceType.ENTITY).when(managedType).getPersistenceType();
return managedType;
}
private <T extends ManagedType<?>> T getManagedType(Class<?> type, Class<T> baseType) {
T managedType = mock(baseType);
doReturn(type).when(managedType).getJavaType();
doReturn(managedType).when(metamodel).managedType(type);
return managedType;
}
@Entity
static class Wrapper {}
@Embeddable
static class ExplicitEmbeddable {}
static class Inner {}
}