Skip to content

Commit 10613e2

Browse files
committed
Use @IdClass as identifier for IdClasses with a single attribute for repository operations.
We now use the IdClass type for lookups when the entity defines a `@IdClass`. Previously, we uses the type of the defined singular identifier attribute which lead to invalid queries. Closes #2330
1 parent 32162b0 commit 10613e2

File tree

6 files changed

+188
-9
lines changed

6 files changed

+188
-9
lines changed

src/main/java/org/springframework/data/jpa/provider/PersistenceProvider.java

+29
Original file line numberDiff line numberDiff line change
@@ -20,15 +20,19 @@
2020

2121
import java.util.Collections;
2222
import java.util.NoSuchElementException;
23+
import java.util.Set;
2324

2425
import javax.persistence.EntityManager;
2526
import javax.persistence.Query;
27+
import javax.persistence.metamodel.IdentifiableType;
2628
import javax.persistence.metamodel.Metamodel;
29+
import javax.persistence.metamodel.SingularAttribute;
2730

2831
import org.eclipse.persistence.jpa.JpaQuery;
2932
import org.eclipse.persistence.queries.ScrollableCursor;
3033
import org.hibernate.ScrollMode;
3134
import org.hibernate.ScrollableResults;
35+
import org.hibernate.metamodel.model.domain.spi.IdentifiableTypeDescriptor;
3236
import org.hibernate.proxy.HibernateProxy;
3337
import org.springframework.data.util.CloseableIterator;
3438
import org.springframework.lang.Nullable;
@@ -93,6 +97,17 @@ public Object getIdentifierFrom(Object entity) {
9397
return ((HibernateProxy) entity).getHibernateLazyInitializer().getIdentifier();
9498
}
9599

