Developing using spring-data-mongodb

From the perspective of developers, when a program needs to interact with a MongoDB instance, they need to use the respective client APIs for their specific platforms. The trouble with doing this is that we need to write a lot of boilerplate code, and it is not necessarily object-oriented. For instance, we have a class called Person with various attributes such as name, age, and address. The corresponding JSON document too shares a similar structure to this Person class as follows:

{
  name:"…",
  age:..,
  address:{lineOne:"…", …}
}

However, to store this document, we need to convert the Person class to a DBObject, which is a map with key and value pairs. What is really desired is to let us persist this Person class itself as an object in the database, without having to convert it to DBObject.

Also, some of the operations such as searching by a particular field of a document, saving an entity, deleting an entity, and searching by ID are pretty common, and we tend to repeatedly write similar boilerplate code. In this recipe, we will see how spring-data-mongodb relieves us of these laborious and cumbersome tasks not only to reduce the development effort but also to dramatically reduce the possibility of introducing bugs in these commonly written functions.

Getting ready

The SpringDataMongoTest project present in the bundle in the chapter is a Maven project and is to be imported into any IDE of your choice. The required Maven artifacts will automatically be downloaded. A single MongoDB instance that listens to port 27017 is required to be up-and-running. Refer to the Single node installation of MongoDB recipe in Chapter 1, Installing and Starting the MongoDB Server, to know how to start a standalone instance.

For the aggregation example, we will use the postal code data. Refer to the Creating test data recipe in Chapter 2, Command-line Operations and Indexes, for the creation of test data.

How to do it…

  1. We will explore the repository feature of spring-data-mongodb first. Open the test case class named com.packtpub.mongo.cookbook.MongoCrudRepositoryTest from your IDE and execute it. If all goes well and the MongoDB server instance is reachable, the test case will execute successfully.
  2. Another test case named com.packtpub.mongo.cookbook.MongoCrudRepositoryTest2 is used to explore more features of the repository support provided by spring-data-mongodb. This test case too should get executed successfully.
  3. We will see how MongoTemplate of spring-data-mongodb can be used to perform CRUD operations and other common operations on MongoDB. Open the com.packtpub.mongo.cookbook.MongoTemplateTest class and execute it.
  4. Alternatively, if an IDE is not used, all the tests can be executed using Maven from the command prompt as follows, with the current directory being the root of the SpringDataMongoTest project:
    $ mvn clean test
    

How it works…

We will first look at what we did in com.packtpub.mongo.cookbook.MongoCrudRepositoryTest, where we saw the repository support provided by spring-data-mongodb. Just in case you didn't notice, we haven't written a single line of code for the repository. The magic of implementing the required code for us is done by the Spring data project. Let's start by looking at the relevant portions of the XML config file:

<mongo:repositories base-package="com.packtpub.mongo.cookbook" />
<mongo:mongo id="mongo" host="localhost" port="27017"/>
<mongo:db-factory id="factory" dbname="test" mongo-ref="mongo"/>
<mongo:template id="mongoTemplate" db-factory-ref="factory"/>

We will first look at the last three lines: spring-data-mongodb namespace declarations to instantiate com.mongodb.Mongo, instantiating a factory for the com.mongodb.DB instances from the client, and a template instance, used to perform various operations on MongoDB, respectively. We will see org.springframework.data.mongodb.core.MongoTemplate in more detail later.

The first line is a namespace declaration for the base package of all the CRUD repositories we have. In this package, we have an interface with the following body:

public interface PersonRepository extends PagingAndSortingRepository<Person, Integer>{

  /**
   *
  * @param lastName
   * @return
   */
  Person findByLastName(String lastName);
}

The PagingAndSortingRepository interface is from the org.springframework.data.repository package of the Spring data core project and extends from CrudRepository of the same project. These interfaces give us some most common methods such as searching by the ID/primary key, deleting an entity, inserting an entity, and updating an entity. The repository needs an object that it maps to the underlying data store. The Spring data project supports a large number of data stores that are not just limited to SQL (using Java Database Connectivity (JDBC) and JPA) or MongoDB but also to other NoSQL stores such as Redis and Hadoop and search engines such as Solr and Elasticsearch. In the case of spring-data-mongodb, the object is mapped to a document in the collection.

The PagingAndSortingRepository<Person, Integer> signature indicates that the first one is the entity that the CRUD repository is built for, and the second one is the type of the primary key/ID field.

We have added just one method named findByLastName; this accepts one string value for the last name as a parameter. This is an interesting operation; it is specific to our repository and not even implemented by us, but it will still work just as expected. The Person class is a POJO where we have annotated the ID field with the org.springframework.data.annotation.Id annotation. Nothing else is really special about this class; it just has some plain getters and setters.

