Skip to content

Commit 0c72cf6

Browse files
committed
Prevent Hibernate basics types to be treated as DTO projection
fixes #3929
1 parent 3725111 commit 0c72cf6

File tree

2 files changed

+131
-112
lines changed

2 files changed

+131
-112
lines changed

spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/AbstractStringBasedJpaQuery.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,7 @@ ReturnedType getReturnedType(ResultProcessor processor) {
156156
}
157157

158158
if ((known != null && !known) || returnedJavaType.isArray() || getMetamodel().isJpaManaged(returnedJavaType)
159-
|| !returnedType.needsCustomConstruction()) {
159+
|| getMetamodel().isBasicType(returnedJavaType) || !returnedType.needsCustomConstruction()) {
160160
if (known == null) {
161161
knownProjections.put(returnedJavaType, false);
162162
}

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

Lines changed: 130 additions & 111 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,11 @@
1616
package org.springframework.data.jpa.util;
1717

1818
import java.util.Collection;
19+
import java.util.HashSet;
1920
import java.util.Map;
2021
import java.util.Objects;
2122
import java.util.Optional;
23+
import java.util.Set;
2224
import java.util.concurrent.ConcurrentHashMap;
2325

2426
import jakarta.persistence.Embeddable;
@@ -43,115 +45,132 @@
4345
*/
4446
public class JpaMetamodel {
4547

46-
private static final Map<Metamodel, JpaMetamodel> CACHE = new ConcurrentHashMap<>(4);
47-
48-
private final Metamodel metamodel;
49-
50-
private final Lazy<Collection<Class<?>>> managedTypes;
51-
private final Lazy<Collection<Class<?>>> jpaEmbeddables;
52-
53-
/**
54-
* Creates a new {@link JpaMetamodel} for the given JPA {@link Metamodel}.
55-
*
56-
* @param metamodel must not be {@literal null}.
57-
*/
58-
private JpaMetamodel(Metamodel metamodel) {
59-
60-
Assert.notNull(metamodel, "Metamodel must not be null");
61-
62-
this.metamodel = metamodel;
63-
64-
this.managedTypes = Lazy.of(() -> metamodel.getManagedTypes().stream() //
65-
.map(ManagedType::getJavaType) //
66-
.filter(Objects::nonNull) //
67-
.collect(StreamUtils.toUnmodifiableSet()));
68-
69-
this.jpaEmbeddables = Lazy.of(() -> metamodel.getEmbeddables().stream() //
70-
.map(ManagedType::getJavaType)
71-
.filter(Objects::nonNull)
72-
.filter(it -> AnnotatedElementUtils.isAnnotated(it, Embeddable.class))
73-
.collect(StreamUtils.toUnmodifiableSet()));
74-
}
75-
76-
public static JpaMetamodel of(Metamodel metamodel) {
77-
return CACHE.computeIfAbsent(metamodel, JpaMetamodel::new);
78-
}
79-
80-
/**
81-
* Returns whether the given type is managed by the backing JPA {@link Metamodel}.
82-
*
83-
* @param type must not be {@literal null}.
84-
* @return
85-
*/
86-
public boolean isJpaManaged(Class<?> type) {
87-
88-
Assert.notNull(type, "Type must not be null");
89-
90-
return managedTypes.get().contains(type);
91-
}
92-
93-
/**
94-
* Returns whether the attribute of given name and type is the single identifier attribute of the given entity.
95-
*
96-
* @param entity must not be {@literal null}.
97-
* @param name must not be {@literal null}.
98-
* @param attributeType must not be {@literal null}.
99-
* @return
100-
*/
101-
public boolean isSingleIdAttribute(Class<?> entity, String name, Class<?> attributeType) {
102-
103-
return metamodel.getEntities().stream() //
104-
.filter(it -> entity.equals(it.getJavaType())) //
105-
.findFirst() //
106-
.flatMap(JpaMetamodel::getSingularIdAttribute) //
107-
.filter(it -> it.getJavaType().equals(attributeType)) //
108-
.map(it -> it.getName().equals(name)) //
109-
.orElse(false);
110-
}
111-
112-
/**
113-
* Returns whether the given type is considered a mapped type, i.e. an actually JPA persisted entity, mapped
114-
* superclass or native JPA embeddable.
115-
*
116-
* @param entity must not be {@literal null}.
117-
* @return
118-
*/
119-
public boolean isMappedType(Class<?> entity) {
120-
121-
Assert.notNull(entity, "Type must not be null");
122-
123-
if (!isJpaManaged(entity)) {
124-
return false;
125-
}
126-
127-
ManagedType<?> managedType = metamodel.managedType(entity);
128-
129-
return !managedType.getPersistenceType().equals(PersistenceType.EMBEDDABLE)
130-
|| jpaEmbeddables.get().contains(entity);
131-
}
132-
133-
/**
134-
* Wipes the static cache of {@link Metamodel} to {@link JpaMetamodel}.
135-
*/
136-
static void clear() {
137-
CACHE.clear();
138-
}
139-
140-
/**
141-
* Returns the {@link SingularAttribute} representing the identifier of the given {@link EntityType} if it contains a
142-
* singular one.
143-
*
144-
* @param entityType must not be {@literal null}.
145-
* @return
146-
*/
147-
private static Optional<? extends SingularAttribute<?, ?>> getSingularIdAttribute(EntityType<?> entityType) {
148-
149-
if (!entityType.hasSingleIdAttribute()) {
150-
return Optional.empty();
151-
}
152-
153-
return entityType.getSingularAttributes().stream() //
154-
.filter(SingularAttribute::isId) //
155-
.findFirst();
156-
}
48+
private static final Map<Metamodel, JpaMetamodel> CACHE = new ConcurrentHashMap<>(4);
49+
50+
private final Metamodel metamodel;
51+
52+
private final Lazy<Collection<Class<?>>> managedTypes;
53+
private final Lazy<Collection<Class<?>>> jpaEmbeddables;
54+
private final Lazy<Collection<Class<?>>> basicTypes;
55+
56+
/**
57+
* Creates a new {@link JpaMetamodel} for the given JPA {@link Metamodel}.
58+
*
59+
* @param metamodel must not be {@literal null}.
60+
*/
61+
private JpaMetamodel(Metamodel metamodel) {
62+
63+
Assert.notNull(metamodel, "Metamodel must not be null");
64+
65+
this.metamodel = metamodel;
66+
67+
this.managedTypes = Lazy.of(() -> metamodel.getManagedTypes().stream() //
68+
.map(ManagedType::getJavaType) //
69+
.filter(Objects::nonNull) //
70+
.collect(StreamUtils.toUnmodifiableSet()));
71+
72+
this.jpaEmbeddables = Lazy.of(() -> metamodel.getEmbeddables().stream() //
73+
.map(ManagedType::getJavaType)
74+
.filter(Objects::nonNull)
75+
.filter(it -> AnnotatedElementUtils.isAnnotated(it, Embeddable.class))
76+
.collect(StreamUtils.toUnmodifiableSet()));
77+
78+
this.basicTypes = Lazy.of(() -> {
79+
if (metamodel instanceof org.hibernate.metamodel.model.domain.JpaMetamodel hibernateMetamodel) {
80+
Set<Class<?>> result = new HashSet<>();
81+
hibernateMetamodel.getTypeConfiguration()
82+
.getJavaTypeRegistry()
83+
.forEachDescriptor(d -> result.add(d.getJavaTypeClass()));
84+
return result;
85+
}
86+
return Set.of();
87+
});
88+
}
89+
90+
public static JpaMetamodel of(Metamodel metamodel) {
91+
return CACHE.computeIfAbsent(metamodel, JpaMetamodel::new);
92+
}
93+
94+
/**
95+
* Returns whether the given type is managed by the backing JPA {@link Metamodel}.
96+
*
97+
* @param type must not be {@literal null}.
98+
* @return
99+
*/
100+
public boolean isJpaManaged(Class<?> type) {
101+
102+
Assert.notNull(type, "Type must not be null");
103+
104+
return managedTypes.get().contains(type);
105+
}
106+
107+
/**
108+
* Returns whether the attribute of given name and type is the single identifier attribute of the given entity.
109+
*
110+
* @param entity must not be {@literal null}.
111+
* @param name must not be {@literal null}.
112+
* @param attributeType must not be {@literal null}.
113+
* @return
114+
*/
115+
public boolean isSingleIdAttribute(Class<?> entity, String name, Class<?> attributeType) {
116+
117+
return metamodel.getEntities().stream() //
118+
.filter(it -> entity.equals(it.getJavaType())) //
119+
.findFirst() //
120+
.flatMap(JpaMetamodel::getSingularIdAttribute) //
121+
.filter(it -> it.getJavaType().equals(attributeType)) //
122+
.map(it -> it.getName().equals(name)) //
123+
.orElse(false);
124+
}
125+
126+
/**
127+
* Returns whether the given type is considered a mapped type, i.e. an actually JPA persisted entity, mapped
128+
* superclass or native JPA embeddable.
129+
*
130+
* @param entity must not be {@literal null}.
131+
* @return
132+
*/
133+
public boolean isMappedType(Class<?> entity) {
134+
135+
Assert.notNull(entity, "Type must not be null");
136+
137+
if (!isJpaManaged(entity)) {
138+
return false;
139+
}
140+
141+
ManagedType<?> managedType = metamodel.managedType(entity);
142+
143+
return !managedType.getPersistenceType().equals(PersistenceType.EMBEDDABLE)
144+
|| jpaEmbeddables.get().contains(entity);
145+
}
146+
147+
/**
148+
* Wipes the static cache of {@link Metamodel} to {@link JpaMetamodel}.
149+
*/
150+
static void clear() {
151+
CACHE.clear();
152+
}
153+
154+
/**
155+
* Returns the {@link SingularAttribute} representing the identifier of the given {@link EntityType} if it contains a
156+
* singular one.
157+
*
158+
* @param entityType must not be {@literal null}.
159+
* @return
160+
*/
161+
private static Optional<? extends SingularAttribute<?, ?>> getSingularIdAttribute(EntityType<?> entityType) {
162+
163+
if (!entityType.hasSingleIdAttribute()) {
164+
return Optional.empty();
165+
}
166+
167+
return entityType.getSingularAttributes().stream() //
168+
.filter(SingularAttribute::isId) //
169+
.findFirst();
170+
}
171+
172+
public boolean isBasicType(Class<?> type) {
173+
Assert.notNull(type, "Type must not be null");
174+
return basicTypes.get().contains(type);
175+
}
157176
}

0 commit comments

Comments
 (0)