From ba48290a3ee43dd79e20ba1a37eeef676759063a Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Mon, 10 Mar 2014 10:24:45 +0100 Subject: [PATCH] DATAMONGO-566 - Add support for derived delete-by queries. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- .../data/mongodb/core/MongoOperations.java | 44 +++- .../data/mongodb/core/MongoTemplate.java | 134 +++++++++--- .../data/mongodb/repository/Query.java | 11 +- .../repository/query/AbstractMongoQuery.java | 49 ++++- .../repository/query/PartTreeMongoQuery.java | 12 +- .../query/StringBasedMongoQuery.java | 10 +- .../data/mongodb/core/MongoTemplateTests.java | 23 ++ .../mongodb/core/MongoTemplateUnitTests.java | 46 +++- ...tractPersonRepositoryIntegrationTests.java | 79 ++++++- .../mongodb/repository/PersonRepository.java | 25 ++- .../query/AbstracMongoQueryUnitTests.java | 202 ++++++++++++++++++ .../query/MongoQueryCreatorUnitTests.java | 33 ++- .../query/StringBasedMongoQueryUnitTests.java | 13 ++ src/docbkx/reference/mongo-repositories.xml | 23 ++ 14 files changed, 666 insertions(+), 38 deletions(-) create mode 100644 spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/AbstracMongoQueryUnitTests.java 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