With all these small details, let's join these dots together by answering some questions you'll have in mind. First, we will see which server, database, and collection our data goes to. If we look at the mongo:mongo XML definition for the config file, we will see that we instantiated the com.mongodb.Mongo class by connecting to a localhost and port 27017. The mongo:db-factory declaration is used to denote that the database to be used is test. One final question is, which collection? The simple name of our class is Person. The name of the collection is the simple name with the first character lowercased; thus, Person will become person, and something like BillingAddress will become the billingAddress collection. These are the default values. However, if you need to override this value, you can annotate your class with the org.springframework.data.mongodb.core.mapping.Document annotation and use its attribute collection to give any name of your choice, as we will see in an example later.

To view the document in the collection, execute just one test case method called saveAndQueryPerson from the com.packtpub.mongo.cookbook.MongoCrudRepositoryTest class. Now, connect to the MongoDB instance from the Mongo shell and execute the following query:

> use test
> db.person.findOne({_id:1})
{
  "_id" : 1,
  "_class" : "com.packtpub.mongo.cookbook.domain.Person",
  "firstName" : "Steve",
  "lastName" : "Johnson",
  "age" : 20,
  "gender" : "Male"

}

As we can see in the preceding result, the contents of the document are similar to the object we persisted using the CRUD repository. The names of the field in the document are the same as the names of the respective attributes in the Java object, with two exceptions. The field annotated with @Id is now _id, irrespective of the name of the field in the Java class, and an additional _class attribute is added to the document whose value is the fully qualified name of the Java class itself. This is not of any use for the application but is used by spring-data-mongodb as metadata.

Now, it makes more sense and gives us an idea about what spring-data-mongodb must be doing for all the basic CRUD methods. All the operations we perform will use the MongoTemplate class (MongoOperations to be precise, which is an interface that MongoTemplate implements) from the spring-data-mongodb project. To find it by using the primary key, it will invoke a find query by the _id field on the collection derived using the Person entity class. The save method simply calls the save method on MongoOperations; this in turn calls the save method on the com.mongodb.DBCollection class.

We still haven't answered how the findByLastName method worked. How does Spring know what query to invoke to return the data? These are the special types of methods that begin with find, findBy, get, or getBy. There are some rules that one needs to follow while naming a method, and the proxy object on the repository interface is able to correctly convert this method into an appropriate query on the collection. For instance, the findByLastName method in the repository of the Person class will execute a query on the lastName field in the document of the Person class. Hence, the findByLastName(String lastName) method will fire the following query in the database:

db.person.find({'lastName': lastName })

Based on the return type of the method defined, it will return either a list or the first result in the returned result from the database. We have used findBy in our queries. However, for anything that begins with find, having any text in between and having text that ends in By works. For instance, findPersonBy is the same as findBy.

For more information on these findBy methods, we have another test class, MongoCrudRepositoryTest2. Open this class in your IDE where it can be read along with this text. We have already executed this test case; now, let's see these findBy methods used and their behavior. This class has seven findBy methods in it, with one of the methods being a variant of another method in the same interface. To get a clear idea of the queries, we will first look at one of the documents in the personTwo collection in the test database. Execute the following commands on the Mongo shell connected to the MongoDB server that runs on a localhost:

> use test
> db.personTwo.findOne({firstName:'Amit'})
{
  "_id" : 2,
  "_class" : "com.packtpub.mongo.cookbook.domain.Person2",
  "firstName" : "Amit",
  "lastName" : "Sharma",
  "age" : 25,
  "gender" : "Male",
  "residentialAddress" : {
  "addressLineOne" : "20, Central street",
  "city" : "Mumbai",
  "state" : "Maharashtra",
  "country" : "India",
  "zip" : "400101"
  }
}

Also, note that the repository uses the Person2 class. However, the name of the collection used is personTwo. This was possible because we used the @Document(collection="personTwo") annotation on top of the Person2 class.

Getting back to the seven methods in the com.packtpub.mongo.cookbook.PersonRepositoryTwo repository class, let's look at them one by one:

Method

Description

findByAgeGreaterThanEqual

This method will fire the {'age':{'$gte':<age>}} query on the personTwo collection.

The secret lies in the name of the method. If we break it up, what we have after findBy tells us what we want. The age property (with first character lowercased) is the field that would be queried on the document with the $gte operator because we have GreaterThanEqual in the name of the method. The value that would be used for the comparison would be the value of the parameter passed. The result is a collection of Person2 entities, as we will have multiple matches.

findByAgeBetween

This method will query on age but will use a combination of $gt and $lt to find the matching result. The query in this case will be {'age' : {'$gt' : from, '$lt' : to}}. It is important to note that both the from and to values are exclusive in the range. There are two methods in the test case: findByAgeBetween and findByAgeBetween2. These methods demonstrate the behavior of the between query for different input values.

