From 5e608677507b39e0da3665e69ffa782f172ae07d Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Tue, 28 Jun 2016 14:31:26 +0200 Subject: [PATCH] DATAMONGO-1454 - Add support for exists projection in repository query methods. We now support exists projections for query methods in query methods for derived and string queries. public PersonRepository extends Repository { boolean existsByFirstname(String firstname); @ExistsQuery(value = "{ 'lastname' : ?0 }") boolean someExistQuery(String lastname); @Query(value = "{ 'lastname' : ?0 }", exists = true) boolean anotherExistQuery(String lastname); } Original pull request: #381. --- .../data/mongodb/repository/ExistsQuery.java | 47 ++++++++++++++ .../data/mongodb/repository/Query.java | 8 +++ .../repository/query/AbstractMongoQuery.java | 17 +++++- .../repository/query/MongoQueryExecution.java | 46 +++++++++++++- .../repository/query/PartTreeMongoQuery.java | 10 +++ .../query/StringBasedMongoQuery.java | 61 ++++++++++++++++--- ...tractPersonRepositoryIntegrationTests.java | 17 ++++++ .../mongodb/repository/PersonRepository.java | 12 ++++ .../query/AbstractMongoQueryUnitTests.java | 6 ++ .../query/StringBasedMongoQueryUnitTests.java | 28 +++++++-- 10 files changed, 234 insertions(+), 18 deletions(-) create mode 100644 spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/ExistsQuery.java diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/ExistsQuery.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/ExistsQuery.java new file mode 100644 index 000000000..de937374d --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/ExistsQuery.java @@ -0,0 +1,47 @@ +/* + * Copyright 2016 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.mongodb.repository; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.springframework.core.annotation.AliasFor; + +/** + * Annotation to declare finder exists queries directly on repository methods. Both attributes allow using a placeholder + * notation of {@code ?0}, {@code ?1} and so on. + * + * @author Mark Paluch + * @since 1.10 + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ ElementType.METHOD, ElementType.ANNOTATION_TYPE }) +@Documented +@Query(exists = true) +public @interface ExistsQuery { + + /** + * Takes a MongoDB JSON string to define the actual query to be executed. This one will take precedence over the + * method name then. Alias for {@link Query#value}. + * + * @return + */ + @AliasFor(annotation = Query.class) + String value() default ""; +} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/Query.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/Query.java index 97fb25ff1..a0ad7a905 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/Query.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/Query.java @@ -62,6 +62,14 @@ public @interface Query { */ boolean count() default false; + /** + * Returns whether the query defined should be executed as exists projection. + * + * @since 1.10 + * @return + */ + boolean exists() default false; + /** * Returns whether the query should delete matching documents. * diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/AbstractMongoQuery.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/AbstractMongoQuery.java index d6b65be25..d59efb045 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/AbstractMongoQuery.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/AbstractMongoQuery.java @@ -20,7 +20,9 @@ import org.springframework.data.convert.EntityInstantiators; import org.springframework.data.mongodb.core.MongoOperations; import org.springframework.data.mongodb.core.query.Query; import org.springframework.data.mongodb.repository.query.MongoQueryExecution.CollectionExecution; +import org.springframework.data.mongodb.repository.query.MongoQueryExecution.CountExecution; import org.springframework.data.mongodb.repository.query.MongoQueryExecution.DeleteExecution; +import org.springframework.data.mongodb.repository.query.MongoQueryExecution.ExistsExecution; import org.springframework.data.mongodb.repository.query.MongoQueryExecution.GeoNearExecution; import org.springframework.data.mongodb.repository.query.MongoQueryExecution.PagedExecution; import org.springframework.data.mongodb.repository.query.MongoQueryExecution.PagingGeoNearExecution; @@ -40,6 +42,7 @@ import org.springframework.util.Assert; * @author Oliver Gierke * @author Thomas Darimont * @author Christoph Strobl + * @author Mark Paluch */ public abstract class AbstractMongoQuery implements RepositoryQuery { @@ -123,8 +126,12 @@ public abstract class AbstractMongoQuery implements RepositoryQuery { return new CollectionExecution(operations, accessor.getPageable()); } else if (method.isPageQuery()) { return new PagedExecution(operations, accessor.getPageable()); + } else if (isCountQuery()) { + return new CountExecution(operations); + } else if (isExistsQuery()) { + return new ExistsExecution(operations); } else { - return new SingleEntityExecution(operations, isCountQuery()); + return new SingleEntityExecution(operations); } } @@ -164,6 +171,14 @@ public abstract class AbstractMongoQuery implements RepositoryQuery { */ protected abstract boolean isCountQuery(); + /** + * Returns whether the query should get an exists projection applied. + * + * @return + * @since 1.10 + */ + protected abstract boolean isExistsQuery(); + /** * Return weather the query should delete matching documents. * diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoQueryExecution.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoQueryExecution.java index e51375178..443ab1c71 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoQueryExecution.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoQueryExecution.java @@ -164,7 +164,6 @@ interface MongoQueryExecution { final class SingleEntityExecution implements MongoQueryExecution { private final MongoOperations operations; - private final boolean countProjection; /* * (non-Javadoc) @@ -172,7 +171,50 @@ interface MongoQueryExecution { */ @Override public Object execute(Query query, Class type, String collection) { - return countProjection ? operations.count(query, type, collection) : operations.findOne(query, type, collection); + return operations.findOne(query, type, collection); + } + } + + /** + * {@link MongoQueryExecution} to perform a count projection. + * + * @author Oliver Gierke + * @author Mark Paluch + * @since 1.10 + */ + @RequiredArgsConstructor + static final class CountExecution implements MongoQueryExecution { + + private final MongoOperations operations; + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.repository.query.AbstractMongoQuery.Execution#execute(org.springframework.data.mongodb.core.query.Query, java.lang.Class, java.lang.String) + */ + @Override + public Object execute(Query query, Class type, String collection) { + return operations.count(query, type, collection); + } + } + + /** + * {@link MongoQueryExecution} to perform an exists projection. + * + * @author Mark Paluch + * @since 1.10 + */ + @RequiredArgsConstructor + static final class ExistsExecution implements MongoQueryExecution { + + private final MongoOperations operations; + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.repository.query.AbstractMongoQuery.Execution#execute(org.springframework.data.mongodb.core.query.Query, java.lang.Class, java.lang.String) + */ + @Override + public Object execute(Query query, Class type, String collection) { + return operations.exists(query, type, collection); } } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/PartTreeMongoQuery.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/PartTreeMongoQuery.java index ec0ac278f..f036e6055 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/PartTreeMongoQuery.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/PartTreeMongoQuery.java @@ -42,6 +42,7 @@ import com.mongodb.util.JSONParseException; * @author Oliver Gierke * @author Christoph Strobl * @author Thomas Darimont + * @author Mark Paluch */ public class PartTreeMongoQuery extends AbstractMongoQuery { @@ -143,6 +144,15 @@ public class PartTreeMongoQuery extends AbstractMongoQuery { return tree.isCountProjection(); } + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.repository.query.AbstractMongoQuery#isExistsQuery() + */ + @Override + protected boolean isExistsQuery() { + return tree.isExistsProjection(); + } + /* * (non-Javadoc) * @see org.springframework.data.mongodb.repository.query.AbstractMongoQuery#isDeleteQuery() diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/StringBasedMongoQuery.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/StringBasedMongoQuery.java index 95b4234e9..bb148128c 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/StringBasedMongoQuery.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/StringBasedMongoQuery.java @@ -43,16 +43,18 @@ import com.mongodb.util.JSON; * @author Oliver Gierke * @author Christoph Strobl * @author Thomas Darimont + * @author Mark Paluch */ public class StringBasedMongoQuery extends AbstractMongoQuery { - private static final String COUND_AND_DELETE = "Manually defined query for %s cannot be both a count and delete query at the same time!"; + private static final String COUNT_EXISTS_AND_DELETE = "Manually defined query for %s cannot be a count and exists or delete query at the same time!"; private static final Logger LOG = LoggerFactory.getLogger(StringBasedMongoQuery.class); private static final ParameterBindingParser BINDING_PARSER = ParameterBindingParser.INSTANCE; private final String query; private final String fieldSpec; private final boolean isCountQuery; + private final boolean isExistsQuery; private final boolean isDeleteQuery; private final List queryParameterBindings; private final List fieldSpecParameterBindings; @@ -96,14 +98,26 @@ public class StringBasedMongoQuery extends AbstractMongoQuery { this.fieldSpec = BINDING_PARSER.parseAndCollectParameterBindingsFromQueryIntoBindings( method.getFieldSpecification(), this.fieldSpecParameterBindings); - this.isCountQuery = method.hasAnnotatedQuery() ? method.getQueryAnnotation().count() : false; - this.isDeleteQuery = method.hasAnnotatedQuery() ? method.getQueryAnnotation().delete() : false; - - if (isCountQuery && isDeleteQuery) { - throw new IllegalArgumentException(String.format(COUND_AND_DELETE, method)); - } - this.parameterBinder = new ExpressionEvaluatingParameterBinder(expressionParser, evaluationContextProvider); + + if (method.hasAnnotatedQuery()) { + + org.springframework.data.mongodb.repository.Query queryAnnotation = method.getQueryAnnotation(); + + this.isCountQuery = queryAnnotation.count(); + this.isExistsQuery = queryAnnotation.exists(); + this.isDeleteQuery = queryAnnotation.delete(); + + if (hasAmbiguousProjectionFlags(this.isCountQuery, this.isExistsQuery, this.isDeleteQuery)) { + throw new IllegalArgumentException(String.format(COUNT_EXISTS_AND_DELETE, method)); + } + + } else { + + this.isCountQuery = false; + this.isExistsQuery = false; + this.isDeleteQuery = false; + } } /* @@ -136,6 +150,15 @@ public class StringBasedMongoQuery extends AbstractMongoQuery { return isCountQuery; } + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.repository.query.AbstractMongoQuery#isExistsQuery() + */ + @Override + protected boolean isExistsQuery() { + return isExistsQuery; + } + /* * (non-Javadoc) * @see org.springframework.data.mongodb.repository.query.AbstractMongoQuery#isDeleteQuery() @@ -145,12 +168,30 @@ public class StringBasedMongoQuery extends AbstractMongoQuery { return this.isDeleteQuery; } + private static boolean hasAmbiguousProjectionFlags(boolean isCountQuery, boolean isExistsQuery, boolean isDeleteQuery) { + return countBooleanValues(isCountQuery, isExistsQuery, isDeleteQuery) > 1; + } + + private static int countBooleanValues(boolean... values) { + + int count = 0; + + for (boolean value : values) { + + if (value) { + count++; + } + } + + return count; + } + /** * A parser that extracts the parameter bindings from a given query string. * * @author Thomas Darimont */ - static enum ParameterBindingParser { + enum ParameterBindingParser { INSTANCE; @@ -257,7 +298,7 @@ public class StringBasedMongoQuery extends AbstractMongoQuery { } else if (value instanceof Pattern) { - String string = ((Pattern) value).toString().trim(); + String string = value.toString().trim(); Matcher valueMatcher = PARSEABLE_BINDING_PATTERN.matcher(string); while (valueMatcher.find()) { diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/AbstractPersonRepositoryIntegrationTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/AbstractPersonRepositoryIntegrationTests.java index 21401a649..ab0747a2d 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/AbstractPersonRepositoryIntegrationTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/AbstractPersonRepositoryIntegrationTests.java @@ -588,6 +588,23 @@ public abstract class AbstractPersonRepositoryIntegrationTests { assertThat(repository.someCountQuery("Matthews"), is(2L)); } + /** + * @see DATAMONGO-1454 + */ + @Test + public void executesDerivedExistsProjectionToBoolean() { + assertThat(repository.existsByFirstname("Oliver August"), is(true)); + assertThat(repository.existsByFirstname("Hans Peter"), is(false)); + } + + /** + * @see DATAMONGO-1454 + */ + @Test + public void executesAnnotatedExistProjection() { + assertThat(repository.someExistQuery("Matthews"), is(true)); + } + /** * @see DATAMONGO-701 */ diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/PersonRepository.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/PersonRepository.java index 52081b67f..849a673f1 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/PersonRepository.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/PersonRepository.java @@ -43,6 +43,7 @@ import org.springframework.data.repository.query.Param; * @author Thomas Darimont * @author Christoph Strobl * @author Fırat KÜÇÜK + * @author Mark Paluch */ public interface PersonRepository extends MongoRepository, QueryDslPredicateExecutor { @@ -251,6 +252,17 @@ public interface PersonRepository extends MongoRepository, Query @Query(value = "{ 'lastname' : ?0 }", count = true) long someCountQuery(String lastname); + /** + * @see DATAMONGO-1454 + */ + boolean existsByFirstname(String firstname); + + /** + * @see DATAMONGO-1454 + */ + @ExistsQuery(value = "{ 'lastname' : ?0 }") + boolean someExistQuery(String lastname); + /** * @see DATAMONGO-770 */ diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/AbstractMongoQueryUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/AbstractMongoQueryUnitTests.java index 03da2ec21..34f4952fa 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/AbstractMongoQueryUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/AbstractMongoQueryUnitTests.java @@ -313,6 +313,7 @@ public class AbstractMongoQueryUnitTests { private static class MongoQueryFake extends AbstractMongoQuery { private boolean isCountQuery; + private boolean isExistsQuery; private boolean isDeleteQuery; public MongoQueryFake(MongoQueryMethod method, MongoOperations operations) { @@ -329,6 +330,11 @@ public class AbstractMongoQueryUnitTests { return isCountQuery; } + @Override + protected boolean isExistsQuery() { + return false; + } + @Override protected boolean isDeleteQuery() { return isDeleteQuery; diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/StringBasedMongoQueryUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/StringBasedMongoQueryUnitTests.java index a65b649ae..24cbd2ce5 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/StringBasedMongoQueryUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/StringBasedMongoQueryUnitTests.java @@ -59,6 +59,7 @@ import com.mongodb.DBRef; * @author Oliver Gierke * @author Christoph Strobl * @author Thomas Darimont + * @author Mark Paluch */ @RunWith(MockitoJUnitRunner.class) public class StringBasedMongoQueryUnitTests { @@ -146,7 +147,7 @@ public class StringBasedMongoQueryUnitTests { public void bindsDbrefCorrectly() throws Exception { StringBasedMongoQuery mongoQuery = createQueryForMethod("findByHavingSizeFansNotZero"); - ConvertingParameterAccessor accessor = StubParameterAccessor.getAccessor(converter, new Object[] {}); + ConvertingParameterAccessor accessor = StubParameterAccessor.getAccessor(converter); org.springframework.data.mongodb.core.query.Query query = mongoQuery.createQuery(accessor); assertThat(query.getQueryObject(), is(new BasicQuery("{ fans : { $not : { $size : 0 } } }").getQueryObject())); @@ -176,8 +177,11 @@ public class StringBasedMongoQueryUnitTests { @Test public void shouldSupportFindByParameterizedCriteriaAndFields() throws Exception { - ConvertingParameterAccessor accessor = StubParameterAccessor.getAccessor(converter, new Object[] { - new Document("firstname", "first").append("lastname", "last"), Collections.singletonMap("lastname", 1) }); + ConvertingParameterAccessor accessor = new ConvertingParameterAccessor(converter, + StubParameterAccessor.getAccessor(converter, // + new Document("firstname", "first").append("lastname", "last"), // + Collections.singletonMap("lastname", 1))); + StringBasedMongoQuery mongoQuery = createQueryForMethod("findByParameterizedCriteriaAndFields", Document.class, Map.class); @@ -194,7 +198,7 @@ public class StringBasedMongoQueryUnitTests { @Test public void shouldSupportRespectExistingQuotingInFindByTitleBeginsWithExplicitQuoting() throws Exception { - ConvertingParameterAccessor accessor = StubParameterAccessor.getAccessor(converter, new Object[] { "fun" }); + ConvertingParameterAccessor accessor = StubParameterAccessor.getAccessor(converter, "fun"); StringBasedMongoQuery mongoQuery = createQueryForMethod("findByTitleBeginsWithExplicitQuoting", String.class); org.springframework.data.mongodb.core.query.Query query = mongoQuery.createQuery(accessor); @@ -209,7 +213,7 @@ public class StringBasedMongoQueryUnitTests { @Test public void shouldParseQueryWithParametersInExpression() throws Exception { - ConvertingParameterAccessor accessor = StubParameterAccessor.getAccessor(converter, new Object[] { 1, 2, 3, 4 }); + ConvertingParameterAccessor accessor = StubParameterAccessor.getAccessor(converter, 1, 2, 3, 4); StringBasedMongoQuery mongoQuery = createQueryForMethod("findByQueryWithParametersInExpression", int.class, int.class, int.class, int.class); @@ -361,6 +365,17 @@ public class StringBasedMongoQueryUnitTests { assertThat(query.getQueryObject().toJson(), is(reference.getQueryObject().toJson())); } + /** + * @see DATAMONGO-1454 + */ + @Test + public void shouldSupportExistsProjection() throws Exception { + + StringBasedMongoQuery mongoQuery = createQueryForMethod("existsByLastname", String.class); + + assertThat(mongoQuery.isExistsQuery(), is(true)); + } + private StringBasedMongoQuery createQueryForMethod(String name, Class... parameters) throws Exception { Method method = SampleRepository.class.getMethod(name, parameters); @@ -419,5 +434,8 @@ public class StringBasedMongoQueryUnitTests { @Query("{'id':?#{ [0] ? { $exists :true} : [1] }, 'foo':42, 'bar': ?#{ [0] ? { $exists :false} : [1] }}") List findByQueryWithExpressionAndMultipleNestedObjects(boolean param0, String param1, String param2); + + @Query(value = "{ 'lastname' : ?0 }", exists = true) + boolean existsByLastname(String lastname); } }