Skip to content

Commit f66174b

Browse files
committed
Check leniently for built-in exists query results.
We now check whether the exists-count is greater than zero instead of checking for 1. This enables uses with partial primary keys, although non-unique primary keys will generally fail when using by-id lookups. Closes #4227
1 parent b180746 commit f66174b

7 files changed

Lines changed: 133 additions & 6 deletions

File tree

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
/*
2+
* Copyright 2026-present 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.repository.support;
17+
18+
import org.jspecify.annotations.Nullable;
19+
20+
/**
21+
* Utility to determine whether a query result exists.
22+
*
23+
* @author Mark Paluch
24+
* @since 4.0.5
25+
*/
26+
class ExistsUtil {
27+
28+
/**
29+
* Determine whether the given count is greater than zero.
30+
*/
31+
public static boolean exists(long count) {
32+
return count > 0;
33+
}
34+
35+
/**
36+
* Determine whether the given count is greater than zero.
37+
*/
38+
public static boolean exists(@Nullable Long singleResult) {
39+
return singleResult != null && exists(singleResult.longValue());
40+
}
41+
42+
}

spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/SimpleJpaRepository.java

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -385,7 +385,7 @@ public boolean existsById(ID id) {
385385

386386
if (!entityInformation.hasCompositeId()) {
387387
query.setParameter(idAttributeNames.iterator().next(), id);
388-
return query.getSingleResult() == 1L;
388+
return ExistsUtil.exists(query.getSingleResult());
389389
}
390390

391391
for (String idAttributeName : idAttributeNames) {
@@ -404,7 +404,7 @@ public boolean existsById(ID id) {
404404
query.setParameter(idAttributeName, idAttributeValue);
405405
}
406406

407-
return query.getSingleResult() == 1L;
407+
return ExistsUtil.exists(query.getSingleResult());
408408
}
409409

410410
@Override
@@ -497,7 +497,7 @@ public boolean exists(Specification<T> spec) {
497497
applySpecificationToCriteria(spec, getDomainClass(), cq);
498498

499499
TypedQuery<Integer> query = applyRepositoryMethodMetadata(this.entityManager.createQuery(cq));
500-
return query.setMaxResults(1).getResultList().size() == 1;
500+
return ExistsUtil.exists(query.setMaxResults(1).getResultList().size());
501501
}
502502

503503
@Override
@@ -601,7 +601,7 @@ public <S extends T> boolean exists(Example<S> example) {
601601
applySpecificationToCriteria(spec, example.getProbeType(), cq);
602602

603603
TypedQuery<Integer> query = applyRepositoryMethodMetadata(this.entityManager.createQuery(cq));
604-
return query.setMaxResults(1).getResultList().size() == 1;
604+
return ExistsUtil.exists(query.setMaxResults(1).getResultList().size());
605605
}
606606

607607
@Override

spring-data-jpa/src/test/java/org/springframework/data/jpa/domain/sample/SampleEntity.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,13 @@
1717

1818
import jakarta.persistence.EmbeddedId;
1919
import jakarta.persistence.Entity;
20+
import jakarta.persistence.Table;
2021

2122
/**
2223
* @author Oliver Gierke
2324
*/
2425
@Entity
26+
@Table(name = "sample_entity")
2527
public class SampleEntity {
2628

2729
@EmbeddedId
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
/*
2+
* Copyright 2026-present 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 jakarta.persistence.Entity;
19+
import jakarta.persistence.Id;
20+
21+
import org.hibernate.annotations.Immutable;
22+
import org.hibernate.annotations.Subselect;
23+
24+
/**
25+
* @author Mark Paluch
26+
*/
27+
@Entity
28+
@Immutable
29+
@Subselect("""
30+
select first
31+
from sample_entity
32+
""")
33+
public class SampleEntityPartialKey {
34+
35+
@Id private String first;
36+
37+
public SampleEntityPartialKey() {}
38+
39+
public SampleEntityPartialKey(String first) {
40+
this.first = first;
41+
}
42+
43+
public String getFirst() {
44+
return first;
45+
}
46+
47+
public void setFirst(String first) {
48+
this.first = first;
49+
}
50+
}

spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/support/EclipseLinkJpaRepositoryTests.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,12 @@ void deleteAllInBatch() {
7777
assertThat(repository.count()).isZero();
7878
}
7979

80+
@Test
81+
@Disabled("We cannot currently setup a read-only entity to simulate a partial key on top of an entity with a composite key")
82+
void shouldReportExistsForPartialKeyEntity() {
83+
84+
}
85+
8086
@Override
8187
@Disabled("https://bugs.eclipse.org/bugs/show_bug.cgi?id=349477")
8288
void deleteAllByIdInBatch() {

spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/support/JpaRepositoryTests.java

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,10 +27,13 @@
2727
import org.junit.jupiter.api.BeforeEach;
2828
import org.junit.jupiter.api.Test;
2929
import org.junit.jupiter.api.extension.ExtendWith;
30+
31+
import org.springframework.data.domain.Example;
3032
import org.springframework.data.jpa.domain.sample.PersistableWithIdClass;
3133
import org.springframework.data.jpa.domain.sample.PersistableWithIdClassPK;
3234
import org.springframework.data.jpa.domain.sample.SampleEntity;
3335
import org.springframework.data.jpa.domain.sample.SampleEntityPK;
36+
import org.springframework.data.jpa.domain.sample.SampleEntityPartialKey;
3437
import org.springframework.data.jpa.repository.JpaRepository;
3538
import org.springframework.data.repository.CrudRepository;
3639
import org.springframework.test.context.ContextConfiguration;
@@ -45,6 +48,7 @@
4548
* @author Jens Schauder
4649
* @author Greg Turnquist
4750
* @author Krzysztof Krason
51+
* @author Mark Paluch
4852
*/
4953
@ExtendWith(SpringExtension.class)
5054
@ContextConfiguration("classpath:hibernate-infrastructure.xml")
@@ -55,11 +59,14 @@ class JpaRepositoryTests {
5559

5660
private JpaRepository<SampleEntity, SampleEntityPK> repository;
5761
private CrudRepository<PersistableWithIdClass, PersistableWithIdClassPK> idClassRepository;
62+
private SamplePartialKeyEntityRepository partialKey;
5863

5964
@BeforeEach
6065
void setUp() {
61-
repository = new JpaRepositoryFactory(em).getRepository(SampleEntityRepository.class);
62-
idClassRepository = new JpaRepositoryFactory(em).getRepository(SampleWithIdClassRepository.class);
66+
JpaRepositoryFactory factory = new JpaRepositoryFactory(em);
67+
repository = factory.getRepository(SampleEntityRepository.class);
68+
idClassRepository = factory.getRepository(SampleWithIdClassRepository.class);
69+
partialKey = factory.getRepository(SamplePartialKeyEntityRepository.class);
6370
}
6471

6572
@Test
@@ -78,6 +85,19 @@ void testCrudOperationsForCompoundKeyEntity() {
7885
assertThat(repository.count()).isZero();
7986
}
8087

88+
@Test
89+
void shouldReportExistsForPartialKeyEntity() {
90+
91+
repository.saveAndFlush(new SampleEntity("foo", "bar"));
92+
repository.saveAndFlush(new SampleEntity("foo", "baz"));
93+
repository.saveAndFlush(new SampleEntity("foo", "foo"));
94+
repository.flush();
95+
96+
assertThat(partialKey.existsById("foo")).isTrue();
97+
assertThat(partialKey.exists(Example.of(new SampleEntityPartialKey("foo")))).isTrue();
98+
assertThat(partialKey.existsByFirst("foo")).isTrue();
99+
}
100+
81101
@Test // DATAJPA-50
82102
void executesCrudOperationsForEntityWithIdClass() {
83103

@@ -164,6 +184,12 @@ private interface SampleEntityRepository extends JpaRepository<SampleEntity, Sam
164184

165185
}
166186

187+
private interface SamplePartialKeyEntityRepository extends JpaRepository<SampleEntityPartialKey, String> {
188+
189+
boolean existsByFirst(String first);
190+
191+
}
192+
167193
private interface SampleWithIdClassRepository
168194
extends CrudRepository<PersistableWithIdClass, PersistableWithIdClassPK> {
169195

spring-data-jpa/src/test/resources/META-INF/persistence.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@
5454
<class>org.springframework.data.jpa.domain.sample.SampleWithIdClass</class>
5555
<class>org.springframework.data.jpa.domain.sample.SampleWithPrimitiveId</class>
5656
<class>org.springframework.data.jpa.domain.sample.SampleWithTimestampVersion</class>
57+
<class>org.springframework.data.jpa.domain.sample.SampleEntityPartialKey</class>
5758
<class>org.springframework.data.jpa.domain.sample.Site</class>
5859
<class>org.springframework.data.jpa.domain.sample.SpecialUser</class>
5960
<class>org.springframework.data.jpa.domain.sample.User</class>

0 commit comments

Comments
 (0)