Skip to content

Commit 8ea70c1

Browse files
committed
Remove duplicate 'distinct' applied in count operations.
When performing a count operation, we are using countDistinct from JPA, and hence, don't need the JpaQueryCreator applying distinct outside the whole thing. Also added some details in the ref docs to help guide users on writing proper distinct-based queries. See #1380.
1 parent c0cadfa commit 8ea70c1

File tree

5 files changed

+57
-14
lines changed

5 files changed

+57
-14
lines changed

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

+6-2
Original file line numberDiff line numberDiff line change
@@ -32,9 +32,12 @@
3232
* @author Oliver Gierke
3333
* @author Marc Lefrançois
3434
* @author Mark Paluch
35+
* @author Greg Turnquist
3536
*/
3637
public class JpaCountQueryCreator extends JpaQueryCreator {
3738

39+
private boolean distinct;
40+
3841
/**
3942
* Creates a new {@link JpaCountQueryCreator}.
4043
*
@@ -46,6 +49,7 @@ public class JpaCountQueryCreator extends JpaQueryCreator {
4649
public JpaCountQueryCreator(PartTree tree, ReturnedType type, CriteriaBuilder builder,
4750
ParameterMetadataProvider provider) {
4851
super(tree, type, builder, provider);
52+
this.distinct = tree.isDistinct();
4953
}
5054

5155
@Override
@@ -63,7 +67,7 @@ protected CriteriaQuery<? extends Object> complete(@Nullable Predicate predicate
6367
}
6468

6569
@SuppressWarnings("rawtypes")
66-
private static Expression getCountQuery(CriteriaQuery<?> query, CriteriaBuilder builder, Root<?> root) {
67-
return query.isDistinct() ? builder.countDistinct(root) : builder.count(root);
70+
private Expression getCountQuery(CriteriaQuery<?> query, CriteriaBuilder builder, Root<?> root) {
71+
return distinct ? builder.countDistinct(root) : builder.count(root);
6872
}
6973
}

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

+7-7
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,6 @@
1818
import static org.springframework.data.jpa.repository.query.QueryUtils.*;
1919
import static org.springframework.data.repository.query.parser.Part.Type.*;
2020

21-
import java.util.ArrayList;
22-
import java.util.Collection;
23-
import java.util.Iterator;
24-
import java.util.List;
25-
import java.util.stream.Collectors;
26-
2721
import jakarta.persistence.criteria.CriteriaBuilder;
2822
import jakarta.persistence.criteria.CriteriaQuery;
2923
import jakarta.persistence.criteria.Expression;
@@ -34,6 +28,12 @@
3428
import jakarta.persistence.criteria.Selection;
3529
import jakarta.persistence.metamodel.SingularAttribute;
3630

31+
import java.util.ArrayList;
32+
import java.util.Collection;
33+
import java.util.Iterator;
34+
import java.util.List;
35+
import java.util.stream.Collectors;
36+
3737
import org.springframework.data.domain.Sort;
3838
import org.springframework.data.jpa.repository.query.ParameterMetadataProvider.ParameterMetadata;
3939
import org.springframework.data.mapping.PropertyPath;
@@ -84,7 +84,7 @@ public JpaQueryCreator(PartTree tree, ReturnedType type, CriteriaBuilder builder
8484
CriteriaQuery<?> criteriaQuery = createCriteriaQuery(builder, type);
8585

8686
this.builder = builder;
87-
this.query = criteriaQuery.distinct(tree.isDistinct());
87+
this.query = criteriaQuery.distinct(tree.isDistinct() && !tree.isCountProjection());
8888
this.root = query.from(type.getDomainType());
8989
this.provider = provider;
9090
this.returnedType = type;

spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpaQueryRewriteIntegrationTests.java

+18-1
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
import org.springframework.data.domain.Sort;
3636
import org.springframework.data.jpa.domain.sample.User;
3737
import org.springframework.data.jpa.repository.JpaRepository;
38+
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
3839
import org.springframework.data.jpa.repository.Query;
3940
import org.springframework.data.jpa.repository.QueryRewriter;
4041
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
@@ -145,7 +146,21 @@ void nativeQueryShouldHandleRewritesUsingRepositoryRewriter() {
145146
entry(SORT, Sort.unsorted().toString()));
146147
}
147148

148-
public interface UserRepositoryWithRewriter extends JpaRepository<User, Integer>, QueryRewriter {
149+
@Test // GH-1380
150+
void counting() {
151+
152+
repository.saveAllAndFlush(List.of( //
153+
new User("Frodo", "Baggins", "[email protected]"), //
154+
new User("Bilbo", "Baggins", "[email protected]"), //
155+
new User("Samwise", "Gamgee", "[email protected]")));
156+
157+
assertThat(repository.count()).isEqualTo(3);
158+
assertThat(repository.countDistinctByLastname("Baggins")).isEqualTo(2);
159+
assertThat(repository.countDistinctByLastname("Gamgee")).isEqualTo(1);
160+
}
161+
162+
public interface UserRepositoryWithRewriter
163+
extends JpaRepository<User, Integer>, QueryRewriter, JpaSpecificationExecutor<User> {
149164

150165
@Query(value = "select original_user_alias.* from SD_USER original_user_alias", nativeQuery = true,
151166
queryRewriter = TestQueryRewriter.class)
@@ -170,6 +185,8 @@ public interface UserRepositoryWithRewriter extends JpaRepository<User, Integer>
170185
queryRewriter = UserRepositoryWithRewriter.class)
171186
List<User> findByNativeQueryUsingRepository(String param);
172187

188+
long countDistinctByLastname(String lastname);
189+
173190
@Override
174191
default String rewrite(String query, Sort sort) {
175192
return replaceAlias(query, sort);

spring-data-jpa/src/test/resources/logback.xml

+5-4
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,13 @@
1010
<logger name="org.springframework.data" level="error"/>
1111

1212
<!-- Uncomment these sections to debug -->
13-
<!-- <logger name="org.springframework.data.jpa" level="trace" />-->
14-
<!-- <logger name="org.springframework.jdbc" level="debug" />-->
15-
<!-- <logger name="org.testcontainers" level="debug" />-->
13+
<!-- <logger name="org.springframework.data.jpa" level="trace" />-->
14+
<!-- <logger name="org.springframework.jdbc" level="debug" />-->
15+
<!-- <logger name="org.hibernate.SQL" level="debug" />-->
16+
<!-- <logger name="org.testcontainers" level="debug" />-->
1617

1718
<root level="error">
1819
<appender-ref ref="console"/>
1920
</root>
2021

21-
</configuration>
22+
</configuration>

src/main/asciidoc/jpa.adoc

+21
Original file line numberDiff line numberDiff line change
@@ -253,6 +253,27 @@ The following table describes the keywords supported for JPA and what a method c
253253

254254
NOTE: `In` and `NotIn` also take any subclass of `Collection` as a parameter as well as arrays or varargs. For other syntactical versions of the same logical operator, check "`<<repository-query-keywords>>`".
255255

256+
[WARNING]
257+
====
258+
`DISTINCT` can be tricky and not always producing the results you expect.
259+
For example, `select distinct u from User u` will produce a complete different result than `select distinct u.lastname from User u`.
260+
In the first case, since you are including `User.id`, nothing will duplicated, hence you'll get the whole table, and it would be of `User` objects.
261+
262+
However, that latter query would narrow the focus to just `User.lastname` and find all unique last names for that table.
263+
This would also yield a `List<String>` result set instead of a `List<User`> result set.
264+
265+
266+
`countDistinctByLastname(String lastname)` can also produce unexpected results.
267+
Spring Data JPA will derive `select count(distinct u.id) from User u where u.lastname = ?1`.
268+
Again, since `u.id` won't hit any duplicates, this query will count up all the users that had the binding last name.
269+
Which would the same as `countByLastname(String lastname)`!
270+
271+
What is the point of this query anyway? To find the number of people with a given last name? To find the number of _distinct_ people with that binding last name?
272+
To find the number of _distinct last names_? (That last one is an entirely different query!)
273+
Using `distinct` sometimes requires writing the query by hand and using `@Query` to best capture the information you seek, since you also may be needing a projection
274+
to capture the result set.
275+
====
276+
256277
[[jpa.query-methods.named-queries]]
257278
=== Using JPA Named Queries
258279

0 commit comments

Comments
 (0)