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 76e98e8c7..86c8ca1d3 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 @@ -163,7 +163,6 @@ interface MongoQueryExecution { static final class SingleEntityExecution implements MongoQueryExecution { private final MongoOperations operations; - private final boolean countProjection; /* * (non-Javadoc) @@ -171,7 +170,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 4c1da7bd9..f97b64afd 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 @@ -40,6 +40,7 @@ import com.mongodb.util.JSONParseException; * @author Oliver Gierke * @author Christoph Strobl * @author Thomas Darimont + * @author Mark Paluch */ public class PartTreeMongoQuery extends AbstractMongoQuery { @@ -141,6 +142,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 33d2e15e7..c79f872ad 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 @@ -1,5 +1,5 @@ /* - * Copyright 2011-2015 the original author or authors. + * Copyright 2011-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. @@ -42,16 +42,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; @@ -95,14 +97,27 @@ 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; + } } /* @@ -135,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() @@ -144,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 */ - private static enum ParameterBindingParser { + private enum ParameterBindingParser { INSTANCE; @@ -256,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 345151207..9d38a0b5b 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 @@ -589,6 +589,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 7406e6334..83812b560 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 @@ -312,6 +312,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) { @@ -328,6 +329,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 41e0c6ef8..f3b4fe167 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 @@ -61,6 +61,7 @@ import com.mongodb.DBRef; * @author Oliver Gierke * @author Christoph Strobl * @author Thomas Darimont + * @author Mark Paluch */ @RunWith(MockitoJUnitRunner.class) public class StringBasedMongoQueryUnitTests { @@ -148,7 +149,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())); @@ -178,8 +179,7 @@ public class StringBasedMongoQueryUnitTests { @Test public void shouldSupportFindByParameterizedCriteriaAndFields() throws Exception { - ConvertingParameterAccessor accessor = StubParameterAccessor.getAccessor(converter, new Object[] { - new BasicDBObject("firstname", "first").append("lastname", "last"), Collections.singletonMap("lastname", 1) }); + ConvertingParameterAccessor accessor = StubParameterAccessor.getAccessor(converter, new BasicDBObject("firstname", "first").append("lastname", "last"), Collections.singletonMap("lastname", 1)); StringBasedMongoQuery mongoQuery = createQueryForMethod("findByParameterizedCriteriaAndFields", DBObject.class, Map.class); @@ -196,7 +196,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); @@ -210,7 +210,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); @@ -362,6 +362,17 @@ public class StringBasedMongoQueryUnitTests { assertThat(query.getQueryObject(), is(reference.getQueryObject())); } + /** + * @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); @@ -420,5 +431,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); } }