From 1697c77f31813d8bc4d8bda2c61ed9c9a5972b44 Mon Sep 17 00:00:00 2001 From: Thomas Darimont Date: Thu, 5 Jun 2014 12:18:46 +0200 Subject: [PATCH] DATACMNS-516 - Add support for limiting the query result in the query derivation mechanism. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit We can now detect whether a given query method wants to limit the query results based on two new supported prefixes (findFirstBy…(…) and findFirstKBy…(…)) where "First" part can also be replaced by "Top" and an following optional K (which must be a decimal int) that denotes the maximal number of result rows to be retuned by the query. This can be used by the stores to allow queries like: - T findFirstBy…OrderBy…Asc -> smallest - T findFirstBy…OrderBy…Desc -> greatest - T findTopBy…OrderBy…Desc -> greatest - T findFirstBy…(Sort sort) -> general purpose - T findTopBy…(Sort sort) -> general purpose - List findFirstKBy…OrderBy…Asc -> smallest K - List findFirstKBy…OrderBy…Desc -> biggest K - List findFirstKBy…(Sort sort) -> general purpose TOP K - List findTopKBy…(Sort sort) -> general purpose TOP K The limiting expressions also support the Distinct expression. Also, for the queries limiting the result set to one instance, wrapping the result into an Optional is supported. If pagination or slicing is applied to a limiting query pagination (and the calculation of the number of pages available) is happening within the limited result. Original pull request: #85. --- .../repository/query/parser/PartTree.java | 57 ++++++++- .../query/parser/PartTreeUnitTests.java | 112 ++++++++++++++++++ 2 files changed, 166 insertions(+), 3 deletions(-) diff --git a/src/main/java/org/springframework/data/repository/query/parser/PartTree.java b/src/main/java/org/springframework/data/repository/query/parser/PartTree.java index 49b37895c..0dddbf757 100644 --- a/src/main/java/org/springframework/data/repository/query/parser/PartTree.java +++ b/src/main/java/org/springframework/data/repository/query/parser/PartTree.java @@ -50,9 +50,11 @@ public class PartTree implements Iterable { * @see http://docs.oracle.com/javase/7/docs/api/java/util/regex/Pattern.html#ubc */ private static final String KEYWORD_TEMPLATE = "(%s)(?=(\\p{Lu}|\\P{InBASIC_LATIN}))"; + private static final String QUERY_PATTERN = "find|read|get|query"; + private static final String COUNT_PATTERN = "count"; private static final String DELETE_PATTERN = "delete|remove"; - private static final Pattern PREFIX_TEMPLATE = Pattern.compile("^(find|read|get|count|query|" + DELETE_PATTERN - + ")(\\p{Lu}.*?)??By"); + private static final Pattern PREFIX_TEMPLATE = Pattern.compile( // + "^(" + QUERY_PATTERN + "|" + COUNT_PATTERN + "|" + DELETE_PATTERN + ")((\\p{Lu}.*?))??By"); /** * The subject, for example "findDistinctUserByNameOrderByAge" would have the subject "DistinctUser". @@ -133,6 +135,26 @@ public class PartTree implements Iterable { return subject.isDelete(); } + /** + * Return {@literal true} if the create {@link PartTree} is meant to be used for a query with limited maximal results. + * + * @return + * @since 1.9 + */ + public boolean isLimiting() { + return getMaxResults() != null; + } + + /** + * Return the number of maximal results to return or {@literal null} if not restricted. + * + * @return + * @since 1.9 + */ + public Integer getMaxResults() { + return subject.getMaxResults(); + } + /** * Returns an {@link Iterable} of all parts contained in the {@link PartTree}. * @@ -234,22 +256,48 @@ public class PartTree implements Iterable { * @author Phil Webb * @author Oliver Gierke * @author Christoph Strobl + * @author Thomas Darimont */ private static class Subject { private static final String DISTINCT = "Distinct"; private static final Pattern COUNT_BY_TEMPLATE = Pattern.compile("^count(\\p{Lu}.*?)??By"); private static final Pattern DELETE_BY_TEMPLATE = Pattern.compile("^(" + DELETE_PATTERN + ")(\\p{Lu}.*?)??By"); + private static final String LIMITING_QUERY_PATTERN = "(First|Top)(\\d*)?"; + private static final Pattern LIMITED_QUERY_TEMPLATE = Pattern.compile("^(" + QUERY_PATTERN + ")(" + DISTINCT + ")?" + + LIMITING_QUERY_PATTERN + "(\\p{Lu}.*?)??By"); private final boolean distinct; private final boolean count; private final boolean delete; + private final Integer maxResults; public Subject(String subject) { this.distinct = subject == null ? false : subject.contains(DISTINCT); this.count = matches(subject, COUNT_BY_TEMPLATE); this.delete = matches(subject, DELETE_BY_TEMPLATE); + this.maxResults = returnMaxResultsIfFirstKSubjectOrNull(subject); + } + + /** + * @param subject + * @return + * @since 1.9 + */ + private Integer returnMaxResultsIfFirstKSubjectOrNull(String subject) { + + if (subject == null) { + return null; + } + + Matcher grp = LIMITED_QUERY_TEMPLATE.matcher(subject); + + if (!grp.find()) { + return null; + } + + return StringUtils.hasText(grp.group(4)) ? Integer.valueOf(grp.group(4)) : 1; } /** @@ -270,6 +318,10 @@ public class PartTree implements Iterable { return distinct; } + public Integer getMaxResults() { + return maxResults; + } + private final boolean matches(String subject, Pattern pattern) { return subject == null ? false : pattern.matcher(subject).find(); } @@ -330,5 +382,4 @@ public class PartTree implements Iterable { return orderBySource; } } - } diff --git a/src/test/java/org/springframework/data/repository/query/parser/PartTreeUnitTests.java b/src/test/java/org/springframework/data/repository/query/parser/PartTreeUnitTests.java index aed7498f1..40579992f 100644 --- a/src/test/java/org/springframework/data/repository/query/parser/PartTreeUnitTests.java +++ b/src/test/java/org/springframework/data/repository/query/parser/PartTreeUnitTests.java @@ -553,6 +553,118 @@ public class PartTreeUnitTests { assertThat(tree.isDelete(), is(true)); } + /** + * @see DATACMNS-516 + */ + @Test + public void disablesFindFirstKImplicitIfNotPresent() { + assertLimiting("findByLastname", User.class, false, null); + } + + /** + * @see DATACMNS-516 + */ + @Test + public void identifiesFindFirstImplicit() { + assertLimiting("findFirstByLastname", User.class, true, 1); + } + + /** + * @see DATACMNS-516 + */ + @Test + public void identifiesFindFirst1Explicit() { + assertLimiting("findFirstByLastname", User.class, true, 1); + } + + /** + * @see DATACMNS-516 + */ + @Test + public void identifiesFindFirstKExplicit() { + assertLimiting("findFirst10ByLastname", User.class, true, 10); + } + + /** + * @see DATACMNS-516 + */ + @Test + public void identifiesFindFirstKUsersExplicit() { + assertLimiting("findFirst10UsersByLastname", User.class, true, 10); + } + + /** + * @see DATACMNS-516 + */ + @Test + public void identifiesFindFirstKDistinctUsersExplicit() { + assertLimiting("findFirst10DistinctUsersByLastname", User.class, true, 10, true); + assertLimiting("findDistinctFirst10UsersByLastname", User.class, true, 10, true); + assertLimiting("findFirst10UsersDistinctByLastname", User.class, true, 10, true); + } + + /** + * @see DATACMNS-516 + */ + @Test + public void identifiesFindTopImplicit() { + assertLimiting("findTopByLastname", User.class, true, 1); + } + + /** + * @see DATACMNS-516 + */ + @Test + public void identifiesFindTop1Explicit() { + assertLimiting("findTop1ByLastname", User.class, true, 1); + } + + /** + * @see DATACMNS-516 + */ + @Test + public void identifiesFindTopKExplicit() { + assertLimiting("findTop10ByLastname", User.class, true, 10); + } + + /** + * @see DATACMNS-516 + */ + @Test + public void identifiesFindTopKUsersExplicit() { + assertLimiting("findTop10UsersByLastname", User.class, true, 10); + } + + /** + * @see DATACMNS-516 + */ + @Test + public void identifiesFindTopKDistinctUsersExplicit() { + assertLimiting("findTop10DistinctUsersByLastname", User.class, true, 10, true); + assertLimiting("findDistinctTop10UsersByLastname", User.class, true, 10, true); + assertLimiting("findTop10UsersDistinctByLastname", User.class, true, 10, true); + } + + @Test + public void shouldNotSupportLimitingCountQueries() { + assertLimiting("countFirst10DistinctUsersByLastname", User.class, false, null, true); + assertLimiting("countTop10DistinctUsersByLastname", User.class, false, null, true); + } + + private static void assertLimiting(String methodName, Class entityType, boolean limiting, Integer maxResults) { + assertLimiting(methodName, entityType, limiting, maxResults, false); + } + + private static void assertLimiting(String methodName, Class entityType, boolean limiting, Integer maxResults, + boolean distinct) { + + PartTree tree = new PartTree(methodName, entityType); + + assertThat(tree.isLimiting(), is(limiting)); + assertThat(tree.getMaxResults(), is(maxResults)); + assertThat(tree.isDistinct(), is(distinct)); + } + private static void assertType(Iterable sources, Type type, String property) { assertType(sources, type, property, 1, true); }