Monday, December 7, 2015

MongoDB with Spring Data tutorial

A few posts before I presented a tutorial on developing REST APIs and I promised that I would make the example a bit more realistic by adding proper persistence layer, security and so on. It is time to make good on that promise and add persistence on MongoDB database using the Spring Data framework.

If you haven't seen my tutorial on REST APIs you are highly encouraged to do  so before reading this tutorial. If you did I will refresh your memory on the scenario of the tutorial. In the tutorial we built an application that manages (geek) diary entries. The application exposed a REST API which is used to perform CRUD operations on diary entries.

The requirements for replicating and running the tutorial were:

  • JDK 1.7 or higher
  • Maven 
  • Server such as Tomcat 7 or higher
  • Git
  • use of IDE such as eclipse is also advised

and now for the extended version of the tutorial, a running instance of MongoDB is required.

The setup of the project is simple: the ‘config’ package contains the configuration classes for Spring, the ‘data’ package contains our diary entries POJO and the DAOs for data persistence, and finally, in the ‘web’ package is placed the controller with the rest services. The setup is presented in the figure below:

We will not go over all classes again but present only the new or changed ones. Let's start by the new Maven dependencies. We introduce the Spring Data framework in order to communicate with our Mongo database.
<dependency>
    <groupId>org.springframework.data</groupId>
    <artifactId>spring-data-mongodb</artifactId>
    <version>${spring.data.version}</version>
</dependency>   

Also, I added some logging dependencies because I faced a couple of hiccups while developing. The complete pom.xml file is presented below for the convenience of the reader.
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <groupId>com.tasosmartidis</groupId>
  <artifactId>rest-api-tutorial</artifactId>
  <packaging>war</packaging>
  <version>0.1</version>
  <name>rest-api-tutorial Maven Webapp</name>
  <url>http://maven.apache.org</url>
  
  <properties>
        <springframework.version>4.0.6.RELEASE</springframework.version>
        <spring.data.version>1.8.1.RELEASE</spring.data.version> 
    </properties>
    
  <dependencies> 
   <!-- Spring -->
   <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-core</artifactId>
        <version>${springframework.version}</version>
    </dependency>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-web</artifactId>
        <version>${springframework.version}</version>
    </dependency>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-webmvc</artifactId>
        <version>${springframework.version}</version>
    </dependency>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-tx</artifactId>
        <version>${springframework.version}</version>
    </dependency>    
    
    <!-- Added for the MongoDB persistence - start --> 
    <!-- Spring data mongodb -->
    <dependency>
        <groupId>org.springframework.data</groupId>
        <artifactId>spring-data-mongodb</artifactId>
        <version>${spring.data.version}</version>
    </dependency>   
    <!-- Added for the MongoDB persistence - end -->
    
    <!-- logging -->
    <dependency>
        <groupId>org.slf4j</groupId>
        <artifactId>slf4j-api</artifactId>
        <version>1.5.6</version>
        <type>jar</type>
    </dependency>
    <dependency>
         <groupId>org.slf4j</groupId>
         <artifactId>slf4j-simple</artifactId>
         <version>1.5.6</version>
    </dependency> 
        
    <!-- javax -->
    <dependency>
        <groupId>javax.servlet</groupId>
        <artifactId>javax.servlet-api</artifactId>
        <version>3.1.0</version>
    </dependency>
    <dependency>
        <groupId>javax.servlet.jsp.jstl</groupId>
        <artifactId>jstl-api</artifactId>
        <version>1.2</version>
    </dependency>
    <dependency>
        <groupId>javax.servlet.jsp</groupId>
        <artifactId>javax.servlet.jsp-api</artifactId>
        <version>2.3.1</version>
    </dependency>
    <!-- Jackson -->
    <dependency>
        <groupId>com.fasterxml.jackson.core</groupId>
        <artifactId>jackson-core</artifactId>
        <version>2.3.0</version>
    </dependency>   
    <dependency>
        <groupId>com.fasterxml.jackson.core</groupId>
        <artifactId>jackson-databind</artifactId>
        <version>2.3.0</version>
    </dependency>    
    <!-- JUnit -->
    <dependency>
        <groupId>junit</groupId>
        <artifactId>junit</artifactId>
        <version>4.11</version>
    </dependency> 
  </dependencies>
  
  <build>
    <finalName>rest-api-tutorial</finalName>
    <plugins>
        <plugin>
            <artifactId>maven-compiler-plugin</artifactId>
            <version>3.0</version>
            <configuration>
                <source>1.8</source>
                <target>1.8</target>
            </configuration>
        </plugin>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-eclipse-plugin</artifactId>
            <version>2.9</version>
            <configuration>
                <downloadSources>true</downloadSources>
                <downloadJavadocs>true</downloadJavadocs>
            </configuration>
        </plugin>   
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-war-plugin</artifactId>
            <configuration>
              <failOnMissingWebXml>false</failOnMissingWebXml>
            </configuration>
        </plugin>   
    </plugins>
  </build>
