DATAJPA-456 - Add support for specifying only the projection part of a custom count query.

We now support to specify just the projection part for the count query generation via the "countProjection" property of the @Query annotation.
This avoids having to repetitively specify parts of the original query.

Original pull request: #84.
This commit is contained in:
Thomas Darimont
2014-04-29 01:23:01 +02:00
committed by Oliver Gierke
parent d4acfeb0fa
commit 122d9c5ea2
8 changed files with 106 additions and 13 deletions

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2008-2012 the original author or authors.
* Copyright 2008-2014 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -27,6 +27,7 @@ import org.springframework.data.annotation.QueryAnnotation;
* Annotation to declare finder queries directly on repository methods.
*
* @author Oliver Gierke
* @author Thomas Darimont
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@@ -45,6 +46,15 @@ public @interface Query {
*/
String countQuery() default "";
/**
* Defines the projection part of the count query that is generated for pagination. If neither {@link #countQuery()}
* not {@link #countProjection()} is configured we will derive the count query from the method name.
*
* @return
* @since 1.6
*/
String countProjection() default "";
/**
* Configures whether the given query is a native one. Defaults to {@literal false}.
*/

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2008-2013 the original author or authors.
* Copyright 2008-2014 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -50,7 +50,7 @@ abstract class AbstractStringBasedJpaQuery extends AbstractJpaQuery {
this.query = new ExpressionBasedStringQuery(queryString, method.getEntityInformation());
this.countQuery = new StringQuery(method.getCountQuery() != null ? method.getCountQuery()
: QueryUtils.createCountQueryFor(this.query.getQueryString()));
: QueryUtils.createCountQueryFor(this.query.getQueryString(), method.getCountQueryProjection()));
}
/*

View File

@@ -232,6 +232,19 @@ public class JpaQueryMethod extends QueryMethod {
return StringUtils.hasText(countQuery) ? countQuery : null;
}
/**
* Returns the count query projection string declared in a {@link Query} annotation or {@literal null} if neither the
* annotation found nor the attribute was specified.
*
* @return
* @since 1.6
*/
String getCountQueryProjection() {
String countProjection = getAnnotationValue("countProjection", String.class);
return StringUtils.hasText(countProjection) ? countProjection : null;
}
/**
* Returns whether the backing query is a native one.
*

View File

@@ -41,6 +41,7 @@ final class NamedQuery extends AbstractJpaQuery {
private final String queryName;
private final String countQueryName;
private final String countProjection;
private final QueryExtractor extractor;
/**
@@ -53,6 +54,7 @@ final class NamedQuery extends AbstractJpaQuery {
this.queryName = method.getNamedQueryName();
this.countQueryName = method.getNamedCountQueryName();
this.extractor = method.getQueryExtractor();
this.countProjection = method.getCountQueryProjection();
Parameters<?, ?> parameters = method.getParameters();
@@ -142,7 +144,7 @@ final class NamedQuery extends AbstractJpaQuery {
} else {
Query query = createQuery(values);
String queryString = extractor.extractQueryString(query);
countQuery = em.createQuery(QueryUtils.createCountQueryFor(queryString), Long.class);
countQuery = em.createQuery(QueryUtils.createCountQueryFor(queryString, countProjection), Long.class);
}
return createBinder(values).bind(countQuery);

View File

@@ -332,22 +332,41 @@ public abstract class QueryUtils {
}
/**
* Creates a count projected query from the given orginal query.
* Creates a count projected query from the given original query.
*
* @param originalQuery must not be {@literal null} or empty
* @param originalQuery must not be {@literal null} or empty.
* @return
*/
public static String createCountQueryFor(String originalQuery) {
return createCountQueryFor(originalQuery, null);
}
Assert.hasText(originalQuery);
/**
* Creates a count projected query from the given original query.
*
* @param originalQuery must not be {@literal null}.
* @param countProjection may be {@literal null}.
* @return
* @since 1.6
*/
public static String createCountQueryFor(String originalQuery, String countProjection) {
Assert.hasText(originalQuery, "OriginalQuery must not be null or empty!");
Matcher matcher = COUNT_MATCH.matcher(originalQuery);
String variable = matcher.matches() ? matcher.group(4) : null;
boolean useVariable = StringUtils.hasText(variable) && !variable.startsWith("new")
&& !variable.startsWith("count(") && !variable.contains(",");
String countQuery = null;
String countQuery = matcher.replaceFirst(String.format(COUNT_REPLACEMENT_TEMPLATE, useVariable ? SIMPLE_COUNT_VALUE
: COMPLEX_COUNT_VALUE));
if (countProjection == null) {
String variable = matcher.matches() ? matcher.group(4) : null;
boolean useVariable = variable != null && StringUtils.hasText(variable) && !variable.startsWith("new")
&& !variable.startsWith("count(") && !variable.contains(",");
String replacement = useVariable ? SIMPLE_COUNT_VALUE : COMPLEX_COUNT_VALUE;
countQuery = matcher.replaceFirst(String.format(COUNT_REPLACEMENT_TEMPLATE, replacement));
} else {
countQuery = matcher.replaceFirst(String.format(COUNT_REPLACEMENT_TEMPLATE, countProjection));
}
return countQuery.replaceFirst(ORDER_BY_PART, "");
}

View File

@@ -1393,6 +1393,34 @@ public class UserRepositoryTests {
assertThat(result, is(data));
}
/**
* @see DATAJPA-456
*/
@Test
public void findPaginatedExplicitQueryWithCountQueryProjection() {
firstUser.setFirstname(null);
flushTestUsers();
Page<User> result = repository.findAllByFirstnameLike("", new PageRequest(0, 10));
assertThat(result.getContent().size(), is(3));
}
/**
* @see DATAJPA-456
*/
@Test
public void findPaginatedNamedQueryWithCountQueryProjection() {
flushTestUsers();
Page<User> result = repository.findByNamedQueryAndCountProjection("Gierke", new PageRequest(0, 10));
assertThat(result.getContent().size(), is(1));
}
private Page<User> executeSpecWithSort(Sort sort) {
flushTestUsers();

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2008-2013 the original author or authors.
* Copyright 2008-2014 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -272,6 +272,15 @@ public class QueryUtilsUnitTests {
assertCountQuery("select p.lastname,p.firstname from Person p", "select count(p) from Person p");
}
/**
* @see DATAJPA-456
*/
@Test
public void createCountQueryFromTheGivenCountProjection() {
assertThat(createCountQueryFor("select p.lastname,p.firstname from Person p", "p.lastname"),
is("select count(p.lastname) from Person p"));
}
private void assertCountQuery(String originalQuery, String countQuery) {
assertThat(createCountQueryFor(originalQuery), is(countQuery));
}

View File

@@ -368,4 +368,16 @@ public interface UserRepository extends JpaRepository<User, Integer>, JpaSpecifi
*/
@Procedure
Integer plus1(@Param("arg") Integer arg);
/**
* @see DATAJPA-456
*/
@Query(value = "select u from User u where u.firstname like ?1%", countProjection = "u.firstname")
Page<User> findAllByFirstnameLike(String firstname, Pageable page);
/**
* @see DATAJPA-456
*/
@Query(name = "User.findBySpringDataNamedQuery", countProjection = "u.firstname")
Page<User> findByNamedQueryAndCountProjection(String firstname, Pageable page);
}