Saving, Updating, and Removing Documents
MongoTemplate lets you save, update, and delete your domain objects and map those objects to documents stored in MongoDB.
Consider the following class:
public class Person {
private String id;
private String name;
private int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
public String getId() {
return id;
}
public String getName() {
return name;
}
public int getAge() {
return age;
}
@Override
public String toString() {
return "Person [id=" + id + ", name=" + name + ", age=" + age + "]";
}
}
Given the Person class in the preceding example, you can save, update and delete the object, as the following example shows:
MongoOperations is the interface that MongoTemplate implements.
|
package org.spring.example;
import static org.springframework.data.mongodb.core.query.Criteria.where;
import static org.springframework.data.mongodb.core.query.Update.update;
import static org.springframework.data.mongodb.core.query.Query.query;
import java.util.List;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.data.mongodb.core.MongoOperations;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.core.SimpleMongoDbFactory;
import com.mongodb.MongoClient;
public class MongoApp {
private static final Log log = LogFactory.getLog(MongoApp.class);
public static void main(String[] args) {
MongoOperations mongoOps = new MongoTemplate(new SimpleMongoDbFactory(new MongoClient(), "database"));
Person p = new Person("Joe", 34);
// Insert is used to initially store the object into the database.
mongoOps.insert(p);
log.info("Insert: " + p);
// Find
p = mongoOps.findById(p.getId(), Person.class);
log.info("Found: " + p);
// Update
mongoOps.updateFirst(query(where("name").is("Joe")), update("age", 35), Person.class);
p = mongoOps.findOne(query(where("name").is("Joe")), Person.class);
log.info("Updated: " + p);
// Delete
mongoOps.remove(p);
// Check that deletion worked
List<Person> people = mongoOps.findAll(Person.class);
log.info("Number of people = : " + people.size());
mongoOps.dropCollection(Person.class);
}
}
The preceding example would produce the following log output (including debug messages from MongoTemplate):
DEBUG apping.MongoPersistentEntityIndexCreator: 80 - Analyzing class class org.spring.example.Person for index information.
DEBUG work.data.mongodb.core.MongoTemplate: 632 - insert Document containing fields: [_class, age, name] in collection: person
INFO org.spring.example.MongoApp: 30 - Insert: Person [id=4ddc6e784ce5b1eba3ceaf5c, name=Joe, age=34]
DEBUG work.data.mongodb.core.MongoTemplate:1246 - findOne using query: { "_id" : { "$oid" : "4ddc6e784ce5b1eba3ceaf5c"}} in db.collection: database.person
INFO org.spring.example.MongoApp: 34 - Found: Person [id=4ddc6e784ce5b1eba3ceaf5c, name=Joe, age=34]
DEBUG work.data.mongodb.core.MongoTemplate: 778 - calling update using query: { "name" : "Joe"} and update: { "$set" : { "age" : 35}} in collection: person
DEBUG work.data.mongodb.core.MongoTemplate:1246 - findOne using query: { "name" : "Joe"} in db.collection: database.person
INFO org.spring.example.MongoApp: 39 - Updated: Person [id=4ddc6e784ce5b1eba3ceaf5c, name=Joe, age=35]
DEBUG work.data.mongodb.core.MongoTemplate: 823 - remove using query: { "id" : "4ddc6e784ce5b1eba3ceaf5c"} in collection: person
INFO org.spring.example.MongoApp: 46 - Number of people = : 0
DEBUG work.data.mongodb.core.MongoTemplate: 376 - Dropped collection [database.person]
MongoConverter caused implicit conversion between a String and an ObjectId stored in the database by recognizing (through convention) the Id property name.
The preceding example is meant to show the use of save, update, and remove operations on MongoTemplate and not to show complex mapping functionality.
|
The query syntax used in the preceding example is explained in more detail in the section “mongo.query”.
How the _id Field is Handled in the Mapping Layer
MongoDB requires that you have an _id field for all documents. If you do not provide one, the driver assigns an ObjectId with a generated value. When you use the MappingMongoConverter, certain rules govern how properties from the Java class are mapped to this _id field:
-
A property or field annotated with
@Id(org.springframework.data.annotation.Id) maps to the_idfield. -
A property or field without an annotation but named
idmaps to the_idfield.
The following outlines what type conversion, if any, is done on the property mapped to the _id document field when using the MappingMongoConverter (the default for MongoTemplate).
-
If possible, an
idproperty or field declared as aStringin the Java class is converted to and stored as anObjectIdby using a SpringConverter<String, ObjectId>. Valid conversion rules are delegated to the MongoDB Java driver. If it cannot be converted to anObjectId, then the value is stored as a string in the database. -
An
idproperty or field declared asBigIntegerin the Java class is converted to and stored as anObjectIdby using a SpringConverter<BigInteger, ObjectId>.
If no field or property specified in the previous sets of rules is present in the Java class, an implicit _id file is generated by the driver but not mapped to a property or field of the Java class.
When querying and updating, MongoTemplate uses the converter that corresponds to the preceding rules for saving documents so that field names and types used in your queries can match what is in your domain classes.
Type Mapping
MongoDB collections can contain documents that represent instances of a variety of types. This feature can be useful if you store a hierarchy of classes or have a class with a property of type Object. In the latter case, the values held inside that property have to be read in correctly when retrieving the object. Thus, we need a mechanism to store type information alongside the actual document.
To achieve that, the MappingMongoConverter uses a MongoTypeMapper abstraction with DefaultMongoTypeMapper as its main implementation. Its default behavior to store the fully qualified classname under _class inside the document. Type hints are written for top-level documents as well as for every value (if it is a complex type and a subtype of the declared property type). The following example (with a JSON representation at the end) shows how the mapping works:
public class Sample {
Contact value;
}
public abstract class Contact { … }
public class Person extends Contact { … }
Sample sample = new Sample();
sample.value = new Person();
mongoTemplate.save(sample);
{
"value" : { "_class" : "com.acme.Person" },
"_class" : "com.acme.Sample"
}
Spring Data MongoDB stores the type information as the last field for the actual root class as well as for the nested type (because it is complex and a subtype of Contact). So, if you now use mongoTemplate.findAll(Object.class, "sample"), you can find out that the document stored is a Sample instance. You can also find out that the value property is actually a Person.
Customizing Type Mapping
If you want to avoid writing the entire Java class name as type information but would rather like to use a key, you can use the @TypeAlias annotation on the entity class. If you need to customize the mapping even more, have a look at the TypeInformationMapper interface. An instance of that interface can be configured at the DefaultMongoTypeMapper, which can, in turn, be configured on MappingMongoConverter. The following example shows how to define a type alias for an entity:
@TypeAlias("pers")
class Person {
}
Note that the resulting document contains pers as the value in the _class Field.
Configuring Custom Type Mapping
The following example shows how to configure a custom MongoTypeMapper in MappingMongoConverter:
MongoTypeMapper with Spring Java Configclass CustomMongoTypeMapper extends DefaultMongoTypeMapper {
//implement custom type mapping here
}
@Configuration
class SampleMongoConfiguration extends AbstractMongoConfiguration {
@Override
protected String getDatabaseName() {
return "database";
}
@Override
public MongoClient mongoClient() {
return new MongoClient();
}
@Bean
@Override
public MappingMongoConverter mappingMongoConverter() throws Exception {
MappingMongoConverter mmc = super.mappingMongoConverter();
mmc.setTypeMapper(customTypeMapper());
return mmc;
}
@Bean
public MongoTypeMapper customTypeMapper() {
return new CustomMongoTypeMapper();
}
}
Note that the preceding example extends the AbstractMongoConfiguration class and overrides the bean definition of the MappingMongoConverter where we configured our custom MongoTypeMapper.
The following example shows how to use XML to configure a custom MongoTypeMapper:
MongoTypeMapper with XML<mongo:mapping-converter type-mapper-ref="customMongoTypeMapper"/>
<bean name="customMongoTypeMapper" class="com.bubu.mongo.CustomMongoTypeMapper"/>
Methods for Saving and Inserting Documents
There are several convenient methods on MongoTemplate for saving and inserting your objects. To have more fine-grained control over the conversion process, you can register Spring converters with the MappingMongoConverter — for example Converter<Person, Document> and Converter<Document, Person>.
| The difference between insert and save operations is that a save operation performs an insert if the object is not already present. |
The simple case of using the save operation is to save a POJO. In this case, the collection name is determined by name (not fully qualified) of the class. You may also call the save operation with a specific collection name. You can use mapping metadata to override the collection in which to store the object.
When inserting or saving, if the Id property is not set, the assumption is that its value will be auto-generated by the database. Consequently, for auto-generation of an ObjectId to succeed, the type of the Id property or field in your class must be a String, an ObjectId, or a BigInteger.
The following example shows how to save a document and retrieving its contents:
import static org.springframework.data.mongodb.core.query.Criteria.where;
import static org.springframework.data.mongodb.core.query.Criteria.query;
…
Person p = new Person("Bob", 33);
mongoTemplate.insert(p);
Person qp = mongoTemplate.findOne(query(where("age").is(33)), Person.class);
The following insert and save operations are available:
-
voidsave(Object objectToSave): Save the object to the default collection. -
voidsave(Object objectToSave, String collectionName): Save the object to the specified collection.
A similar set of insert operations is also available:
-
voidinsert(Object objectToSave): Insert the object to the default collection. -
voidinsert(Object objectToSave, String collectionName): Insert the object to the specified collection.
Into Which Collection Are My Documents Saved?
There are two ways to manage the collection name that is used for the documents. The default collection name that is used is the class name changed to start with a lower-case letter. So a com.test.Person class is stored in the person collection. You can customize this by providing a different collection name with the @Document annotation. You can also override the collection name by providing your own collection name as the last parameter for the selected MongoTemplate method calls.
Inserting or Saving Individual Objects
The MongoDB driver supports inserting a collection of documents in a single operation. The following methods in the MongoOperations interface support this functionality:
-
insert: Inserts an object. If there is an existing document with the same
id, an error is generated. -
insertAll: Takes a
Collectionof objects as the first parameter. This method inspects each object and inserts it into the appropriate collection, based on the rules specified earlier. -
save: Saves the object, overwriting any object that might have the same
id.
Inserting Several Objects in a Batch
The MongoDB driver supports inserting a collection of documents in one operation. The following methods in the MongoOperations interface support this functionality:
-
insert methods: Take a
Collectionas the first argument. They insert a list of objects in a single batch write to the database.
Updating Documents in a Collection
For updates, you can update the first document found by using MongoOperation.updateFirst or you can update all documents that were found to match the query by using the MongoOperation.updateMulti method. The following example shows an update of all SAVINGS accounts where we are adding a one-time $50.00 bonus to the balance by using the $inc operator:
MongoTemplateimport static org.springframework.data.mongodb.core.query.Criteria.where;
import static org.springframework.data.mongodb.core.query.Query;
import static org.springframework.data.mongodb.core.query.Update;
...
WriteResult wr = mongoTemplate.updateMulti(new Query(where("accounts.accountType").is(Account.Type.SAVINGS)),
new Update().inc("accounts.$.balance", 50.00), Account.class);
In addition to the Query discussed earlier, we provide the update definition by using an Update object. The Update class has methods that match the update modifiers available for MongoDB.
Most methods return the Update object to provide a fluent style for the API.
Methods for Executing Updates for Documents
-
updateFirst: Updates the first document that matches the query document criteria with the updated document.
-
updateMulti: Updates all objects that match the query document criteria with the updated document.
Methods in the Update Class
You can use a little "'syntax sugar'" with the Update class, as its methods are meant to be chained together. Also, you can kick-start the creation of a new Update instance by using public static Update update(String key, Object value) and using static imports.
The Update class contains the following methods:
-
UpdateaddToSet(String key, Object value)Update using the$addToSetupdate modifier -
UpdatecurrentDate(String key)Update using the$currentDateupdate modifier -
UpdatecurrentTimestamp(String key)Update using the$currentDateupdate modifier with$typetimestamp -
Updateinc(String key, Number inc)Update using the$incupdate modifier -
Updatemax(String key, Object max)Update using the$maxupdate modifier -
Updatemin(String key, Object min)Update using the$minupdate modifier -
Updatemultiply(String key, Number multiplier)Update using the$mulupdate modifier -
Updatepop(String key, Update.Position pos)Update using the$popupdate modifier -
Updatepull(String key, Object value)Update using the$pullupdate modifier -
UpdatepullAll(String key, Object[] values)Update using the$pullAllupdate modifier -
Updatepush(String key, Object value)Update using the$pushupdate modifier -
UpdatepushAll(String key, Object[] values)Update using the$pushAllupdate modifier -
Updaterename(String oldName, String newName)Update using the$renameupdate modifier -
Updateset(String key, Object value)Update using the$setupdate modifier -
UpdatesetOnInsert(String key, Object value)Update using the$setOnInsertupdate modifier -
Updateunset(String key)Update using the$unsetupdate modifier
Some update modifiers, such as $push and $addToSet, allow nesting of additional operators.
// { $push : { "category" : { "$each" : [ "spring" , "data" ] } } }
new Update().push("category").each("spring", "data")
// { $push : { "key" : { "$position" : 0 , "$each" : [ "Arya" , "Arry" , "Weasel" ] } } }
new Update().push("key").atPosition(Position.FIRST).each(Arrays.asList("Arya", "Arry", "Weasel"));
// { $push : { "key" : { "$slice" : 5 , "$each" : [ "Arya" , "Arry" , "Weasel" ] } } }
new Update().push("key").slice(5).each(Arrays.asList("Arya", "Arry", "Weasel"));
// { $addToSet : { "values" : { "$each" : [ "spring" , "data" , "mongodb" ] } } }
new Update().addToSet("values").each("spring", "data", "mongodb");
“Upserting” Documents in a Collection
Related to performing an updateFirst operation, you can also perform an “upsert” operation, which will perform an insert if no document is found that matches the query. The document that is inserted is a combination of the query document and the update document. The following example shows how to use the upsert method:
template.upsert(query(where("ssn").is(1111).and("firstName").is("Joe").and("Fraizer").is("Update")), update("address", addr), Person.class);
Finding and Upserting Documents in a Collection
The findAndModify(…) method on MongoCollection can update a document and return either the old or newly updated document in a single operation. MongoTemplate provides four findAndModify overloaded methods that take Query and Update classes and converts from Document to your POJOs:
<T> T findAndModify(Query query, Update update, Class<T> entityClass);
<T> T findAndModify(Query query, Update update, Class<T> entityClass, String collectionName);
<T> T findAndModify(Query query, Update update, FindAndModifyOptions options, Class<T> entityClass);
<T> T findAndModify(Query query, Update update, FindAndModifyOptions options, Class<T> entityClass, String collectionName);
The following example inserts a few Person objects into the container and performs a findAndUpdate operation:
mongoTemplate.insert(new Person("Tom", 21));
mongoTemplate.insert(new Person("Dick", 22));
mongoTemplate.insert(new Person("Harry", 23));
Query query = new Query(Criteria.where("firstName").is("Harry"));
Update update = new Update().inc("age", 1);
Person p = mongoTemplate.findAndModify(query, update, Person.class); // return's old person object
assertThat(p.getFirstName(), is("Harry"));
assertThat(p.getAge(), is(23));
p = mongoTemplate.findOne(query, Person.class);
assertThat(p.getAge(), is(24));
// Now return the newly updated document when updating
p = template.findAndModify(query, update, new FindAndModifyOptions().returnNew(true), Person.class);
assertThat(p.getAge(), is(25));
The FindAndModifyOptions method lets you set the options of returnNew, upsert, and remove. An example extending from the previous code snippet follows:
Query query2 = new Query(Criteria.where("firstName").is("Mary"));
p = mongoTemplate.findAndModify(query2, update, new FindAndModifyOptions().returnNew(true).upsert(true), Person.class);
assertThat(p.getFirstName(), is("Mary"));
assertThat(p.getAge(), is(1));
Finding and Replacing Documents
The most straight forward method of replacing an entire Document is via its id using the save method. However this
might not always be feasible. findAndReplace offers an alternative that allows to identify the document to replace via
a simple query.
Optional<User> result = template.update(Person.class) (1)
.matching(query(where("firstame").is("Tom"))) (2)
.replaceWith(new Person("Dick"))
.withOptions(FindAndReplaceOptions.options().upsert()) (3)
.as(User.class) (4)
.findAndReplace(); (5)
| 1 | Use the fluent update API with the domain type given for mapping the query and deriving the collection name or just use MongoOperations#findAndReplace. |
| 2 | The actual match query mapped against the given domain type. Provide sort, fields and collation settings via the query. |
| 3 | Additional optional hook to provide options other than the defaults, like upsert. |
| 4 | An optional projection type used for mapping the operation result. If none given the initial domain type is used. |
| 5 | Trigger the actual execution. Use findAndReplaceValue to obtain the nullable result instead of an Optional. |
Please note that the replacement must not hold an id itself as the id of the existing Document will be
carried over to the replacement by the store itself. Also keep in mind that findAndReplace will only replace the first
document matching the query criteria depending on a potentially given sort order.
|
Methods for Removing Documents
You can use one of five overloaded methods to remove an object from the database:
template.remove(tywin, "GOT"); (1)
template.remove(query(where("lastname").is("lannister")), "GOT"); (2)
template.remove(new Query().limit(3), "GOT"); (3)
template.findAllAndRemove(query(where("lastname").is("lannister"), "GOT"); (4)
template.findAllAndRemove(new Query().limit(3), "GOT"); (5)
| 1 | Remove a single entity specified by its _id from the associated collection. |
| 2 | Remove all documents that match the criteria of the query from the GOT collection. |
| 3 | Remove the first three documents in the GOT collection. Unlike <2>, the documents to remove are identified by their _id, executing the given query, applying sort, limit, and skip options first, and then removing all at once in a separate step. |
| 4 | Remove all documents matching the criteria of the query from the GOT collection. Unlike <3>, documents do not get deleted in a batch but one by one. |
| 5 | Remove the first three documents in the GOT collection. Unlike <3>, documents do not get deleted in a batch but one by one. |
Optimistic Locking
The @Version annotation provides syntax similar to that of JPA in the context of MongoDB and makes sure updates are only applied to documents with a matching version. Therefore, the actual value of the version property is added to the update query in such a way that the update does not have any effect if another operation altered the document in the meantime. In that case, an OptimisticLockingFailureException is thrown. The following example shows these features:
@Document
class Person {
@Id String id;
String firstname;
String lastname;
@Version Long version;
}
Person daenerys = template.insert(new Person("Daenerys")); (1)
Person tmp = template.findOne(query(where("id").is(daenerys.getId())), Person.class); (2)
daenerys.setLastname("Targaryen");
template.save(daenerys); (3)
template.save(tmp); // throws OptimisticLockingFailureException (4)
| 1 | Intially insert document. version is set to 0. |
| 2 | Load the just inserted document. version is still 0. |
| 3 | Update the document with version = 0. Set the lastname and bump version to 1. |
| 4 | Try to update the previously loaded document that still has version = 0. The operation fails with an OptimisticLockingFailureException, as the current version is 1. |
Optimistic Locking requires to set the WriteConcern to ACKNOWLEDGED. Otherwise OptimisticLockingFailureException can be silently swallowed.
|