100+
/*
101+
* (non-Javadoc)
102+
* @see org.springframework.data.jpa.provider.PersistenceProvider#getIdClassAttributes(javax.persistence.metamodel.IdentifiableType)
103+
*/
104+
@Override
105+
public <T> Set<SingularAttribute<? super T, ?>> getIdClassAttributes(IdentifiableType<T> type) {
106+
return type instanceof IdentifiableTypeDescriptor && ((IdentifiableTypeDescriptor<T>) type).hasIdClass()
107+
? super.getIdClassAttributes(type)
108+
: Collections.emptySet();
109+
}
110+
96111
/*
97112
* (non-Javadoc)
98113
* @see org.springframework.data.jpa.provider.PersistenceProvider#executeQueryWithResultStream(javax.persistence.Query)
@@ -291,6 +306,20 @@ public boolean canExtractQuery() {
291306
return true;
292307
}
293308

309+
/**
310+
* @param type the entity type.
311+
* @return the set of identifier attributes used in a {@code @IdClass} for {@code type}. Empty when {@code type} does
312+
* not use {@code @IdClass}.
313+
* @since 2.4.14
314+
*/
315+
public <T> Set<SingularAttribute<? super T, ?>> getIdClassAttributes(IdentifiableType<T> type) {
316+
try {
317+
return type.getIdClassAttributes();
318+
} catch (IllegalArgumentException e) {
319+
return Collections.emptySet();
320+
}
321+
}
322+
294323
/**
295324
* Holds the PersistenceProvider specific interface names.
296325
*

src/main/java/org/springframework/data/jpa/repository/support/JpaMetamodelEntityInformation.java

+17-7
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ public JpaMetamodelEntityInformation(Class<T> domainClass, Metamodel metamodel)
8686

8787
IdentifiableType<T> identifiableType = (IdentifiableType<T>) type;
8888

89-
this.idMetadata = new IdMetadata<>(identifiableType);
89+
this.idMetadata = new IdMetadata<>(identifiableType, PersistenceProvider.fromMetamodel(metamodel));
9090
this.versionAttribute = findVersionAttribute(identifiableType, metamodel);
9191
}
9292

@@ -260,20 +260,22 @@ public boolean isNew(T entity) {
260260
private static class IdMetadata<T> implements Iterable<SingularAttribute<? super T, ?>> {
261261

262262
private final IdentifiableType<T> type;
263+
private final Set<SingularAttribute<? super T, ?>> idClassAttributes;
263264
private final Set<SingularAttribute<? super T, ?>> attributes;
264265
private @Nullable Class<?> idType;
265266

266267
@SuppressWarnings("unchecked")
267-
IdMetadata(IdentifiableType<T> source) {
268+
IdMetadata(IdentifiableType<T> source, PersistenceProvider persistenceProvider) {
268269

269270
this.type = source;
271+
this.idClassAttributes = persistenceProvider.getIdClassAttributes(source);
270272
this.attributes = (Set<SingularAttribute<? super T, ?>>) (source.hasSingleIdAttribute()
271273
? Collections.singleton(source.getId(source.getIdType().getJavaType()))
272274
: source.getIdClassAttributes());
273275
}
274276

275277
boolean hasSimpleId() {
276-
return attributes.size() == 1;
278+
return idClassAttributes.isEmpty() && attributes.size() == 1;
277279
}
278280

279281
public Class<?> getType() {
@@ -296,18 +298,26 @@ public Class<?> getType() {
296298
private Class<?> tryExtractIdTypeWithFallbackToIdTypeLookup() {
297299

298300
try {
301+
302+
Class<?> idClassType = lookupIdClass(type);
303+
if (idClassType != null) {
304+
return idClassType;
305+
}
306+
299307
Type<?> idType = type.getIdType();
300-
return idType == null ? fallbackIdTypeLookup(type) : idType.getJavaType();
308+
return idType == null ? null : idType.getJavaType();
301309
} catch (IllegalStateException e) {
302310
// see https://hibernate.onjira.com/browse/HHH-6951
303-
return fallbackIdTypeLookup(type);
311+
return null;
304312
}
305313
}
306314

307315
@Nullable
308-
private static Class<?> fallbackIdTypeLookup(IdentifiableType<?> type) {
316+
private static Class<?> lookupIdClass(IdentifiableType<?> type) {
309317

310-
IdClass annotation = AnnotationUtils.findAnnotation(type.getJavaType(), IdClass.class);
318+
IdClass annotation = type.getJavaType() != null
319+
? AnnotationUtils.findAnnotation(type.getJavaType(), IdClass.class)
320+
: null;
311321
return annotation == null ? null : annotation.value();
312322
}
313323

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
/*
2+
* Copyright 2021 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.data.jpa.domain.sample;
17+
18+
import javax.persistence.Entity;
19+
import javax.persistence.Id;
20+
import javax.persistence.IdClass;
21+
22+
/**
23+
* Sample entity using {@link IdClass} annotation to demarcate ids.
24+
*
25+
* @author Mark Paluch
26+
*/
27+
@Entity
28+
@IdClass(PersistableWithSingleIdClassPK.class)
29+
public class PersistableWithSingleIdClass {
30+
31+
private static final long serialVersionUID = 1L;
32+
33+
@Id private Long first;
34+
35+
protected PersistableWithSingleIdClass() {
36+
37+
}
38+
39+
public PersistableWithSingleIdClass(Long first) {
40+
this.first = first;
41+
}
42+
43+
/**
44+
* @return the first
45+
*/
46+
public Long getFirst() {
47+
return first;
48+
}
49+
50+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
/*
2+
* Copyright 2021 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.data.jpa.domain.sample;
17+
18+
import static org.springframework.util.ObjectUtils.*;
19+
20+
import java.io.Serializable;
21+
22+
/**
23+
* @author Mark Paluch
24+
*/
25+
public class PersistableWithSingleIdClassPK implements Serializable {
26+
27+
private static final long serialVersionUID = 23126782341L;
28+
29+
private Long first;
30+
31+
public PersistableWithSingleIdClassPK() {
32+
33+
}
34+
35+
public PersistableWithSingleIdClassPK(Long first) {
36+
this.first = first;
37+
}
38+
39+
public void setFirst(Long first) {
40+
this.first = first;
41+
}
42+
43+
/*
44+
* (non-Javadoc)
45+
* @see java.lang.Object#equals(java.lang.Object)
46+
*/
47+
@Override
48+
public boolean equals(Object obj) {
49+
50+
if (this == obj) {
51+
return true;
52+
}
53+
54+
if (obj == null || !(obj.getClass().equals(getClass()))) {
55+
return false;
56+
}
57+
58+
PersistableWithSingleIdClassPK that = (PersistableWithSingleIdClassPK) obj;
59+
60+
return nullSafeEquals(this.first, that.first);
61+
}
62+
63+
/*
64+
* (non-Javadoc)
65+
* @see java.lang.Object#hashCode()
66+
*/
67+
@Override
68+
public int hashCode() {
69+
70+
int result = 17;
71+
72+
result += nullSafeHashCode(this.first);
73+
74+
return result;
75+
}
76+
}

src/test/java/org/springframework/data/jpa/repository/support/JpaMetamodelEntityInformationIntegrationTests.java

+12
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,18 @@ void returnsIdOfPersistableInstanceCorrectly() {
9292
assertThat(id).isEqualTo(new PersistableWithIdClassPK(2L, 4L));
9393
}
9494

95+
@Test // GH-2330
96+
void returnsIdOfSingleAttributeIdClassCorrectly() {
97+
98+
PersistableWithSingleIdClass entity = new PersistableWithSingleIdClass(2L);
99+
100+
JpaEntityInformation<PersistableWithSingleIdClass, ?> information = getEntityInformation(
101+
PersistableWithSingleIdClass.class, em);
102+
Object id = information.getId(entity);
103+
104+
assertThat(id).isEqualTo(new PersistableWithSingleIdClassPK(2L));
105+
}
106+
95107
@Test // DATAJPA-413
96108
void returnsIdOfEntityWithIdClassCorrectly() {
97109

src/test/resources/META-INF/persistence.xml

+4-2
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@
3232
<class>org.springframework.data.jpa.domain.sample.Order</class>
3333
<class>org.springframework.data.jpa.domain.sample.Parent</class>
3434
<class>org.springframework.data.jpa.domain.sample.PersistableWithIdClass</class>
35+
<class>org.springframework.data.jpa.domain.sample.PersistableWithSingleIdClass
36+
</class>
3537
<class>org.springframework.data.jpa.domain.sample.PrimitiveVersionProperty</class>
3638
<class>org.springframework.data.jpa.domain.sample.Product</class>
3739
<class>org.springframework.data.jpa.domain.sample.Role</class>
@@ -89,9 +91,9 @@
8991
<property name="hibernate.dialect" value="org.hibernate.dialect.HSQLDialect" />
9092
</properties>
9193
</persistence-unit>
92-
94+
9395
<!-- Custom PUs for metadata tests -->
94-
96+
9597
<persistence-unit name="metadata">
9698
<provider>org.hibernate.jpa.HibernatePersistenceProvider</provider>
9799
<class>org.springframework.data.jpa.domain.sample.CustomAbstractPersistable</class>

0 commit comments

Comments
 (0)