Skip to content

Commit

Permalink
Guard against Hibernate exposing non-embeddables as embeddables on ve…
Browse files Browse the repository at this point in the history
…rsions > 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.
  • Loading branch information
odrotbohm committed Jan 26, 2022
1 parent 47f36f6 commit 3cd3f15
Show file tree
Hide file tree
Showing 3 changed files with 94 additions and 1 deletion.
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ public JpaPersistentPropertyImpl(JpaMetamodel metamodel, Property property,

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()));
}

/*
Expand Down
36 changes: 36 additions & 0 deletions src/main/java/org/springframework/data/jpa/util/JpaMetamodel.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -39,10 +44,13 @@
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}.
Expand All @@ -54,10 +62,17 @@ private JpaMetamodel(Metamodel metamodel) {
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) {
Expand Down Expand Up @@ -96,6 +111,27 @@ public boolean isSingleIdAttribute(Class<?> entity, String name, Class<?> attrib
.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}.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,17 @@
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;
Expand Down Expand Up @@ -60,4 +67,54 @@ void cacheIsEffectiveUnlessCleared() {
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 {}
}

0 comments on commit 3cd3f15

Please sign in to comment.