</project>

For configuration we used the WebAppInitializer class which has only 3 methods. The getServletMappings() indicates the paths in which the DispatcherServlet will be mapped to. We map it to the default servlet which will handle all requests coming into our application.  The getServletConfigClasses() handles the configuration for the DispacherServlet. The getRootConfigClasses() handles the configuration of the application context created by ContextLoaderListener. In the getRootConfigClasses() method we will add a new class that handles the configuration of our database. The WebAppInitializer now looks like this:
package com.tasosmartidis.rest_api_tutorial.config;

import org.springframework.web.servlet.support.AbstractAnnotationConfigDispatcherServletInitializer;

public class WebAppInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {

    @Override
    protected Class<?>[] getRootConfigClasses() {
        return new Class<?>[] {RootConfig.class, SpringMongoConfig.class};
    }

    @Override
    protected Class<?>[] getServletConfigClasses() {
        return new Class<?>[] {WebConfig.class};
    }

    @Override
    protected String[] getServletMappings() {
        return new String[] {"/"};
    }
}

The new SpringMongoConfig class facilitates us to work with MongoDB via Spring. We define the database name we work with, which is "geek_diaries", the location of our database (local) to connect to and a bean for creating the MongoTemplate object which provides implementation of MongoOperations. SpringMongoConfig is presented in the following source code:
package com.tasosmartidis.rest_api_tutorial.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.mongodb.config.AbstractMongoConfiguration;
import org.springframework.data.mongodb.core.MongoTemplate;

import com.mongodb.Mongo;
import com.mongodb.MongoClient;

@Configuration
public class SpringMongoConfig extends AbstractMongoConfiguration {

    @Override
    protected String getDatabaseName() { 
        return "geek_diaries";
    }

    @Override
    @Bean
    public Mongo mongo() throws Exception { 
        return new MongoClient("127.0.0.1");
    }
    
    @Bean
    public MongoTemplate mongoTemplate() throws Exception {
        return new MongoTemplate(mongo(),getDatabaseName());
    }
}

As the reader probably remembers, our REST API exposed services for performing CRUD operations. We had a DaoMock object that implemented the Dao interface:
package com.tasosmartidis.rest_api_tutorial.data;

import java.util.Map;

public interface Dao {

    public DiaryEntry createDiaryEntry(DiaryEntry newEntry);
    
    public DiaryEntry getDiaryEntry(String entryId);
    
    public DiaryEntry updateDiaryEntry(DiaryEntry updatedEntry);
    
    public String deleteDiaryEntry(String entryId);
    
    public Map<String,DiaryEntry> getAllDiaryEntries();
}

Our new DaoMongo class will implement this interface and will take care of performing CRUD on the actual Mongo instance for our REST API. The class will be presented first and then some comments will follow about its implementation.
package com.tasosmartidis.rest_api_tutorial.data;

import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.data.mongodb.core.MongoOperations;
import org.springframework.data.mongodb.core.query.Criteria;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional;

import com.tasosmartidis.rest_api_tutorial.config.SpringMongoConfig;


@Repository("daoMongo")
@Transactional
public class DaoMongo implements Dao {

    private MongoOperations mongoOperations;
    
    public DaoMongo() {
        mongoOperations = loadMongoConfiguration();
    }
    