findByAgeGreaterThan

This method is a special method that also sorts the result because there are two parameters to the method: the value against which the age parameter will be compared is the first parameter, and the second parameter is the field of type org.springframework.data.domain.Sort. For more details, refer to the Javadoc for spring-data-mongodb.

findPeopleByLastNameLike

This method is used to find results by the last name that matches a pattern. Regular expressions are used for the matching purpose. For instance, in this case, the query fired will be {'lastName' : <lastName as regex>}. This method's name begins with findPeopleBy instead of findBy, which works in the same way as findBy. Thus, when we say findBy in all the descriptions, we actually mean find…By.

The value provided as the parameter will be used to match the last name.

findByResidentialAddressCountry

This is an interesting method to look at. Here, we are looking to search by the country of the residential address. This is, in fact, a field in the Address class in the residentialAddress field of the person. Take a look at the document from the personTwo collection mentioned earlier for how the query will be used.

When the Spring data finds the name as ResidentialAddressCountry, it will try to find various combinations using this string. For instance, it can look at residentialAddressCountry in Person or in residential.addressCountry, residentialAddress.country, or residential.address.country. If there are no conflicting values, as in our case, residentialAddress.country is a success path in the Person2 object tree, and thus, this will be used in the query.

However, if there are conflicts, then underscores can be used to clearly specify what we are looking at. In this case, the method can be renamed to findByResidentialAddress_country to clearly specify what we expect as the result. The findByCountry2 test case method demonstrates this.

findByFirstNameAndCountry

This is an interesting method as well. We are not always able to use the method names to implement what we actually want to, as the name of the method required for Spring to automatically implement the query might be bit awkward to use as is. For instance, findByCountryOfResidence sounds better than findByResidentialAddressCountry. However, we are stuck with the latter, as this is how spring-data-mongodb will construct the query. Using findByCountryOfResidence gives no details on how to construct the query to Spring data.

However, there is a solution to this. You might choose to use the @Query annotation and specify the query to be executed when the method is invoked. The following is the annotation we used in our case:

@Query("{'firstName':?0, 'residentialAddress.country': ?1}")

We write the value as a query that will get executed, and we bind the parameters of the functions to the query as numbered parameters that start from 0. Thus, the first parameter of the method will be bound to?0, the second to ?1, and so on.

We saw how the findBy or getBy method is automatically translated to queries for MongoDB. Similarly, we have some well-known prefixes for the methods. The countBy prefix returns the long number for the count for a given condition, which is derived from the rest of the method names that are similar to findBy. We can have deleteBy or removeBy to delete the documents by the derived condition. Also, one thing to note about the com.packtpub.mongo.cookbook.domain.Person2 class is that it does not have a no-argument constructor or setter to set the values. Spring will, instead, use reflection to instantiate this object.

A lot of findBy methods are supported by spring-data-mongodb, and all are not covered here. For more details, refer to the spring-data-mongodb reference manual. A lot of XML-based or Java-based configuration options are available too and can be found in the reference manual. The links are given in the See also section later in this recipe.

We are not done yet, though; we have another test case, com.packtpub.mongo.cookbook.MongoTemplateTest. This uses org.springframework.data.mongodb.core.MongoTemplate to perform various operations. We can open the test case class, and we will see what operations are performed and which methods of the MongoTemplate class are invoked.

Let's look at some of the important and frequently used methods of the MongoTemplate class:

Method

Description

save

This method is used to save (insert if new; otherwise, update) an entity in MongoDB. The method takes one parameter, the entity, and finds the target collection based on its name or the @Document annotation present in it.

There is an overloaded version of the save method that also accepts the second parameter, which is the name of the collection to which the data entity passed needs to be persisted.

remove

This method will be used to remove documents from the collection. It has got some overloaded versions in this class. All of them accept either an entity to be deleted or the org.springframework.data.mongodb.core.query.Query instance, which is used to determine the document(s) to be deleted. The second parameter is then the name of the collection from which the document is to be deleted. When an entity is provided, the name of the collection can be derived. With a Query instance provided, we have to give either the name of the collection or the entity class name, which in turn will be used to derive the name of the collection.

updateMulti

This is the function invoked to update multiple documents with one update call. The first parameter is the query that will be used to match the documents. The second parameter is an instance of org.springframework.data.mongodb.core.query.Update. This is the update that will be executed on the documents selected using the first query object. The next parameter is the entity class or the collection name to execute the update on. For more details on the method and its various overloaded versions, refer to the Javadoc.

updateFirst

This is the opposite of the updateMulti method. This operation will update just the first matching document. We have not covered this method in our unit test case.

insert

We mentioned that the save method can perform insertions and updates. The insert method in the template calls the insert method of the underlying Mongo client. If one entity document is to be inserted, there is no difference in calling the insert or save method.

