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.
This commit is contained in:
committed by
Oliver Gierke
parent
70e5efd0d9
commit
ba48290a3e
@@ -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
|
||||
*/
|
||||
<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}.
|
||||
|
||||
@@ -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,24 +1050,48 @@ 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 {@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
|
||||
* @return
|
||||
*/
|
||||
private Map.Entry<MongoPersistentProperty, Object> extractIdPropertyAndValue(Object object) {
|
||||
|
||||
Assert.notNull(object, "Id cannot be extracted from 'null'.");
|
||||
|
||||
Class<?> objectType = object.getClass();
|
||||
MongoPersistentEntity<?> entity = mappingContext.getPersistentEntity(objectType);
|
||||
MongoPersistentProperty idProp = entity == null ? null : entity.getIdProperty();
|
||||
|
||||
if (idProp == null) {
|
||||
throw new MappingException("No id property found for object of type " + objectType);
|
||||
}
|
||||
|
||||
Object idValue = BeanWrapper.create(object, mongoConverter.getConversionService())
|
||||
.getProperty(idProp, Object.class);
|
||||
return Collections.singletonMap(idProp, idValue).entrySet().iterator().next();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1075,21 +1102,31 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware {
|
||||
*/
|
||||
private Query getIdQueryFor(Object object) {
|
||||
|
||||
Assert.notNull(object);
|
||||
Map.Entry<MongoPersistentProperty, Object> id = extractIdPropertyAndValue(object);
|
||||
return new Query(where(id.getKey().getFieldName()).is(id.getValue()));
|
||||
}
|
||||
|
||||
Class<?> objectType = object.getClass();
|
||||
MongoPersistentEntity<?> entity = mappingContext.getPersistentEntity(objectType);
|
||||
MongoPersistentProperty idProp = entity == null ? null : entity.getIdProperty();
|
||||
/**
|
||||
* 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) {
|
||||
|
||||
if (idProp == null) {
|
||||
throw new MappingException("No id property found for object of type " + objectType);
|
||||
Assert.notEmpty(objects, "Cannot create Query for empty collection.");
|
||||
|
||||
Iterator<?> it = objects.iterator();
|
||||
Map.Entry<MongoPersistentProperty, Object> firstEntry = extractIdPropertyAndValue(it.next());
|
||||
|
||||
ArrayList<Object> ids = new ArrayList<Object>(objects.size());
|
||||
ids.add(firstEntry.getValue());
|
||||
|
||||
while (it.hasNext()) {
|
||||
ids.add(extractIdPropertyAndValue(it.next()).getValue());
|
||||
}
|
||||
|
||||
ConversionService service = mongoConverter.getConversionService();
|
||||
Object idProperty = null;
|
||||
|
||||
idProperty = BeanWrapper.create(object, service).getProperty(idProp, Object.class);
|
||||
return new Query(where(idProp.getFieldName()).is(idProperty));
|
||||
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 <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) {
|
||||
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<Void>() {
|
||||
public Void doInCollection(DBCollection collection) throws MongoException, DataAccessException {
|
||||
return execute(collectionName, new CollectionCallback<WriteResult>() {
|
||||
public WriteResult doInCollection(DBCollection collection) throws MongoException, DataAccessException {
|
||||
|
||||
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,
|
||||
writeConcernToUse);
|
||||
|
||||
handleAnyWriteResultErrors(wr, dboq, MongoActionOperation.REMOVE);
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
/*
|
||||
* (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,
|
||||
AggregationOperationContext context) {
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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<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 {
|
||||
|
||||
@Id public String id;
|
||||
|
||||
@@ -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<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 {
|
||||
|
||||
@Id BigInteger id;
|
||||
|
||||
@@ -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<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));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -257,10 +257,33 @@ public interface PersonRepository extends MongoRepository<Person, String>, Query
|
||||
* @see DATAMONGO-870
|
||||
*/
|
||||
Slice<Person> findByAgeGreaterThan(int age, Pageable pageable);
|
||||
|
||||
|
||||
/**
|
||||
* @see DATAMONGO-821
|
||||
*/
|
||||
@Query("{ creator : { $exists : true } }")
|
||||
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);
|
||||
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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<Person, Long> {
|
||||
|
||||
List<Person> findByLocationNearAndFirstname(Point location, Distance maxDistance, String firstname);
|
||||
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -394,6 +394,29 @@ public class PersonRepositoryTests {
|
||||
</tgroup>
|
||||
</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<Person, String> {
|
||||
List <Person> 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">
|
||||
<title>Geo-spatial repository queries</title>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user