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 7ff775bb44..23d9b073d1 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 @@ -15,7 +15,7 @@ */ package org.springframework.data.mongodb.core; -import static org.springframework.data.mongodb.core.query.Criteria.*; +import static org.springframework.data.mongodb.core.query.Criteria.where; import java.io.IOException; import java.lang.reflect.InvocationTargetException; @@ -101,11 +101,12 @@ /** * Primary implementation of {@link MongoOperations}. - * + * * @author Thomas Risberg * @author Graeme Rocher * @author Mark Pollack * @author Oliver Gierke + * @author Amol Nayak */ public class MongoTemplate implements MongoOperations, ApplicationContextAware { @@ -153,7 +154,7 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware { /** * Constructor used for a basic template configuration - * + * * @param mongo * @param databaseName */ @@ -164,7 +165,7 @@ public MongoTemplate(Mongo mongo, String databaseName) { /** * Constructor used for a template configuration with user credentials in the form of * {@link org.springframework.data.authentication.UserCredentials} - * + * * @param mongo * @param databaseName * @param userCredentials @@ -175,7 +176,7 @@ public MongoTemplate(Mongo mongo, String databaseName, UserCredentials userCrede /** * Constructor used for a basic template configuration - * + * * @param mongoDbFactory */ public MongoTemplate(MongoDbFactory mongoDbFactory) { @@ -184,7 +185,7 @@ public MongoTemplate(MongoDbFactory mongoDbFactory) { /** * Constructor used for a basic template configuration. - * + * * @param mongoDbFactory * @param mongoConverter */ @@ -212,7 +213,7 @@ public MongoTemplate(MongoDbFactory mongoDbFactory, MongoConverter mongoConverte /** * Configures the {@link WriteResultChecking} to be used with the template. Setting {@literal null} will reset the * default of {@value #DEFAULT_WRITE_RESULT_CHECKING}. - * + * * @param resultChecking */ public void setWriteResultChecking(WriteResultChecking resultChecking) { @@ -221,7 +222,7 @@ public void setWriteResultChecking(WriteResultChecking resultChecking) { /** * Configures the {@link WriteConcern} to be used with the template. - * + * * @param writeConcern */ public void setWriteConcern(WriteConcern writeConcern) { @@ -230,7 +231,7 @@ public void setWriteConcern(WriteConcern writeConcern) { /** * Configures the {@link WriteConcernResolver} to be used with the template. - * + * * @param writeConcernResolver */ public void setWriteConcernResolver(WriteConcernResolver writeConcernResolver) { @@ -240,7 +241,7 @@ public void setWriteConcernResolver(WriteConcernResolver writeConcernResolver) { /** * Used by @{link {@link #prepareCollection(DBCollection)} to set the {@link ReadPreference} before any operations are * performed. - * + * * @param readPreference */ public void setReadPreference(ReadPreference readPreference) { @@ -261,7 +262,7 @@ public void setApplicationContext(ApplicationContext applicationContext) throws /** * Returns the default {@link org.springframework.data.mongodb.core.core.convert.MongoConverter}. - * + * * @return */ public MongoConverter getConverter() { @@ -318,7 +319,7 @@ public void executeQuery(Query query, String collectionName, DocumentCallbackHan /** * Execute a MongoDB query and iterate over the query results on a per-document basis with a * {@link DocumentCallbackHandler} using the provided CursorPreparer. - * + * * @param query the query class that specifies the criteria used to find a record and also an optional fields * specification, must not be {@literal null}. * @param collectionName name of the collection to retrieve the objects from @@ -598,7 +599,7 @@ protected void ensureNotIterable(Object o) { /** * Prepare the collection before any processing is done using it. This allows a convenient way to apply settings like * slaveOk() etc. Can be overridden in sub-classes. - * + * * @param collection */ protected void prepareCollection(DBCollection collection) { @@ -610,7 +611,7 @@ protected void prepareCollection(DBCollection collection) { /** * Prepare the WriteConcern before any processing is done using it. This allows a convenient way to apply custom * settings in sub-classes. - * + * * @param writeConcern any WriteConcern already configured or null * @return The prepared WriteConcern or null */ @@ -730,11 +731,13 @@ public Object doInCollection(DBCollection collection) throws MongoException, Dat MongoAction mongoAction = new MongoAction(writeConcern, MongoActionOperation.INSERT, collectionName, entityClass, dbDoc, null); WriteConcern writeConcernToUse = prepareWriteConcern(mongoAction); + WriteResult wr; if (writeConcernToUse == null) { - collection.insert(dbDoc); + wr = collection.insert(dbDoc); } else { - collection.insert(dbDoc, writeConcernToUse); + wr = collection.insert(dbDoc, writeConcernToUse); } + handleAnyWriteResultErrors(wr, dbDoc, "insert"); return dbDoc.get(ID); } }); @@ -753,11 +756,13 @@ public Void doInCollection(DBCollection collection) throws MongoException, DataA MongoAction mongoAction = new MongoAction(writeConcern, MongoActionOperation.INSERT_LIST, collectionName, null, null, null); WriteConcern writeConcernToUse = prepareWriteConcern(mongoAction); + WriteResult wr; if (writeConcernToUse == null) { - collection.insert(dbDocList); + wr = collection.insert(dbDocList); } else { - collection.insert(dbDocList.toArray((DBObject[]) new BasicDBObject[dbDocList.size()]), writeConcernToUse); + wr = collection.insert(dbDocList.toArray((DBObject[]) new BasicDBObject[dbDocList.size()]), writeConcernToUse); } + handleAnyWriteResultErrors(wr, null, "insert_list"); return null; } }); @@ -784,11 +789,13 @@ public Object doInCollection(DBCollection collection) throws MongoException, Dat MongoAction mongoAction = new MongoAction(writeConcern, MongoActionOperation.SAVE, collectionName, entityClass, dbDoc, null); WriteConcern writeConcernToUse = prepareWriteConcern(mongoAction); + WriteResult wr; if (writeConcernToUse == null) { - collection.save(dbDoc); + wr = collection.save(dbDoc); } else { - collection.save(dbDoc, writeConcernToUse); + wr = collection.save(dbDoc, writeConcernToUse); } + handleAnyWriteResultErrors(wr, dbDoc, "save"); return dbDoc.get(ID); } }); @@ -873,7 +880,7 @@ public void remove(Object object, String collection) { /** * Returns a {@link Query} for the given entity by its id. - * + * * @param object must not be {@literal null}. * @return */ @@ -1182,7 +1189,7 @@ protected void maybeEmitEvent(MongoMappingEvent event) { /** * Create the specified collection using the provided options - * + * * @param collectionName * @param collectionOptions * @return the collection that was created @@ -1204,7 +1211,7 @@ public DBCollection doInDB(DB db) throws MongoException, DataAccessException { * Map the results of an ad-hoc query on the default MongoDB collection to an object using the template's converter *

* The query document is specified as a standard DBObject and so is the fields specification. - * + * * @param collectionName name of the collection to retrieve the objects from * @param query the query document that specifies the criteria used to find a record * @param fields the document that specifies the fields to be returned @@ -1229,7 +1236,7 @@ protected T doFindOne(String collectionName, DBObject query, DBObject fields * The query document is specified as a standard DBObject and so is the fields specification. *

* Can be overridden by subclasses. - * + * * @param collectionName name of the collection to retrieve the objects from * @param query the query document that specifies the criteria used to find a record * @param fields the document that specifies the fields to be returned @@ -1259,7 +1266,7 @@ protected List doFind(String collectionName, DBObject query, DBObject * Map the results of an ad-hoc query on the default MongoDB collection to a List using the template's converter. *

* The query document is specified as a standard DBObject and so is the fields specification. - * + * * @param collectionName name of the collection to retrieve the objects from * @param query the query document that specifies the criteria used to find a record * @param fields the document that specifies the fields to be returned @@ -1298,7 +1305,7 @@ protected DBObject convertToDbObject(CollectionOptions collectionOptions) { * The first document that matches the query is returned and also removed from the collection in the database. *

* The query document is specified as a standard DBObject and so is the fields specification. - * + * * @param collectionName name of the collection to retrieve the objects from * @param query the query document that specifies the criteria used to find a record * @param entityClass the parameterized type of the returned list. @@ -1345,7 +1352,7 @@ protected T doFindAndModify(String collectionName, DBObject query, DBObject /** * Populates the id property of the saved object, if it's not set already. - * + * * @param savedObject * @param id */ @@ -1398,7 +1405,7 @@ private DBCollection getAndPrepareCollection(DB db, String collectionName) { *

  • Execute the given {@link ConnectionCallback} for a {@link DBObject}.
  • *
  • Apply the given {@link DbObjectCallback} to each of the {@link DBObject}s to obtain the result.
  • *
      - * + * * @param * @param collectionCallback the callback to retrieve the {@link DBObject} with * @param objectCallback the {@link DbObjectCallback} to transform {@link DBObject}s into the actual domain type @@ -1427,7 +1434,7 @@ private T executeFindOneInternal(CollectionCallback collectionCall *
    1. Iterate over the {@link DBCursor} and applies the given {@link DbObjectCallback} to each of the * {@link DBObject}s collecting the actual result {@link List}.
    2. *
        - * + * * @param * @param collectionCallback the callback to retrieve the {@link DBCursor} with * @param preparer the {@link CursorPreparer} to potentially modify the {@link DBCursor} before ireating over it @@ -1521,9 +1528,19 @@ protected void handleAnyWriteResultErrors(WriteResult wr, DBObject query, String String error = wr.getError(); if (error != null) { - - String message = String.format("Execution of %s%s failed: %s", operation, query == null ? "" : "' using '" + String message; + if(operation.equals("insert") || operation.equals("save")) { + //assuming the insert operations will begin with insert string + message = String.format("Insert/Save for %s failed: %s", query,error); + } + else if(operation.equals("insert_list")) { + message = String.format("Insert list failed: %s", error); + } + else { + message = String.format("Execution of %s%s failed: %s", operation, query == null ? "" : "' using '" + query.toString() + "' query", error); + } + if (WriteResultChecking.EXCEPTION == this.writeResultChecking) { throw new DataIntegrityViolationException(message); @@ -1537,7 +1554,7 @@ protected void handleAnyWriteResultErrors(WriteResult wr, DBObject query, String /** * Tries to convert the given {@link RuntimeException} into a {@link DataAccessException} but returns the original * exception if the conversation failed. Thus allows safe rethrowing of the return value. - * + * * @param ex * @return */ @@ -1557,7 +1574,7 @@ private static final MongoConverter getDefaultMongoConverter(MongoDbFactory fact /** * Simple {@link CollectionCallback} that takes a query {@link DBObject} plus an optional fields specification * {@link DBObject} and executes that against the {@link DBCollection}. - * + * * @author Oliver Gierke * @author Thomas Risberg */ @@ -1590,7 +1607,7 @@ public DBObject doInCollection(DBCollection collection) throws MongoException, D /** * Simple {@link CollectionCallback} that takes a query {@link DBObject} plus an optional fields specification * {@link DBObject} and executes that against the {@link DBCollection}. - * + * * @author Oliver Gierke * @author Thomas Risberg */ @@ -1621,7 +1638,7 @@ public DBCursor doInCollection(DBCollection collection) throws MongoException, D /** * Simple {@link CollectionCallback} that takes a query {@link DBObject} plus an optional fields specification * {@link DBObject} and executes that against the {@link DBCollection}. - * + * * @author Thomas Risberg */ private static class FindAndRemoveCallback implements CollectionCallback { @@ -1666,7 +1683,7 @@ public DBObject doInCollection(DBCollection collection) throws MongoException, D /** * Simple internal callback to allow operations on a {@link DBObject}. - * + * * @author Oliver Gierke */ @@ -1678,7 +1695,7 @@ private interface DbObjectCallback { /** * Simple {@link DbObjectCallback} that will transform {@link DBObject} into the given target type using the given * {@link MongoReader}. - * + * * @author Oliver Gierke */ private class ReadDbObjectCallback implements DbObjectCallback { @@ -1762,7 +1779,7 @@ public DBCursor prepare(DBCursor cursor) { /** * {@link DbObjectCallback} that assumes a {@link GeoResult} to be created, delegates actual content unmarshalling to * a delegate and creates a {@link GeoResult} from the result. - * + * * @author Oliver Gierke */ static class GeoNearResultDbObjectCallback implements DbObjectCallback> { @@ -1773,7 +1790,7 @@ static class GeoNearResultDbObjectCallback implements DbObjectCallback delegate, Metric metric) { 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 92fd4847a8..3487780b38 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 @@ -1,5 +1,5 @@ /* - * Copyright 2011 the original author or authors. + * Copyright 2012 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. @@ -15,11 +15,17 @@ */ package org.springframework.data.mongodb.core; -import static org.hamcrest.Matchers.*; -import static org.junit.Assert.*; -import static org.springframework.data.mongodb.core.query.Criteria.*; -import static org.springframework.data.mongodb.core.query.Query.*; -import static org.springframework.data.mongodb.core.query.Update.*; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.hasItem; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.isOneOf; +import static org.hamcrest.Matchers.notNullValue; +import static org.hamcrest.Matchers.nullValue; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThat; +import static org.springframework.data.mongodb.core.query.Criteria.where; +import static org.springframework.data.mongodb.core.query.Query.query; +import static org.springframework.data.mongodb.core.query.Update.update; import java.util.ArrayList; import java.util.Arrays; @@ -37,9 +43,9 @@ import org.junit.rules.ExpectedException; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.context.ApplicationContext; import org.springframework.core.convert.converter.Converter; import org.springframework.dao.DataAccessException; +import org.springframework.dao.DataIntegrityViolationException; import org.springframework.data.annotation.Id; import org.springframework.data.annotation.PersistenceConstructor; import org.springframework.data.mongodb.InvalidMongoDbApiUsageException; @@ -70,9 +76,10 @@ /** * Integration test for {@link MongoTemplate}. - * + * * @author Oliver Gierke * @author Thomas Risberg + * @author Amol Nayak */ @RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration("classpath:infrastructure.xml") @@ -163,6 +170,103 @@ public void bogusUpdateDoesNotTriggerException() throws Exception { mongoTemplate.updateFirst(q, u, Person.class); } + /** + * The method to test the exception that is thrown if we insert a DB object with the same object + * id twice. + * + */ + @Test + public void insertDuplicateIds() { + MongoTemplate template = new MongoTemplate(factory); + template.setWriteResultChecking(WriteResultChecking.EXCEPTION); + Person person = new Person(new ObjectId(),"Amol"); + person.setAge(28); + template.insert(person); + try { + template.insert(person); + } catch (DataIntegrityViolationException e) { + Assert.assertTrue(e.getMessage() + .indexOf("E11000 duplicate key error index: " + + "database.person.$_id_ dup key:") > 0); + return; + } + Assert.assertTrue("Should have caught DataIntegrityViolationException", false); + } + + + /** + * The method is used to test the error message on update when we try to push to + * an element that is not an array + */ + @Test + public void updateWithIncorrectPushOperator() { + MongoTemplate template = new MongoTemplate(factory); + template.setWriteResultChecking(WriteResultChecking.EXCEPTION); + ObjectId id = new ObjectId(); + Person person = new Person(id,"Amol"); + person.setAge(28); + template.insert(person); + try { + Query query = new Query(Criteria.where("firstName").is("Amol")); + Update upd = new Update().push("age", 29); + template.updateFirst(query, upd, Person.class); + } catch (DataIntegrityViolationException e) { + Assert.assertEquals("Execution of update with '{ \"$push\" : { \"age\" : 29}}'' using '{ \"firstName\" : \"Amol\"}' " + + "query failed: Cannot apply $push/$pushAll modifier to non-array",e.getMessage()); + return; + } + Assert.assertTrue("Should have caught DataIntegrityViolationException", false); + } + + /** + * This method attempts to violate a unique index on the person name (well not likely + * but I have assumed that for the junit purpose). The WriteResultChecking is set to EXCEPTION + * so we should expect to see an exception with two person records with same name are attempted to + * be inserted + */ + @Test + public void saveAndViolateAUniqueIndex() { + MongoTemplate template = new MongoTemplate(factory); + template.setWriteResultChecking(WriteResultChecking.EXCEPTION); + template.indexOps(Person.class).ensureIndex(new Index().on("firstName", Order.DESCENDING).unique()); + Person person = new Person(new ObjectId(),"Amol"); + person.setAge(28); + template.save(person); + person = new Person(new ObjectId(),"Amol"); + person.setAge(28); + try { + template.save(person); + } catch (DataIntegrityViolationException e) { + Assert.assertTrue(e.getMessage().indexOf("E11000 duplicate key error index: database.person.$firstName_-1 dup key:") > 0); + return; + } + Assert.assertTrue("Should have caught DataIntegrityViolationException", false); + } + + /** + * + */ + @Test + public void duplicateObjectIdInsertInInsertList() { + MongoTemplate template = new MongoTemplate(factory); + template.setWriteResultChecking(WriteResultChecking.EXCEPTION); + ObjectId id = new ObjectId(); + Person person = new Person(id,"Amol"); + person.setAge(28); + List records = new ArrayList(); + records.add(person); + records.add(person); + try { + template.insertAll(records); + } catch (DataIntegrityViolationException e) { + Assert.assertTrue(e.getMessage().startsWith("Insert list failed: E11000 duplicate key error index: " + + "database.person.$_id_ dup key: { : ObjectId")); + return; + } + Assert.assertTrue("Should have caught DataIntegrityViolationException", false); + + } + @Test public void testEnsureIndex() throws Exception { @@ -939,7 +1043,7 @@ public void updatesDBRefsCorrectly() { DBRef first = new DBRef(factory.getDb(), "foo", new ObjectId()); DBRef second = new DBRef(factory.getDb(), "bar", new ObjectId()); - template.updateFirst(null, Update.update("dbRefs", Arrays.asList(first, second)), ClassWithDBRefs.class); + template.updateFirst(null, update("dbRefs", Arrays.asList(first, second)), ClassWithDBRefs.class); } class ClassWithDBRefs {