Browse Source

DATAMONGO-566 - Add support for derived delete-by queries.

Using keywords remove or delete in derived query, or setting @Query(delete=true) removes documents matching the query. If the return type is assignable to Number, the total number of affected documents is returned. In case the return type is collection like the query is executed against the store in first place. All documents included in the resulting collection are deleted in a subsequent call.

Additionally findAllAndRemove(…) methods have been added to MongoTemplate.

Original pull request: #147.
pull/151/merge
Christoph Strobl 12 years ago committed by Oliver Gierke
parent
commit
ba48290a3e
  1. 44
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoOperations.java
  2. 134
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoTemplate.java
  3. 11
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/Query.java
  4. 49
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/AbstractMongoQuery.java
  5. 12
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/PartTreeMongoQuery.java
  6. 10
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/StringBasedMongoQuery.java
  7. 23
      spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateTests.java
  8. 46
      spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateUnitTests.java
  9. 79
      spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/AbstractPersonRepositoryIntegrationTests.java
  10. 25
      spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/PersonRepository.java
  11. 202
      spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/AbstracMongoQueryUnitTests.java
  12. 33
      spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/MongoQueryCreatorUnitTests.java
  13. 13
      spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/StringBasedMongoQueryUnitTests.java
  14. 23
      src/docbkx/reference/mongo-repositories.xml

44
spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoOperations.java