However, as we saw in the test case in the insertMultiple method, we created a list of three Person instances and passed them to the insert method. All the three documents for the three Person instances will go to the server as part of one call. The behavior of an insert when it fails is determined by the continue on error parameter of the write concern. It will determine whether the bulk insert fails on the first failure or continues even after errors that report only the last error. The page at http://docs.mongodb.org/manual/core/bulk-inserts/ gives more details on bulk inserts and various write concern parameters that can alter the behavior.

findAndRemove/findAllAndRemove

Both these operations are used to find and then remove the document(s). The findAndRemove method finds one document and then returns the deleted document. This operation is atomic. However, the findAllAndRemove method finds all the documents and removes them before returning the list of all the entities of all the documents deleted.

findAndModify

This method is functionally similar to findAndModify that we have with the Mongo client library. It will atomically find and modify the document. If the query matches more than one document, only the first match will be updated. The first two parameters of this method are the query and the update to execute. The next parameter is either the entity class or the collection name to execute the operation on. Also, there is a special class called org.springframework.data.mongodb.core.FindAndModifyOptions that makes sense only for the findAndModify operation. This instance tells us whether we are looking for the new instance or the old instance after the operation is performed, and whether the upsert operation is to be performed and it is relevant only if the document with the matching query doesn't exist. There is an additional Boolean flag to tell the client whether this is a find and remove operation. In fact, the findAndRemove operation we saw earlier is just a convenience function that delegates to findAndModify with this remove flag set.

In the preceding table, we mentioned the Query and Update classes when talking about update. These are special convenience classes in spring-data-mongodb; they let us build MongoDB queries using a syntax that is easy to understand and improves readability. For instance, the query to check whether lastName is Johnson in the Mongo query language is {'lastName':'Johnson'}. The same query can be constructed in spring-data-mongodb as follows:

new Query(Criteria.where("lastName").is("Johnson"))

This syntax looks neat as compared to giving the query in JSON. Let's take another example where we want to find all females under 30 years of age in our database. The query will now be built as follows:

new Query(Criteria.where("age").lt(30).and("gender").is("Female"))

Similarly, for update, we want to set a youngCustomer Boolean flag to be true for some of the customers, based on some conditions. To set this flag in the document, the MongoDB format will be as follows:

{'$set' : {'youngCustomer' : true}}

In spring-data-mongodb, the same will be achieved as follows:

new Update().set("youngCustomer", true)

Refer to the Javadoc for all the possible methods that are available to build the query and updates in spring-data-mongodb that are to be used with MongoTemplate.

The methods mentioned earlier are by no means the only ones available in the MongoTemplate class. There are a lot of other methods for geospatial indexes, convenience methods to get the count of documents in the collection, aggregation, and MapReduce support, and so on. Refer to the Javadoc of MongoTemplate for more details and methods.

Speaking of aggregation, we also have a test case method called aggregationTest to perform the aggregation operation on the collection. We have a postalCodes collection in MongoDB; this collection contains the postal code details of various cities. An example document in the collection is as follows:

{
  "_id" : ObjectId("539743b26412fd18f3510f1b"),
  "postOfficeName" : "A S D Mello Road Fuller Marg",
  "pincode" : 400001,
  "districtsName" : "Mumbai",
  "city" : "Mumbai",
  "state" : "Maharashtra"
}

Our aggregation operation intends to find the top five states by the number of documents in the collection. In Mongo, the aggregation pipeline will look as follows:

[
{'$project':{'state':1, '_id':0}},
{'$group':{'_id':'$state', 'count':{'$sum':1}}}
{'$sort':{'count':-1}},
{'$limit':5}
]

In spring-data-mongodb, we invoked the aggregation operation using the MongoTemplate class as follows:

Aggregation aggregation = newAggregation(

  project("state", "_id"),
  group("state").count().as("count"),
  sort(Direction.DESC, "count"),
  limit(5)
);

AggregationResults<DBObject> results = mongoTemplate.aggregate(
  aggregation,
  "postalCodes",
  DBObject.class);

The key is in creating an instance of the org.springframework.data.mongodb.core.aggregation.Aggregation class. The newAggregation method is statically imported from the same class and accepts varargs for different instances of org.springframework.data.mongodb.core.aggregation.AggregationOperation that correspond to the one operation in the chain. The Aggregation class has various static methods to create the instances of AggregationOperation. We have used a few of them, such as project, group, sort, and limit. For more details and available methods, refer to the Javadoc. The aggregate method in MongoTemplate takes three arguments. The first one is the instance of the Aggregation class, the second one is the name of the collection, and the third one is the return type of the aggregation result. Refer to the aggregation operation test case for more details.

See also

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset