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<Person, String> {

  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.
This commit is contained in:
Mark Paluch
2016-06-28 14:31:26 +02:00
committed by Oliver Gierke
parent 8a5da0e737
commit 5e60867750
10 changed files with 234 additions and 18 deletions

View File

@@ -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 "";
}

View File

@@ -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.
*

View File

@@ -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.
*

View File

@@ -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);
}
}

View File

@@ -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()

View File

@@ -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<ParameterBinding> queryParameterBindings;
private final List<ParameterBinding> 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()) {

View File

@@ -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
*/

View File

@@ -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<Person, String>, QueryDslPredicateExecutor<Person> {
@@ -251,6 +252,17 @@ public interface PersonRepository extends MongoRepository<Person, String>, 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
*/

View File

@@ -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;

View File

@@ -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<Person> findByQueryWithExpressionAndMultipleNestedObjects(boolean param0, String param1, String param2);
@Query(value = "{ 'lastname' : ?0 }", exists = true)
boolean existsByLastname(String lastname);
}
}