@ -50,6 +50,7 @@ import com.mongodb.WriteResult;
* @author Oliver Gierke * @author Oliver Gierke
* @author Tobias Trelle * @author Tobias Trelle
* @author Chuong Ngo * @author Chuong Ngo
* @author Christoph Strobl
*/ */
public interface MongoOperations { public interface MongoOperations {
@ -863,7 +864,7 @@ public interface MongoOperations {
* *
* @param object * @param object
*/ */
void remove(Object object); WriteResult remove(Object object);
/** /**
* Removes the given object from the given collection. * Removes the given object from the given collection.
@ -871,7 +872,7 @@ public interface MongoOperations {
* @param object * @param object
* @param collection must not be {@literal null} or empty. * @param collection must not be {@literal null} or empty.
*/ */
void remove(Object object, String collection); WriteResult remove(Object object, String collection);
/** /**
* Remove all documents that match the provided query document criteria from the the collection used to store the * Remove all documents that match the provided query document criteria from the the collection used to store the
@ -880,7 +881,7 @@ public interface MongoOperations {
* @param query * @param query
* @param entityClass * @param entityClass
*/ */
void remove(Query query, Class<?> entityClass); WriteResult remove(Query query, Class<?> entityClass);
/** /**
* Remove all documents that match the provided query document criteria from the the collection used to store the * Remove all documents that match the provided query document criteria from the the collection used to store the
@ -890,7 +891,7 @@ public interface MongoOperations {
* @param entityClass * @param entityClass
* @param collectionName * @param collectionName
*/ */
void remove(Query query, Class<?> entityClass, String collectionName); WriteResult remove(Query query, Class<?> entityClass, String collectionName);
/** /**
* Remove all documents from the specified collection that match the provided query document criteria. There is no * Remove all documents from the specified collection that match the provided query document criteria. There is no
@ -899,7 +900,40 @@ public interface MongoOperations {
* @param query the query document that specifies the criteria used to remove a record * @param query the query document that specifies the criteria used to remove a record
* @param collectionName name of the collection where the objects will removed * @param collectionName name of the collection where the objects will removed
*/ */
void remove(Query query, String collectionName); WriteResult remove(Query query, String collectionName);
/**
* Returns and removes all documents form the specified collection that match the provided query.
*
* @param query
* @param collectionName
* @return
* @since 1.5
*/
<T> List<T> findAllAndRemove(Query query, String collectionName);
/**
* Returns and removes all documents matching the given query form the collection used to store the entityClass.
*
* @param query
* @param entityClass
* @return
* @since 1.5
*/
<T> List<T> findAllAndRemove(Query query, Class<T> entityClass);
/**
* Returns and removes all documents that match the provided query document criteria from the the collection used to
* store the entityClass. The Class parameter is also used to help convert the Id of the object if it is present in
* the query.
*
* @param query
* @param entityClass
* @param collectionName
* @return
* @since 1.5
*/
<T> List<T> findAllAndRemove(Query query, Class<T> entityClass, String collectionName);
/** /**
* Returns the underlying {@link MongoConverter}. * Returns the underlying {@link MongoConverter}.

134
spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoTemplate.java

@ -27,6 +27,7 @@ import java.util.HashSet;
import java.util.Iterator; import java.util.Iterator;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Map.Entry;
import java.util.Scanner; import java.util.Scanner;
import java.util.Set; import java.util.Set;
@ -47,6 +48,7 @@ import org.springframework.dao.DataAccessException;
import org.springframework.dao.InvalidDataAccessApiUsageException; import org.springframework.dao.InvalidDataAccessApiUsageException;
import org.springframework.dao.OptimisticLockingFailureException; import org.springframework.dao.OptimisticLockingFailureException;
import org.springframework.dao.support.PersistenceExceptionTranslator; import org.springframework.dao.support.PersistenceExceptionTranslator;
import org.springframework.data.annotation.Id;
import org.springframework.data.authentication.UserCredentials; import org.springframework.data.authentication.UserCredentials;
import org.springframework.data.convert.EntityReader; import org.springframework.data.convert.EntityReader;
import org.springframework.data.mapping.PersistentEntity; import org.springframework.data.mapping.PersistentEntity;
@ -95,6 +97,7 @@ import org.springframework.data.mongodb.core.query.Query;
import org.springframework.data.mongodb.core.query.Update; import org.springframework.data.mongodb.core.query.Update;
import org.springframework.jca.cci.core.ConnectionCallback; import org.springframework.jca.cci.core.ConnectionCallback;
import org.springframework.util.Assert; import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
import org.springframework.util.ResourceUtils; import org.springframework.util.ResourceUtils;
import org.springframework.util.StringUtils; import org.springframework.util.StringUtils;
@ -1047,35 +1050,36 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware {
return dbObject.containsField(persistentEntity.getVersionProperty().getFieldName()); return dbObject.containsField(persistentEntity.getVersionProperty().getFieldName());
} }
public void remove(Object object) { public WriteResult remove(Object object) {
if (object == null) { if (object == null) {
return; return null;
} }
remove(getIdQueryFor(object), object.getClass()); return remove(getIdQueryFor(object), object.getClass());
} }
public void remove(Object object, String collection) { public WriteResult remove(Object object, String collection) {
Assert.hasText(collection); Assert.hasText(collection);
if (object == null) { if (object == null) {
return; return null;
} }
doRemove(collection, getIdQueryFor(object), object.getClass()); return doRemove(collection, getIdQueryFor(object), object.getClass());
} }
/** /**
* Returns a {@link Query} for the given entity by its id. * Returns {@link Entry} containing the {@link MongoPersistentProperty} defining the {@literal id} as
* {@link Entry#getKey()} and the {@link Id}s property value as its {@link Entry#getValue()}.
* *
* @param object must not be {@literal null}. * @param object
* @return * @return
*/ */
private Query getIdQueryFor(Object object) { private Map.Entry<MongoPersistentProperty, Object> extractIdPropertyAndValue(Object object) {
Assert.notNull(object); Assert.notNull(object, "Id cannot be extracted from 'null'.");
Class<?> objectType = object.getClass(); Class<?> objectType = object.getClass();
MongoPersistentEntity<?> entity = mappingContext.getPersistentEntity(objectType); MongoPersistentEntity<?> entity = mappingContext.getPersistentEntity(objectType);
@ -1085,11 +1089,44 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware {
throw new MappingException("No id property found for object of type " + objectType); throw new MappingException("No id property found for object of type " + objectType);
} }
ConversionService service = mongoConverter.getConversionService(); Object idValue = BeanWrapper.create(object, mongoConverter.getConversionService())
Object idProperty = null; .getProperty(idProp, Object.class);
return Collections.singletonMap(idProp, idValue).entrySet().iterator().next();
}
/**
* Returns a {@link Query} for the given entity by its id.
*
* @param object must not be {@literal null}.
* @return
*/
private Query getIdQueryFor(Object object) {
Map.Entry<MongoPersistentProperty, Object> id = extractIdPropertyAndValue(object);
return new Query(where(id.getKey().getFieldName()).is(id.getValue()));
}
/**
* Returns a {@link Query} for the given entities by their ids.
*
* @param objects must not be {@literal null} or {@literal empty}.
* @return
*/
private Query getIdInQueryFor(Collection<?> objects) {
Assert.notEmpty(objects, "Cannot create Query for empty collection.");
Iterator<?> it = objects.iterator();
Map.Entry<MongoPersistentProperty, Object> firstEntry = extractIdPropertyAndValue(it.next());
idProperty = BeanWrapper.create(object, service).getProperty(idProp, Object.class); ArrayList<Object> ids = new ArrayList<Object>(objects.size());
return new Query(where(idProp.getFieldName()).is(idProperty)); ids.add(firstEntry.getValue());
while (it.hasNext()) {
ids.add(extractIdPropertyAndValue(it.next()).getValue());
}
return new Query(where(firstEntry.getKey().getFieldName()).in(ids));
} }
private void assertUpdateableIdIfNotSet(Object entity) { private void assertUpdateableIdIfNotSet(Object entity) {
@ -1111,19 +1148,19 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware {
} }
} }
public void remove(Query query, String collectionName) { public WriteResult remove(Query query, String collectionName) {
remove(query, null, collectionName); return remove(query, null, collectionName);
} }
public void remove(Query query, Class<?> entityClass) { public WriteResult remove(Query query, Class<?> entityClass) {
remove(query, entityClass, determineCollectionName(entityClass)); return remove(query, entityClass, determineCollectionName(entityClass));
} }
public void remove(Query query, Class<?> entityClass, String collectionName) { public WriteResult remove(Query query, Class<?> entityClass, String collectionName) {
doRemove(collectionName, query, entityClass); return doRemove(collectionName, query, entityClass);
} }
protected <T> void doRemove(final String collectionName, final Query query, final Class<T> entityClass) { protected <T> WriteResult doRemove(final String collectionName, final Query query, final Class<T> entityClass) {
if (query == null) { if (query == null) {
throw new InvalidDataAccessApiUsageException("Query passed in to remove can't be null!"); throw new InvalidDataAccessApiUsageException("Query passed in to remove can't be null!");
@ -1134,8 +1171,8 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware {
final DBObject queryObject = query.getQueryObject(); final DBObject queryObject = query.getQueryObject();
final MongoPersistentEntity<?> entity = getPersistentEntity(entityClass); final MongoPersistentEntity<?> entity = getPersistentEntity(entityClass);
execute(collectionName, new CollectionCallback<Void>() { return execute(collectionName, new CollectionCallback<WriteResult>() {
public Void doInCollection(DBCollection collection) throws MongoException, DataAccessException { public WriteResult doInCollection(DBCollection collection) throws MongoException, DataAccessException {
maybeEmitEvent(new BeforeDeleteEvent<T>(queryObject, entityClass)); maybeEmitEvent(new BeforeDeleteEvent<T>(queryObject, entityClass));
@ -1151,11 +1188,12 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware {
WriteResult wr = writeConcernToUse == null ? collection.remove(dboq) : collection.remove(dboq, WriteResult wr = writeConcernToUse == null ? collection.remove(dboq) : collection.remove(dboq,
writeConcernToUse); writeConcernToUse);
handleAnyWriteResultErrors(wr, dboq, MongoActionOperation.REMOVE); handleAnyWriteResultErrors(wr, dboq, MongoActionOperation.REMOVE);
maybeEmitEvent(new AfterDeleteEvent<T>(queryObject, entityClass)); maybeEmitEvent(new AfterDeleteEvent<T>(queryObject, entityClass));
return null; return wr;
} }
}); });
} }
@ -1311,6 +1349,54 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware {
return aggregate(aggregation, collectionName, outputType, null); return aggregate(aggregation, collectionName, outputType, null);
} }
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.core.MongoOperations#findAllAndRemove(org.springframework.data.mongodb.core.query.Query, java.lang.String)
*/
@Override
public <T> List<T> findAllAndRemove(Query query, String collectionName) {
return findAndRemove(query, null, collectionName);
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.core.MongoOperations#findAllAndRemove(org.springframework.data.mongodb.core.query.Query, java.lang.Class)
*/
@Override
public <T> List<T> findAllAndRemove(Query query, Class<T> entityClass) {
return findAllAndRemove(query, entityClass, determineCollectionName(entityClass));
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.core.MongoOperations#findAllAndRemove(org.springframework.data.mongodb.core.query.Query, java.lang.Class, java.lang.String)
*/
@Override
public <T> List<T> findAllAndRemove(Query query, Class<T> entityClass, String collectionName) {
return doFindAndDelete(collectionName, query, entityClass);
}
/**
* Retrieve and remove all documents matching the given {@code query} by calling {@link #find(Query, Class, String)}
* and {@link #remove(Query, Class, String)}, whereas the {@link Query} for {@link #remove(Query, Class, String)} is
* constructed out of the find result.
*
* @param collectionName
* @param query
* @param entityClass
* @return
*/
protected <T> List<T> doFindAndDelete(String collectionName, Query query, Class<T> entityClass) {
List<T> result = find(query, entityClass, collectionName);
if (!CollectionUtils.isEmpty(result)) {
remove(getIdInQueryFor(result), entityClass, collectionName);
}
return result;
}
protected <O> AggregationResults<O> aggregate(Aggregation aggregation, String collectionName, Class<O> outputType, protected <O> AggregationResults<O> aggregate(Aggregation aggregation, String collectionName, Class<O> outputType,
AggregationOperationContext context) { AggregationOperationContext context) {

11
spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/Query.java

@ -1,5 +1,5 @@
/* /*
* Copyright 2011-2013 the original author or authors. * Copyright 2011-2014 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -29,6 +29,7 @@ import org.springframework.data.annotation.QueryAnnotation;
* *
* @author Oliver Gierke * @author Oliver Gierke
* @author Thomas Darimont * @author Thomas Darimont
* @author Christoph Strobl
*/ */
@Retention(RetentionPolicy.RUNTIME) @Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD) @Target(ElementType.METHOD)
@ -59,4 +60,12 @@ public @interface Query {
* @return * @return
*/ */
boolean count() default false; boolean count() default false;
/**
* Returns whether the query should delete matching documents.
*
* @since 1.5
* @return
*/
boolean delete() default false;
} }

49
spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/AbstractMongoQuery.java

@ -37,11 +37,14 @@ import org.springframework.data.repository.query.RepositoryQuery;
import org.springframework.data.util.TypeInformation; import org.springframework.data.util.TypeInformation;
import org.springframework.util.Assert; import org.springframework.util.Assert;
import com.mongodb.WriteResult;
/** /**
* Base class for {@link RepositoryQuery} implementations for Mongo. * Base class for {@link RepositoryQuery} implementations for Mongo.
* *
* @author Oliver Gierke * @author Oliver Gierke
* @author Thomas Darimont * @author Thomas Darimont
* @author Christoph Strobl
*/ */
public abstract class AbstractMongoQuery implements RepositoryQuery { public abstract class AbstractMongoQuery implements RepositoryQuery {
@ -84,7 +87,9 @@ public abstract class AbstractMongoQuery implements RepositoryQuery {
Object result = null; Object result = null;
if (method.isGeoNearQuery() && method.isPageQuery()) { if (isDeleteQuery()) {
result = new DeleteExecution().execute(query);
} else if (method.isGeoNearQuery() && method.isPageQuery()) {
MongoParameterAccessor countAccessor = new MongoParametersParameterAccessor(method, parameters); MongoParameterAccessor countAccessor = new MongoParametersParameterAccessor(method, parameters);
Query countQuery = createCountQuery(new ConvertingParameterAccessor(operations.getConverter(), countAccessor)); Query countQuery = createCountQuery(new ConvertingParameterAccessor(operations.getConverter(), countAccessor));
@ -142,6 +147,14 @@ public abstract class AbstractMongoQuery implements RepositoryQuery {
*/ */
protected abstract boolean isCountQuery(); protected abstract boolean isCountQuery();
/**
* Return weather the query should delete matching documents.
*
* @return
* @since 1.5
*/
protected abstract boolean isDeleteQuery();
private abstract class Execution { private abstract class Execution {
abstract Object execute(Query query); abstract Object execute(Query query);
@ -351,4 +364,38 @@ public abstract class AbstractMongoQuery implements RepositoryQuery {
return componentType == null ? false : GeoResult.class.equals(componentType.getType()); return componentType == null ? false : GeoResult.class.equals(componentType.getType());
} }
} }
/**
* {@link Execution} removing documents matching the query.
*
* @since 1.5
*/
final class DeleteExecution extends Execution {
@Override
Object execute(Query query) {
MongoEntityMetadata<?> metadata = method.getEntityInformation();
return deleteAndConvertResult(query, metadata);
}
private Object deleteAndConvertResult(Query query, MongoEntityMetadata<?> metadata) {
if (method.isCollectionQuery()) {
return findAndRemove(query, metadata);
}
WriteResult writeResult = remove(query, metadata);
return writeResult != null ? writeResult.getN() : 0L;
}
private List<?> findAndRemove(Query query, MongoEntityMetadata<?> metadata) {
return operations.findAllAndRemove(query, metadata.getJavaType());
}
private WriteResult remove(Query query, MongoEntityMetadata<?> metadata) {
return operations.remove(query, metadata.getCollectionName());
}
}
} }

12
spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/PartTreeMongoQuery.java

@ -1,5 +1,5 @@
/* /*
* Copyright 2002-2013 the original author or authors. * Copyright 2002-2014 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -28,6 +28,7 @@ import org.springframework.data.repository.query.parser.PartTree;
* {@link RepositoryQuery} implementation for Mongo. * {@link RepositoryQuery} implementation for Mongo.
* *
* @author Oliver Gierke * @author Oliver Gierke
* @author Christoph Strobl
*/ */
public class PartTreeMongoQuery extends AbstractMongoQuery { public class PartTreeMongoQuery extends AbstractMongoQuery {
@ -86,4 +87,13 @@ public class PartTreeMongoQuery extends AbstractMongoQuery {
protected boolean isCountQuery() { protected boolean isCountQuery() {
return tree.isCountProjection(); return tree.isCountProjection();
} }
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.repository.query.AbstractMongoQuery#isDeleteQuery()
*/
@Override
protected boolean isDeleteQuery() {
return tree.isDelete();
}
} }

10
spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/StringBasedMongoQuery.java

@ -1,5 +1,5 @@
/* /*
* Copyright 2011-2013 the original author or authors. * Copyright 2011-2014 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -30,6 +30,7 @@ import com.mongodb.util.JSON;
* Query to use a plain JSON String to create the {@link Query} to actually execute. * Query to use a plain JSON String to create the {@link Query} to actually execute.
* *
* @author Oliver Gierke * @author Oliver Gierke
* @author Christoph Strobl
*/ */
public class StringBasedMongoQuery extends AbstractMongoQuery { public class StringBasedMongoQuery extends AbstractMongoQuery {
@ -39,6 +40,7 @@ public class StringBasedMongoQuery extends AbstractMongoQuery {
private final String query; private final String query;
private final String fieldSpec; private final String fieldSpec;
private final boolean isCountQuery; private final boolean isCountQuery;
private final boolean isDeleteQuery;
/** /**
* Creates a new {@link StringBasedMongoQuery}. * Creates a new {@link StringBasedMongoQuery}.
@ -53,6 +55,7 @@ public class StringBasedMongoQuery extends AbstractMongoQuery {
this.query = query; this.query = query;
this.fieldSpec = method.getFieldSpecification(); this.fieldSpec = method.getFieldSpecification();
this.isCountQuery = method.hasAnnotatedQuery() ? method.getQueryAnnotation().count() : false; this.isCountQuery = method.hasAnnotatedQuery() ? method.getQueryAnnotation().count() : false;
this.isDeleteQuery = method.hasAnnotatedQuery() ? method.getQueryAnnotation().delete() : false;
} }
public StringBasedMongoQuery(MongoQueryMethod method, MongoOperations mongoOperations) { public StringBasedMongoQuery(MongoQueryMethod method, MongoOperations mongoOperations) {
@ -95,6 +98,11 @@ public class StringBasedMongoQuery extends AbstractMongoQuery {
return isCountQuery; return isCountQuery;
} }
@Override
protected boolean isDeleteQuery() {
return this.isDeleteQuery;
}
private String replacePlaceholders(String input, ConvertingParameterAccessor accessor) { private String replacePlaceholders(String input, ConvertingParameterAccessor accessor) {
Matcher matcher = PLACEHOLDER.matcher(input); Matcher matcher = PLACEHOLDER.matcher(input);

23
spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateTests.java

@ -2491,6 +2491,29 @@ public class MongoTemplateTests {
assertThat(result.get(0).dbRefProperty.field, is(sample.field)); assertThat(result.get(0).dbRefProperty.field, is(sample.field));
} }
/**
* @see DATAMONGO-566
*/
@Test
public void testFindAllAndRemoveFullyReturnsAndRemovesDocuments() {
Sample spring = new Sample("100", "spring");
Sample data = new Sample("200", "data");
Sample mongodb = new Sample("300", "mongodb");
template.insert(Arrays.asList(spring, data, mongodb), Sample.class);
Query qry = query(where("field").in("spring", "mongodb"));
List<Sample> result = template.findAllAndRemove(qry, Sample.class);
assertThat(result, hasSize(2));
assertThat(
template.getDb().getCollection("sample")
.find(new BasicDBObject("field", new BasicDBObject("$in", Arrays.asList("spring", "mongodb")))).count(),
is(0));
assertThat(template.getDb().getCollection("sample").find(new BasicDBObject("field", "data")).count(), is(1));
}
static class DocumentWithDBRefCollection { static class DocumentWithDBRefCollection {
@Id public String id; @Id public String id;

46
spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateUnitTests.java

@ -49,6 +49,7 @@ import org.springframework.data.mongodb.core.convert.MappingMongoConverter;
import org.springframework.data.mongodb.core.convert.QueryMapper; import org.springframework.data.mongodb.core.convert.QueryMapper;
import org.springframework.data.mongodb.core.index.MongoPersistentEntityIndexCreator; import org.springframework.data.mongodb.core.index.MongoPersistentEntityIndexCreator;
import org.springframework.data.mongodb.core.mapping.MongoMappingContext; import org.springframework.data.mongodb.core.mapping.MongoMappingContext;
import org.springframework.data.mongodb.core.query.BasicQuery;
import org.springframework.data.mongodb.core.query.Query; import org.springframework.data.mongodb.core.query.Query;
import org.springframework.data.mongodb.core.query.Update; import org.springframework.data.mongodb.core.query.Update;
import org.springframework.test.util.ReflectionTestUtils; import org.springframework.test.util.ReflectionTestUtils;
@ -56,6 +57,7 @@ import org.springframework.test.util.ReflectionTestUtils;
import com.mongodb.BasicDBObject; import com.mongodb.BasicDBObject;
import com.mongodb.DB; import com.mongodb.DB;
import com.mongodb.DBCollection; import com.mongodb.DBCollection;
import com.mongodb.DBCursor;
import com.mongodb.DBObject; import com.mongodb.DBObject;
import com.mongodb.Mongo; import com.mongodb.Mongo;
import com.mongodb.MongoException; import com.mongodb.MongoException;
@ -75,6 +77,7 @@ public class MongoTemplateUnitTests extends MongoOperationsUnitTests {
@Mock Mongo mongo; @Mock Mongo mongo;
@Mock DB db; @Mock DB db;
@Mock DBCollection collection; @Mock DBCollection collection;
@Mock DBCursor cursor;
MongoExceptionTranslator exceptionTranslator = new MongoExceptionTranslator(); MongoExceptionTranslator exceptionTranslator = new MongoExceptionTranslator();
MappingMongoConverter converter; MappingMongoConverter converter;
@ -86,11 +89,14 @@ public class MongoTemplateUnitTests extends MongoOperationsUnitTests {
when(factory.getDb()).thenReturn(db); when(factory.getDb()).thenReturn(db);
when(factory.getExceptionTranslator()).thenReturn(exceptionTranslator); when(factory.getExceptionTranslator()).thenReturn(exceptionTranslator);
when(db.getCollection(Mockito.any(String.class))).thenReturn(collection); when(db.getCollection(Mockito.any(String.class))).thenReturn(collection);
when(collection.find(Mockito.any(DBObject.class))).thenReturn(cursor);
when(cursor.limit(anyInt())).thenReturn(cursor);
when(cursor.sort(Mockito.any(DBObject.class))).thenReturn(cursor);
when(cursor.hint(anyString())).thenReturn(cursor);
this.mappingContext = new MongoMappingContext(); this.mappingContext = new MongoMappingContext();
this.converter = new MappingMongoConverter(new DefaultDbRefResolver(factory), mappingContext); this.converter = new MappingMongoConverter(new DefaultDbRefResolver(factory), mappingContext);
this.template = new MongoTemplate(factory, converter); this.template = new MongoTemplate(factory, converter);
} }
@Test(expected = IllegalArgumentException.class) @Test(expected = IllegalArgumentException.class)
@ -282,6 +288,44 @@ public class MongoTemplateUnitTests extends MongoOperationsUnitTests {
})); }));
} }
/**
* @see DATAMONGO-566
*/
@Test
public void findAllAndRemoveShouldRetrieveMatchingDocumentsPriorToRemoval() {
BasicQuery query = new BasicQuery("{'foo':'bar'}");
template.findAllAndRemove(query, VersionedEntity.class);
verify(collection, times(1)).find(Matchers.eq(query.getQueryObject()));
}
/**
* @see DATAMONGO-566
*/
@Test
public void findAllAndRemoveShouldRemoveDocumentsReturedByFindQuery() {
Mockito.when(cursor.hasNext()).thenReturn(true).thenReturn(true).thenReturn(false);
Mockito.when(cursor.next()).thenReturn(new BasicDBObject("_id", Integer.valueOf(0)))
.thenReturn(new BasicDBObject("_id", Integer.valueOf(1)));
ArgumentCaptor<DBObject> queryCaptor = ArgumentCaptor.forClass(DBObject.class);
BasicQuery query = new BasicQuery("{'foo':'bar'}");
template.findAllAndRemove(query, VersionedEntity.class);
verify(collection, times(1)).remove(queryCaptor.capture());
DBObject idField = DBObjectTestUtils.getAsDBObject(queryCaptor.getValue(), "_id");
assertThat((Object[]) idField.get("$in"), is(new Object[] { Integer.valueOf(0), Integer.valueOf(1) }));
}
@Test
public void findAllAndRemoveShouldNotTriggerRemoveIfFindResultIsEmpty() {
template.findAllAndRemove(new BasicQuery("{'foo':'bar'}"), VersionedEntity.class);
verify(collection, never()).remove(Mockito.any(DBObject.class));
}
class AutogenerateableId { class AutogenerateableId {
@Id BigInteger id; @Id BigInteger id;

79
spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/AbstractPersonRepositoryIntegrationTests.java

@ -43,6 +43,7 @@ import org.springframework.data.mongodb.core.geo.Metric;
import org.springframework.data.mongodb.core.geo.Metrics; import org.springframework.data.mongodb.core.geo.Metrics;
import org.springframework.data.mongodb.core.geo.Point; import org.springframework.data.mongodb.core.geo.Point;
import org.springframework.data.mongodb.core.geo.Polygon; import org.springframework.data.mongodb.core.geo.Polygon;
import org.springframework.data.mongodb.core.query.BasicQuery;
import org.springframework.data.mongodb.repository.Person.Sex; import org.springframework.data.mongodb.repository.Person.Sex;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
@ -763,8 +764,7 @@ public abstract class AbstractPersonRepositoryIntegrationTests {
assertThat(result, is(arrayWithSize(1))); assertThat(result, is(arrayWithSize(1)));
assertThat(result, is(arrayContaining(leroi))); assertThat(result, is(arrayContaining(leroi)));
} }
/** /**
* @see DATAMONGO-821 * @see DATAMONGO-821
*/ */
@ -784,4 +784,79 @@ public abstract class AbstractPersonRepositoryIntegrationTests {
assertThat(result.getNumberOfElements(), is(1)); assertThat(result.getNumberOfElements(), is(1));
assertThat(result.getContent().get(0), is(alicia)); assertThat(result.getContent().get(0), is(alicia));
} }
/**
* @see DATAMONGO-566
*/
@Test
public void deleteByShouldReturnListOfDeletedElementsWhenRetunTypeIsCollectionLike() {
List<Person> result = repository.deleteByLastname("Beauford");
assertThat(result, hasItem(carter));
assertThat(result, hasSize(1));
}
/**
* @see DATAMONGO-566
*/
@Test
public void deleteByShouldRemoveElementsMatchingDerivedQuery() {
repository.deleteByLastname("Beauford");
assertThat(operations.count(new BasicQuery("{'lastname':'Beauford'}"), Person.class), is(0L));
}
/**
* @see DATAMONGO-566
*/
@Test
public void deleteByShouldReturnNumberOfDocumentsRemovedIfReturnTypeIsLong() {
assertThat(repository.deletePersonByLastname("Beauford"), is(1L));
}
/**
* @see DATAMONGO-566
*/
@Test
public void deleteByShouldReturnZeroInCaseNoDocumentHasBeenRemovedAndReturnTypeIsNumber() {
assertThat(repository.deletePersonByLastname("dorfuaeB"), is(0L));
}
/**
* @see DATAMONGO-566
*/
@Test
public void deleteByShouldReturnEmptyListInCaseNoDocumentHasBeenRemovedAndReturnTypeIsCollectionLike() {
assertThat(repository.deleteByLastname("dorfuaeB"), empty());
}
/**
* @see DATAMONGO-566
*/
@Test
public void deleteByUsingAnnotatedQueryShouldReturnListOfDeletedElementsWhenRetunTypeIsCollectionLike() {
List<Person> result = repository.removeByLastnameUsingAnnotatedQuery("Beauford");
assertThat(result, hasItem(carter));
assertThat(result, hasSize(1));
}
/**
* @see DATAMONGO-566
*/
@Test
public void deleteByUsingAnnotatedQueryShouldRemoveElementsMatchingDerivedQuery() {
repository.removeByLastnameUsingAnnotatedQuery("Beauford");
assertThat(operations.count(new BasicQuery("{'lastname':'Beauford'}"), Person.class), is(0L));
}
/**
* @see DATAMONGO-566
*/
@Test
public void deleteByUsingAnnotatedQueryShouldReturnNumberOfDocumentsRemovedIfReturnTypeIsLong() {
assertThat(repository.removePersonByLastnameUsingAnnotatedQuery("Beauford"), is(1L));
}
} }

25
spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/PersonRepository.java

@ -257,10 +257,33 @@ public interface PersonRepository extends MongoRepository<Person, String>, Query
* @see DATAMONGO-870 * @see DATAMONGO-870
*/ */
Slice<Person> findByAgeGreaterThan(int age, Pageable pageable); Slice<Person> findByAgeGreaterThan(int age, Pageable pageable);
/** /**
* @see DATAMONGO-821 * @see DATAMONGO-821
*/ */
@Query("{ creator : { $exists : true } }") @Query("{ creator : { $exists : true } }")
Page<Person> findByHavingCreator(Pageable page); Page<Person> findByHavingCreator(Pageable page);
/**
* @see DATAMONGO-566
*/
List<Person> deleteByLastname(String lastname);
/**
* @see DATAMONGO-566
*/
Long deletePersonByLastname(String lastname);
/**
* @see DATAMONGO-566
*/
@Query(value = "{ 'lastname' : ?0 }", delete = true)
List<Person> removeByLastnameUsingAnnotatedQuery(String lastname);
/**
* @see DATAMONGO-566
*/
@Query(value = "{ 'lastname' : ?0 }", delete = true)
Long removePersonByLastnameUsingAnnotatedQuery(String lastname);
} }

202
spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/AbstracMongoQueryUnitTests.java

@ -0,0 +1,202 @@
/*
* Copyright 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.
* 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.query;
import static org.junit.Assert.*;
import static org.mockito.Mockito.*;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
import org.bson.types.ObjectId;
import org.hamcrest.core.Is;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Matchers;
import org.mockito.Mock;
import org.mockito.runners.MockitoJUnitRunner;
import org.springframework.data.mongodb.MongoDbFactory;
import org.springframework.data.mongodb.core.MongoOperations;
import org.springframework.data.mongodb.core.Person;
import org.springframework.data.mongodb.core.convert.DbRefResolver;
import org.springframework.data.mongodb.core.convert.DefaultDbRefResolver;
import org.springframework.data.mongodb.core.convert.MappingMongoConverter;
import org.springframework.data.mongodb.core.mapping.BasicMongoPersistentEntity;
import org.springframework.data.mongodb.core.mapping.MongoMappingContext;
import org.springframework.data.mongodb.core.query.BasicQuery;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.data.mongodb.repository.MongoRepository;
import org.springframework.data.repository.core.RepositoryMetadata;
import com.mongodb.WriteResult;
/**
* @author Christoph Strobl
*/
@RunWith(MockitoJUnitRunner.class)
public class MongoQueryExecutionUnitTests {
private @Mock RepositoryMetadata metadataMock;
private @Mock MongoOperations mongoOperationsMock;
@SuppressWarnings("rawtypes")//
private @Mock BasicMongoPersistentEntity persitentEntityMock;
private @Mock MongoMappingContext mappingContextMock;
private @Mock WriteResult writeResultMock;
@SuppressWarnings({ "unchecked", "rawtypes" })
@Before
public void setUp() {
when(metadataMock.getDomainType()).thenReturn((Class) Person.class);
when(metadataMock.getReturnedDomainClass(Matchers.any(Method.class))).thenReturn((Class) Person.class);
when(persitentEntityMock.getCollection()).thenReturn("persons");
when(mappingContextMock.getPersistentEntity(Matchers.any(Class.class))).thenReturn(persitentEntityMock);
when(persitentEntityMock.getType()).thenReturn(Person.class);
DbRefResolver dbRefResolver = new DefaultDbRefResolver(mock(MongoDbFactory.class));
MappingMongoConverter converter = new MappingMongoConverter(dbRefResolver, mappingContextMock);
converter.afterPropertiesSet();
when(mongoOperationsMock.getConverter()).thenReturn(converter);
}
/**
* @see DATAMONGO-566
*/
@SuppressWarnings("unchecked")
@Test
public void testDeleteExecutionCallsRemoveCorreclty() {
createQueryForMethod("deletePersonByLastname", String.class).setDeleteQuery(true).execute(new Object[] { "booh" });
verify(this.mongoOperationsMock, times(1)).remove(Matchers.any(Query.class), Matchers.eq("persons"));
verify(this.mongoOperationsMock, times(0)).find(Matchers.any(Query.class), Matchers.any(Class.class),
Matchers.anyString());
}
/**
* @see DATAMONGO-566
*/
@SuppressWarnings("unchecked")
@Test
public void testDeleteExecutionLoadsListOfRemovedDocumentsWhenReturnTypeIsCollectionLike() {
when(this.mongoOperationsMock.find(Matchers.any(Query.class), Matchers.any(Class.class), Matchers.anyString()))
.thenReturn(Arrays.asList(new Person(new ObjectId(new Date()), "bar")));
createQueryForMethod("deleteByLastname", String.class).setDeleteQuery(true).execute(new Object[] { "booh" });
verify(this.mongoOperationsMock, times(1)).findAllAndRemove(Matchers.any(Query.class), Matchers.eq(Person.class));
}
/**
* @see DATAMONGO-566
*/
@Test
public void testDeleteExecutionReturnsZeroWhenWriteResultIsNull() {
MongoQueryFake query = createQueryForMethod("deletePersonByLastname", String.class);
query.setDeleteQuery(true);
assertThat(query.execute(new Object[] { "fake" }), Is.<Object> is(0L));
}
/**
* @see DATAMONGO-566
*/
@Test
public void testDeleteExecutionReturnsNrDocumentsDeletedFromWriteResult() {
when(writeResultMock.getN()).thenReturn(100);
when(this.mongoOperationsMock.remove(Matchers.any(Query.class), Matchers.eq("persons")))
.thenReturn(writeResultMock);
MongoQueryFake query = createQueryForMethod("deletePersonByLastname", String.class);
query.setDeleteQuery(true);
assertThat(query.execute(new Object[] { "fake" }), Is.<Object> is(100L));
verify(this.mongoOperationsMock, times(1)).remove(Matchers.any(Query.class), Matchers.eq("persons"));
}
private MongoQueryFake createQueryForMethod(String methodName, Class<?>... paramTypes) {
try {
return this.createQueryForMethod(Repo.class.getMethod(methodName, paramTypes));
} catch (NoSuchMethodException e) {
throw new IllegalArgumentException(e.getMessage(), e);
} catch (SecurityException e) {
throw new IllegalArgumentException(e.getMessage(), e);
}
}
private MongoQueryFake createQueryForMethod(Method method) {
return new MongoQueryFake(createMongoQueryMethodFrom(method), this.mongoOperationsMock);
}
private MongoQueryMethod createMongoQueryMethodFrom(Method method) {
return new MongoQueryMethod(method, metadataMock, this.mappingContextMock);
}
class MongoQueryFake extends AbstractMongoQuery {
public MongoQueryFake(MongoQueryMethod method, MongoOperations operations) {
super(method, operations);
}
private boolean isCountQuery;
private boolean isDeleteQuery;
@Override
protected Query createQuery(ConvertingParameterAccessor accessor) {
return new BasicQuery("{'foo':'bar'}");
}
@Override
protected boolean isCountQuery() {
return isCountQuery;
}
@Override
protected boolean isDeleteQuery() {
return isDeleteQuery;
}
public MongoQueryFake setCountQuery(boolean isCountQuery) {
this.isCountQuery = isCountQuery;
return this;
}
public MongoQueryFake setDeleteQuery(boolean isDeleteQuery) {
this.isDeleteQuery = isDeleteQuery;
return this;
}
}
private interface Repo extends MongoRepository<Person, Long> {
List<Person> deleteByLastname(String lastname);
Long deletePersonByLastname(String lastname);
}
}

33
spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/MongoQueryCreatorUnitTests.java

@ -1,5 +1,5 @@
/* /*
* Copyright 2011-2013 the original author or authors. * Copyright 2011-2014 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -58,6 +58,7 @@ import org.springframework.data.util.TypeInformation;
* *
* @author Oliver Gierke * @author Oliver Gierke
* @author Thomas Darimont * @author Thomas Darimont
* @author Christoph Strobl
*/ */
@RunWith(MockitoJUnitRunner.class) @RunWith(MockitoJUnitRunner.class)
public class MongoQueryCreatorUnitTests { public class MongoQueryCreatorUnitTests {
@ -407,6 +408,36 @@ public class MongoQueryCreatorUnitTests {
assertThat(query, is(query(where("firstName").regex("^dave$", "i").and("age").is(42)))); assertThat(query, is(query(where("firstName").regex("^dave$", "i").and("age").is(42))));
} }
/**
* @see DATAMONGO-566
*/
@Test
public void shouldCreateDeleteByQueryCorrectly() {
PartTree tree = new PartTree("deleteByFirstName", Person.class);
MongoQueryCreator creator = new MongoQueryCreator(tree, getAccessor(converter, "dave", 42), context);
Query query = creator.createQuery();
assertThat(tree.isDelete(), is(true));
assertThat(query, is(query(where("firstName").is("dave"))));
}
/**
* @see DATAMONGO-566
*/
@Test
public void shouldCreateDeleteByQueryCorrectlyForMultipleCriteriaAndCaseExpressions() {
PartTree tree = new PartTree("deleteByFirstNameAndAgeAllIgnoreCase", Person.class);
MongoQueryCreator creator = new MongoQueryCreator(tree, getAccessor(converter, "dave", 42), context);
Query query = creator.createQuery();
assertThat(tree.isDelete(), is(true));
assertThat(query, is(query(where("firstName").regex("^dave$", "i").and("age").is(42))));
}
interface PersonRepository extends Repository<Person, Long> { interface PersonRepository extends Repository<Person, Long> {
List<Person> findByLocationNearAndFirstname(Point location, Distance maxDistance, String firstname); List<Person> findByLocationNearAndFirstname(Point location, Distance maxDistance, String firstname);

13
spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/StringBasedMongoQueryUnitTests.java

@ -140,6 +140,16 @@ public class StringBasedMongoQueryUnitTests {
assertThat(query.getQueryObject(), is(new BasicQuery("{ fans : { $not : { $size : 0 } } }").getQueryObject())); assertThat(query.getQueryObject(), is(new BasicQuery("{ fans : { $not : { $size : 0 } } }").getQueryObject()));
} }
/**
* @see DATAMONGO-566
*/
@Test
public void constructsDeleteQueryCorrectly() throws Exception {
StringBasedMongoQuery mongoQuery = createQueryForMethod("removeByLastname", String.class);
assertThat(mongoQuery.isDeleteQuery(), is(true));
}
private StringBasedMongoQuery createQueryForMethod(String name, Class<?>... parameters) throws Exception { private StringBasedMongoQuery createQueryForMethod(String name, Class<?>... parameters) throws Exception {
Method method = SampleRepository.class.getMethod(name, parameters); Method method = SampleRepository.class.getMethod(name, parameters);
@ -161,5 +171,8 @@ public class StringBasedMongoQueryUnitTests {
@Query("{ fans : { $not : { $size : 0 } } }") @Query("{ fans : { $not : { $size : 0 } } }")
Person findByHavingSizeFansNotZero(); Person findByHavingSizeFansNotZero();
@Query(value = "{ 'lastname' : ?0 }", delete = true)
void removeByLastname(String lastname);
} }
} }

23
src/docbkx/reference/mongo-repositories.xml

@ -394,6 +394,29 @@ public class PersonRepositoryTests {
</tgroup> </tgroup>
</table></para> </table></para>
<section id="mongodb.repositories.queries.delete">
<title>Repository delete queries</title>
<para>The above keywords can be used in conjunciton with
<code>delete…By</code> or <code>remove…By</code> to create queries
deleting matching documents.</para>
<example>
<title><code>Delete…By</code> Query</title>
<programlisting language="java">public interface PersonRepository extends MongoRepository&lt;Person, String&gt; {
List &lt;Person&gt; deleteByLastname(String lastname);
Long deletePersonByLastname(String lastname);
}</programlisting>
</example>
<para>Using return type <interfacename>List</interfacename> will
retrieve and return all matching documents before actually deleting
them. A numeric return type directly removes the matching documents
returning the total number of documents removed.</para>
</section>
<section id="mongodb.repositories.queries.geo-spatial"> <section id="mongodb.repositories.queries.geo-spatial">
<title>Geo-spatial repository queries</title> <title>Geo-spatial repository queries</title>

Loading…
Cancel
Save