    public DiaryEntry createDiaryEntry(DiaryEntry newEntry) {       
        // The DiaryEntry instance will have the ID assigned to it automatically
        mongoOperations.save(newEntry);
        
        return newEntry;
    }

    public DiaryEntry getDiaryEntry(String entryId) {
        // query to search diary entries. We will retrieve entries with their specified unique ID 
        Query searchQuery = new Query(); 
        searchQuery.addCriteria(Criteria.where("entryId").is(entryId)); 
        
        // retrieve the specified DiaryEntry from database
        return mongoOperations.findOne(searchQuery,DiaryEntry.class);       
    }

    public DiaryEntry updateDiaryEntry(DiaryEntry updatedEntry) {
        
        // If entry with the same id exists, it updates the entry
        mongoOperations.save(updatedEntry);
        
        // note that we could also use 'Update' object and e.g., its 'set' method
        // to update individual fields of an entry
                
        return getDiaryEntry(updatedEntry.getEntryId());
    }

    public String deleteDiaryEntry(String entryId) {
        Query searchQuery = new Query(); 
        searchQuery.addCriteria(Criteria.where("entryId").is(entryId));
        
        // delete the specified DiaryEntry from database
        mongoOperations.remove(searchQuery,DiaryEntry.class); 
        
        return entryId;
    }

    public Map<String, DiaryEntry> getAllDiaryEntries() {
        List<DiaryEntry> allEntries = mongoOperations.findAll(DiaryEntry.class); 
        return putEntriesToMap(allEntries);
    }
    
    private Map<String, DiaryEntry> putEntriesToMap(List<DiaryEntry> allEntries) {
        Map<String, DiaryEntry> mapWithEntries = new HashMap<>();
        
        for(DiaryEntry entry : allEntries) {
            mapWithEntries.put(entry.getEntryId(), entry);
        }
        
        return mapWithEntries;
    }

    private MongoOperations loadMongoConfiguration() {
        @SuppressWarnings("resource")
        ApplicationContext applicationContext = 
                 new AnnotationConfigApplicationContext(SpringMongoConfig.class);
        MongoOperations mongoOperation = (MongoOperations) applicationContext.getBean("mongoTemplate");
        
        return mongoOperation;
    }

}

The class has a private method loadMongoConfiguration() which is called in its constructor and provides a MongoOpetations object. We use this object to perform CRUD operations in our different methods. In our methods we do not have any exception handling; it is not required by Spring framework for the CRUD methods and we do not add any for simplicity and readability of the code. But in a real world application we would provide such exception handling. Also, when the tutorial was created the scenario was a simple idea and not very well examined, just defined some methods. In order to keep the implementation of the interface we added some processing such as the putEntriesToMap() method which helps with the 'getAllDiaryEntries' . Such methods are not related with Sping and MongoDB, we just use them to stay loyal to our initial tutorial scenario. Ok, we are almost done! we just need to add a couple of annotations to our POJO and change the autowired bean from DaoMock to our newly created DaoMongo. In the DiaryEntry POJO we specify the name of the collection for our documents (one object corresponds to a document in our collection) and also we specify that the 'entryId'  field will be our unique identification for each document which will be generated automatically. The updated class:
package com.tasosmartidis.rest_api_tutorial.data;

import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.mapping.Document;

@Document(collection="entries") 
public class DiaryEntry {

    @Id
    private String entryId;
    private String entryTitle;
    private String entryText;
    
    public String getEntryId() {
        return entryId;
    }
    public void setEntryId(String entryId) {
        this.entryId = entryId;
    }
    public String getEntryTitle() {
        return entryTitle;
    }
    public void setEntryTitle(String entryTitle) {
        this.entryTitle = entryTitle;
    }
    public String getEntryText() {
        return entryText;
    }
    public void setEntryText(String entryText) {
        this.entryText = entryText;
    }
    
