diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoOperations.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoOperations.java index 36ecb036a..3f003a1f8 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoOperations.java +++ b/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 Tobias Trelle * @author Chuong Ngo + * @author Christoph Strobl */ public interface MongoOperations { @@ -863,7 +864,7 @@ public interface MongoOperations { * * @param object */ - void remove(Object object); + WriteResult remove(Object object); /** * Removes the given object from the given collection. @@ -871,7 +872,7 @@ public interface MongoOperations { * @param object * @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 @@ -880,7 +881,7 @@ public interface MongoOperations { * @param query * @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 @@ -890,7 +891,7 @@ public interface MongoOperations { * @param entityClass * @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 @@ -899,7 +900,40 @@ public interface MongoOperations { * @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 */ - 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 + */ + List 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 + */ + List findAllAndRemove(Query query, Class 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 + */ + List findAllAndRemove(Query query, Class entityClass, String collectionName); /** * Returns the underlying {@link MongoConverter}. diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoTemplate.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoTemplate.java index 3f1427fa7..682465868 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoTemplate.java +++ b/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.List; import java.util.Map; +import java.util.Map.Entry; import java.util.Scanner; import java.util.Set; @@ -47,6 +48,7 @@ import org.springframework.dao.DataAccessException; import org.springframework.dao.InvalidDataAccessApiUsageException; import org.springframework.dao.OptimisticLockingFailureException; import org.springframework.dao.support.PersistenceExceptionTranslator; +import org.springframework.data.annotation.Id; import org.springframework.data.authentication.UserCredentials; import org.springframework.data.convert.EntityReader; 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.jca.cci.core.ConnectionCallback; import org.springframework.util.Assert; +import org.springframework.util.CollectionUtils; import org.springframework.util.ResourceUtils; import org.springframework.util.StringUtils; @@ -1047,35 +1050,36 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware { return dbObject.containsField(persistentEntity.getVersionProperty().getFieldName()); } - public void remove(Object object) { + public WriteResult remove(Object object) { 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); 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 */ - private Query getIdQueryFor(Object object) { + private Map.Entry extractIdPropertyAndValue(Object object) { - Assert.notNull(object); + Assert.notNull(object, "Id cannot be extracted from 'null'."); Class objectType = object.getClass(); 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); } - ConversionService service = mongoConverter.getConversionService(); - Object idProperty = null; + Object idValue = BeanWrapper.create(object, mongoConverter.getConversionService()) + .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 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 firstEntry = extractIdPropertyAndValue(it.next()); - idProperty = BeanWrapper.create(object, service).getProperty(idProp, Object.class); - return new Query(where(idProp.getFieldName()).is(idProperty)); + ArrayList ids = new ArrayList(objects.size()); + 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) { @@ -1111,19 +1148,19 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware { } } - public void remove(Query query, String collectionName) { - remove(query, null, collectionName); + public WriteResult remove(Query query, String collectionName) { + return remove(query, null, collectionName); } - public void remove(Query query, Class entityClass) { - remove(query, entityClass, determineCollectionName(entityClass)); + public WriteResult remove(Query query, Class entityClass) { + return remove(query, entityClass, determineCollectionName(entityClass)); } - public void remove(Query query, Class entityClass, String collectionName) { - doRemove(collectionName, query, entityClass); + public WriteResult remove(Query query, Class entityClass, String collectionName) { + return doRemove(collectionName, query, entityClass); } - protected void doRemove(final String collectionName, final Query query, final Class entityClass) { + protected WriteResult doRemove(final String collectionName, final Query query, final Class entityClass) { if (query == 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 MongoPersistentEntity entity = getPersistentEntity(entityClass); - execute(collectionName, new CollectionCallback() { - public Void doInCollection(DBCollection collection) throws MongoException, DataAccessException { + return execute(collectionName, new CollectionCallback() { + public WriteResult doInCollection(DBCollection collection) throws MongoException, DataAccessException { maybeEmitEvent(new BeforeDeleteEvent(queryObject, entityClass)); @@ -1151,11 +1188,12 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware { WriteResult wr = writeConcernToUse == null ? collection.remove(dboq) : collection.remove(dboq, writeConcernToUse); + handleAnyWriteResultErrors(wr, dboq, MongoActionOperation.REMOVE); maybeEmitEvent(new AfterDeleteEvent(queryObject, entityClass)); - return null; + return wr; } }); } @@ -1311,6 +1349,54 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware { 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 List 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 List findAllAndRemove(Query query, Class 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 List findAllAndRemove(Query query, Class 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 List doFindAndDelete(String collectionName, Query query, Class entityClass) { + + List result = find(query, entityClass, collectionName); + + if (!CollectionUtils.isEmpty(result)) { + remove(getIdInQueryFor(result), entityClass, collectionName); + } + + return result; + } + protected AggregationResults aggregate(Aggregation aggregation, String collectionName, Class outputType, AggregationOperationContext context) { 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 138cd7b70..fa9ed9b32 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 @@ -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"); * 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 Thomas Darimont + * @author Christoph Strobl */ @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) @@ -59,4 +60,12 @@ public @interface Query { * @return */ boolean count() default false; + + /** + * Returns whether the query should delete matching documents. + * + * @since 1.5 + * @return + */ + boolean delete() default false; } 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 7dab99565..85a60dbaa 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 @@ -37,11 +37,14 @@ import org.springframework.data.repository.query.RepositoryQuery; import org.springframework.data.util.TypeInformation; import org.springframework.util.Assert; +import com.mongodb.WriteResult; + /** * Base class for {@link RepositoryQuery} implementations for Mongo. * * @author Oliver Gierke * @author Thomas Darimont + * @author Christoph Strobl */ public abstract class AbstractMongoQuery implements RepositoryQuery { @@ -84,7 +87,9 @@ public abstract class AbstractMongoQuery implements RepositoryQuery { 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); Query countQuery = createCountQuery(new ConvertingParameterAccessor(operations.getConverter(), countAccessor)); @@ -142,6 +147,14 @@ public abstract class AbstractMongoQuery implements RepositoryQuery { */ protected abstract boolean isCountQuery(); + /** + * Return weather the query should delete matching documents. + * + * @return + * @since 1.5 + */ + protected abstract boolean isDeleteQuery(); + private abstract class Execution { abstract Object execute(Query query); @@ -351,4 +364,38 @@ public abstract class AbstractMongoQuery implements RepositoryQuery { 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()); + } + } + } 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 eb34feb3d..ebeb5350e 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 @@ -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"); * 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. * * @author Oliver Gierke + * @author Christoph Strobl */ public class PartTreeMongoQuery extends AbstractMongoQuery { @@ -86,4 +87,13 @@ public class PartTreeMongoQuery extends AbstractMongoQuery { protected boolean isCountQuery() { return tree.isCountProjection(); } + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.repository.query.AbstractMongoQuery#isDeleteQuery() + */ + @Override + protected boolean isDeleteQuery() { + return tree.isDelete(); + } } 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 20b8d0c0f..622e02ee5 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-2013 the original author or authors. + * Copyright 2011-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. @@ -30,6 +30,7 @@ import com.mongodb.util.JSON; * Query to use a plain JSON String to create the {@link Query} to actually execute. * * @author Oliver Gierke + * @author Christoph Strobl */ public class StringBasedMongoQuery extends AbstractMongoQuery { @@ -39,6 +40,7 @@ public class StringBasedMongoQuery extends AbstractMongoQuery { private final String query; private final String fieldSpec; private final boolean isCountQuery; + private final boolean isDeleteQuery; /** * Creates a new {@link StringBasedMongoQuery}. @@ -53,6 +55,7 @@ public class StringBasedMongoQuery extends AbstractMongoQuery { this.query = query; this.fieldSpec = method.getFieldSpecification(); this.isCountQuery = method.hasAnnotatedQuery() ? method.getQueryAnnotation().count() : false; + this.isDeleteQuery = method.hasAnnotatedQuery() ? method.getQueryAnnotation().delete() : false; } public StringBasedMongoQuery(MongoQueryMethod method, MongoOperations mongoOperations) { @@ -95,6 +98,11 @@ public class StringBasedMongoQuery extends AbstractMongoQuery { return isCountQuery; } + @Override + protected boolean isDeleteQuery() { + return this.isDeleteQuery; + } + private String replacePlaceholders(String input, ConvertingParameterAccessor accessor) { Matcher matcher = PLACEHOLDER.matcher(input); diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateTests.java index e30f0fa16..5cfcb52c0 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateTests.java +++ b/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)); } + /** + * @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 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 { @Id public String id; diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateUnitTests.java index 1947cec84..6bd058403 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateUnitTests.java +++ b/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.index.MongoPersistentEntityIndexCreator; 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.Update; import org.springframework.test.util.ReflectionTestUtils; @@ -56,6 +57,7 @@ import org.springframework.test.util.ReflectionTestUtils; import com.mongodb.BasicDBObject; import com.mongodb.DB; import com.mongodb.DBCollection; +import com.mongodb.DBCursor; import com.mongodb.DBObject; import com.mongodb.Mongo; import com.mongodb.MongoException; @@ -75,6 +77,7 @@ public class MongoTemplateUnitTests extends MongoOperationsUnitTests { @Mock Mongo mongo; @Mock DB db; @Mock DBCollection collection; + @Mock DBCursor cursor; MongoExceptionTranslator exceptionTranslator = new MongoExceptionTranslator(); MappingMongoConverter converter; @@ -86,11 +89,14 @@ public class MongoTemplateUnitTests extends MongoOperationsUnitTests { when(factory.getDb()).thenReturn(db); when(factory.getExceptionTranslator()).thenReturn(exceptionTranslator); 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.converter = new MappingMongoConverter(new DefaultDbRefResolver(factory), mappingContext); this.template = new MongoTemplate(factory, converter); - } @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 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 { @Id BigInteger id; 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 2238d9ca4..5c480d769 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 @@ -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.Point; 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.test.context.junit4.SpringJUnit4ClassRunner; @@ -763,8 +764,7 @@ public abstract class AbstractPersonRepositoryIntegrationTests { assertThat(result, is(arrayWithSize(1))); assertThat(result, is(arrayContaining(leroi))); } - - + /** * @see DATAMONGO-821 */ @@ -784,4 +784,79 @@ public abstract class AbstractPersonRepositoryIntegrationTests { assertThat(result.getNumberOfElements(), is(1)); assertThat(result.getContent().get(0), is(alicia)); } + + /** + * @see DATAMONGO-566 + */ + @Test + public void deleteByShouldReturnListOfDeletedElementsWhenRetunTypeIsCollectionLike() { + + List 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 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)); + } + } 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 5ced733d0..b8827b959 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 @@ -257,10 +257,33 @@ public interface PersonRepository extends MongoRepository, Query * @see DATAMONGO-870 */ Slice findByAgeGreaterThan(int age, Pageable pageable); - + /** * @see DATAMONGO-821 */ @Query("{ creator : { $exists : true } }") Page findByHavingCreator(Pageable page); + + /** + * @see DATAMONGO-566 + */ + List deleteByLastname(String lastname); + + /** + * @see DATAMONGO-566 + */ + Long deletePersonByLastname(String lastname); + + /** + * @see DATAMONGO-566 + */ + @Query(value = "{ 'lastname' : ?0 }", delete = true) + List removeByLastnameUsingAnnotatedQuery(String lastname); + + /** + * @see DATAMONGO-566 + */ + @Query(value = "{ 'lastname' : ?0 }", delete = true) + Long removePersonByLastnameUsingAnnotatedQuery(String lastname); + } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/AbstracMongoQueryUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/AbstracMongoQueryUnitTests.java new file mode 100644 index 000000000..a9d86f927 --- /dev/null +++ b/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. 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. 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 { + + List deleteByLastname(String lastname); + + Long deletePersonByLastname(String lastname); + } + +} diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/MongoQueryCreatorUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/MongoQueryCreatorUnitTests.java index 0cce34daf..b87624570 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/MongoQueryCreatorUnitTests.java +++ b/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"); * 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 Thomas Darimont + * @author Christoph Strobl */ @RunWith(MockitoJUnitRunner.class) public class MongoQueryCreatorUnitTests { @@ -407,6 +408,36 @@ public class MongoQueryCreatorUnitTests { 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 { List findByLocationNearAndFirstname(Point location, Distance maxDistance, String firstname); 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 0fa679064..3f9d6f198 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 @@ -140,6 +140,16 @@ public class StringBasedMongoQueryUnitTests { 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 { Method method = SampleRepository.class.getMethod(name, parameters); @@ -161,5 +171,8 @@ public class StringBasedMongoQueryUnitTests { @Query("{ fans : { $not : { $size : 0 } } }") Person findByHavingSizeFansNotZero(); + @Query(value = "{ 'lastname' : ?0 }", delete = true) + void removeByLastname(String lastname); + } } diff --git a/src/docbkx/reference/mongo-repositories.xml b/src/docbkx/reference/mongo-repositories.xml index 9ca801feb..df8a1a58a 100644 --- a/src/docbkx/reference/mongo-repositories.xml +++ b/src/docbkx/reference/mongo-repositories.xml @@ -394,6 +394,29 @@ public class PersonRepositoryTests { +
+ Repository delete queries + + The above keywords can be used in conjunciton with + delete…By or remove…By to create queries + deleting matching documents. + + + <code>Delete…By</code> Query + + public interface PersonRepository extends MongoRepository<Person, String> { + List <Person> deleteByLastname(String lastname); + + Long deletePersonByLastname(String lastname); +} + + + Using return type List 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. +
+
Geo-spatial repository queries