    @Override
    public int hashCode() {
        final int prime = 31;
        int result = 1;
        result = prime * result + ((entryId == null) ? 0 : entryId.hashCode());
        result = prime * result
                + ((entryTitle == null) ? 0 : entryTitle.hashCode());
        return result;
    }
    @Override
    public boolean equals(Object obj) {
        if (this == obj)
            return true;
        if (obj == null)
            return false;
        if (getClass() != obj.getClass())
            return false;
        DiaryEntry other = (DiaryEntry) obj;
        if (entryId == null) {
            if (other.entryId != null)
                return false;
        } else if (!entryId.equals(other.entryId))
            return false;
        if (entryTitle == null) {
            if (other.entryTitle != null)
                return false;
        } else if (!entryTitle.equals(other.entryTitle))
            return false;
        return true;
    }
    
}

Next our controller class, which exposes the REST API is almost unchanged, just use the DaoMongo instead of DaoMock:
package com.tasosmartidis.rest_api_tutorial.web;

import java.util.Map;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;

import com.tasosmartidis.rest_api_tutorial.data.DaoMongo;
import com.tasosmartidis.rest_api_tutorial.data.DiaryEntry;

@RestController
@RequestMapping("/service/geek-diaries")
public class DiaryService {
    
    @Autowired
    DaoMongo dao; 
    
    @RequestMapping(value="/entry/{entryId}", method=RequestMethod.GET)
    public ResponseEntity<DiaryEntry> getDiaryEntry(@PathVariable String entryId) {
        DiaryEntry diaryEntry = dao.getDiaryEntry(entryId);
        
        return new ResponseEntity<DiaryEntry>(diaryEntry,HttpStatus.OK); 
    }
    
    @RequestMapping(value="/entry/", method=RequestMethod.POST)
    public ResponseEntity<DiaryEntry> createDiaryEntry(@RequestBody DiaryEntry newEntry) { 
         DiaryEntry newDiaryEntry = dao.createDiaryEntry(newEntry);

        return new ResponseEntity<DiaryEntry>(newDiaryEntry,HttpStatus.OK); 
    }
    
    @RequestMapping(value="/entry/", method=RequestMethod.PUT)
    public ResponseEntity<DiaryEntry> updateDiaryEntry(@RequestBody DiaryEntry newEntry) { 
         DiaryEntry updatedDiaryEntry = dao.updateDiaryEntry(newEntry);

        return new ResponseEntity<DiaryEntry>(updatedDiaryEntry,HttpStatus.OK); 
    }
    
    @RequestMapping(value="/entry/{entryId}", method=RequestMethod.DELETE)
    public ResponseEntity<String> deleteDiaryEntry(@PathVariable String entryId) { 
         String deletedDiaryEntry = dao.deleteDiaryEntry(entryId);

        return new ResponseEntity<String>(deletedDiaryEntry,HttpStatus.OK); 
    }
    
    @RequestMapping(value="/entry/", method=RequestMethod.GET)
    public ResponseEntity<Map<String, DiaryEntry>> getDiaryEntries() { 
        Map<String, DiaryEntry> diaryEntries = dao.getAllDiaryEntries();

        return new ResponseEntity<Map<String, DiaryEntry>>(diaryEntries,HttpStatus.OK); 
    }
}

Don't you love it when software is properly designed and layered so that changes are easy to make? That's it! There is nothing else to change from our initial tutorial. As a brief recap, the maven was extended to include the new dependencies (i.e., spring data), an additional configuration class for connecting to MongoDB was created, the DaoMongo class for CRUD operations was defined and replaced DaoMock in the DiaryService controller.

Now let's see how the application works. We already started MongoDB, Tomcat and we deployed the packaged war. We will use POSTMAN to perform the CRUD operations. Let's start by creating a couple of diary entries using a POST operation:

and another one:

We can see the created entries from the MongDB shell as shown in the figure below:
and by calling the getAllEntries service which is a GET request:

Next we will retrieve a specific diary entry by indicating its unique ID in the URL of the GET request:
 Let's update one of our entries with a PUT operation:

 And conclude our CRUD operations with a DELETE request for a resource:

Let's see again in our MongoDB shell, the documents that exist in our collection:

And we are done! We extended our REST API to persist data in MongoDB using Spring Data. The complete working project is available on GitHub.

No comments:

Post a Comment