Friday 13 July 2012

Using Spring Data to access MongoDB

Spring Data is the data access Spring project for integrating with data stores. This post will cover the Spring Data sub project for accessing the document store MongoDB. It follows on from the Morphia post by showing how Spring Data for MongoDB would persist and query the same POJOs.

The four domain objects are shown below:

package com.city81.mongodb.springdata.entity;

import org.springframework.data.annotation.Id;

public abstract class BaseEntity {

 @Id
 protected String id;
 private Long version;

 public BaseEntity() {
  super();
 }

 public String getId() {
  return id;
 }

 public void setId(String id) {
  this.id = id;
 }

 public Long getVersion() {
  return version;
 }

 public void setVersion(Long version) {
  this.version = version;
 }

}


package com.city81.mongodb.springdata.entity;

import java.util.List;

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

@Document
public class Customer extends BaseEntity {

 private String name;
 private List<Account> accounts;
 private Address address;
 
 public String getName() {
  return name;
 }

 public void setName(String name) {
  this.name = name;
 } 
  
 public List<Account> getAccounts() {
  return accounts;
 }

 public void setAccounts(List<Account> accounts) {
  this.accounts = accounts;
 }

 public Address getAddress() {
  return address;
 }

 public void setAddress(Address address) {
  this.address = address;
 }
 
}

package com.city81.mongodb.springdata.entity;


public class Address {

 private String number;
 private String street;
 private String town;
 private String postcode;
 
 public String getNumber() {
  return number;
 }
 public void setNumber(String number) {
  this.number = number;
 }
 public String getStreet() {
  return street;
 }
 public void setStreet(String street) {
  this.street = street;
 }
 public String getTown() {
  return town;
 }
 public void setTown(String town) {
  this.town = town;
 }
 public String getPostcode() {
  return postcode;
 }
 public void setPostcode(String postcode) {
  this.postcode = postcode;
 }
}

package com.city81.mongodb.springdata.entity;

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

@Document
public class Account extends BaseEntity {

 private String accountName;
 
 public String getAccountName() {
  return accountName;
 }

 public void setAccountName(String accountName) {
  this.accountName = accountName;
 }

}

Some classes are marked with the @Document annotation. This is optional but does allow you to provide a collection name eg

@Document(collection="personalBanking")


Also in the Customer class, the Address and Accounts attributes would be stored as embedded within the document but they can be stored separately by marking the variables with @DBRef. These objects will then be eagerly loaded when the Customer record is retrieved.

Next, using XML based metadata to register a MongoDB instance and a MongoTemplate instance.

The MongoFactoryBean is used to register an instance of com.mongodb.Mongo. Using the Bean as opposed to creating an instance of Mongo itself ensures that the calling code doesn't have to handle the checked exception UnknownHostException. It also ensures database specific exceptions are translated to be Spring exceptions of the DataAccessException hierarchy.

The MongoTemplate provides the operations (via the MongoOperations interface) to interact with MongoDB documents. This thread safe class has several constructors but for this example we only need to call the one that takes an instance of Mongo and the database name.

Whilst you could call the operations on the MongoTemplate to manage the entities, there exists a MongoRepository interface which can be extended to be 'document' specific via the use of Generics. The <mongo:repositories base-package="com.city81.mongodb.springdata.dao" />   registers beans extending this interface.



<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
 xmlns:context="http://www.springframework.org/schema/context"
 xmlns:mongo="http://www.springframework.org/schema/data/mongo" 
 xsi:schemaLocation="http://www.springframework.org/schema/beans        
  http://www.springframework.org/schema/beans/spring-beans-3.0.xsd         
  http://www.springframework.org/schema/context         
  http://www.springframework.org/schema/context/spring-context-3.0.xsd
  http://www.springframework.org/schema/data/mongo 
  http://www.springframework.org/schema/data/mongo/spring-mongo.xsd">    
  
 <context:annotation-config />
  
 <context:component-scan base-package="com.city81.mongodb.springdata" />
 
 <!-- MongoFactoryBean instance --> 
 <bean id="mongo" class="org.springframework.data.mongodb.core.MongoFactoryBean">
  <property name="host" value="localhost" />
 </bean>   
 
 <!-- MongoTemplate instance --> 
 <bean id="mongoTemplate" class="org.springframework.data.mongodb.core.MongoTemplate">
  <constructor-arg name="mongo" ref="mongo" />
  <constructor-arg name="databaseName" value="bank" />
 </bean> 
 
 <mongo:repositories base-package="com.city81.mongodb.springdata.dao" />
  
</beans>

The repository class in this example is the CustomerRepository. It gets wired with the MongoTemplate so provides the same (this time implicit type safe) operations but also provides the ability to add other methods. In this example, a find method has been added to demonstrate how a query can be built from the method name itself. There is no need to implement this method as Spring Data will parse the method name and determine the criteria ie findByNameAndAddressNumberAndAccountsAccountName will return documents where the customer name is equal to the first arg (name), and where the customer address number is equal to the second arg (number) and where the customer has an account which has an account name equal to the thrid arg (accountName).


package com.city81.mongodb.springdata.dao;

import java.util.List;

import org.springframework.data.mongodb.repository.MongoRepository;
import org.springframework.stereotype.Repository;

import com.city81.mongodb.springdata.entity.Customer;

@Repository
public interface CustomerRepository extends MongoRepository<Customer,String> {
 
 List<Customer> findByNameAndAddressNumberAndAccountsAccountName(
   String name, String number, String accountName);
 
}


In this example, we'll add a service layer in the form of the CustomerService class, which for this simple example just wraps the repository calls. The class has the CustomerRepository wired in and this service class is then in turn called from the Example class, which performs similar logic to the Morphia Example class.

package com.city81.mongodb.springdata.dao;

import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import com.city81.mongodb.springdata.entity.Customer;

@Service
public class CustomerService {  

 @Autowired
 CustomerRepository customerRepository;
  
 public void insertCustomer(Customer customer) {    
  customerRepository.save(customer);
 }
 
 public List<Customer> findAllCustomers() {           
  return customerRepository.findAll();
 }       
 
 public void dropCustomerCollection() {        
  customerRepository.deleteAll();   
 } 
 
 public List<Customer> findSpecificCustomers(
   String name, String number, String accountName) {           
  return customerRepository.findByNameAndAddressNumberAndAccountsAccountName(
    name, number, accountName);
 } 
 
}

package com.city81.mongodb.springdata;

import java.util.ArrayList;
import java.util.List;

import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

import com.city81.mongodb.springdata.dao.CustomerService;
import com.city81.mongodb.springdata.entity.Account;
import com.city81.mongodb.springdata.entity.Address;
import com.city81.mongodb.springdata.entity.Customer;

public class Example {
 
 public static void main( String[] args ) {     
  
  ConfigurableApplicationContext context
   = new ClassPathXmlApplicationContext("spring/applicationContext.xml");       
  
  CustomerService customerService = context.getBean(CustomerService.class);       
  
  // delete all Customer records
  customerService.dropCustomerCollection();  
  
     Address address = new Address();
     address.setNumber("81");
     address.setStreet("Mongo Street");
     address.setTown("City");
     address.setPostcode("CT81 1DB");
    
     Account account = new Account();
     account.setAccountName("Personal Account");
     List<Account> accounts = new ArrayList<Account>();
     accounts.add(account);
     
     Customer customer = new Customer();
     customer.setAddress(address);
     customer.setName("Mr Bank Customer");
     customer.setAccounts(accounts);
          
     // insert a Customer record into the database
  customerService.insertCustomer(customer);          
     
     address = new Address();
     address.setNumber("101");
     address.setStreet("Mongo Road");
     address.setTown("Town");
     address.setPostcode("TT10 5DB");
    
     account = new Account();
     account.setAccountName("Business Account");
     accounts = new ArrayList<Account>();
     accounts.add(account);
     
     customer = new Customer();
     customer.setAddress(address);
     customer.setName("Mr Customer");
     customer.setAccounts(accounts);  

     // insert a Customer record into the database
  customerService.insertCustomer(customer);     
      
  // find all Customer records
  System.out.println("\nALL CUSTOMERS:");
  List<Customer> allCustomers = customerService.findAllCustomers();     
  for (Customer foundCustomer : allCustomers) {
   System.out.println(foundCustomer.getId() + " " + foundCustomer.getName());   
   System.out.println(foundCustomer.getAddress().getTown());   
   System.out.println(foundCustomer.getAccounts().get(0).getAccountName() + "\n");   
  }
  
  // find by customer name, address number and account name
  System.out.println("\nSPECIFIC CUSTOMERS:");  
  List<Customer> specficCustomers = customerService.findSpecificCustomers(
    "Mr Customer","101","Business Account");     
  for (Customer foundCustomer : specficCustomers) {
   System.out.println(foundCustomer.getId() + " " + foundCustomer.getName());   
   System.out.println(foundCustomer.getAddress().getTown());   
   System.out.println(foundCustomer.getAccounts().get(0).getAccountName() + "\n");   
  }
  
 } 
 
}
The output from the above would look similiar to the below:

04-Oct-2012 13:48:48 org.springframework.context.support.AbstractApplicationContext prepareRefresh
INFO: Refreshing org.springframework.context.support.ClassPathXmlApplicationContext@15eb0a9: startup date [Thu Oct 04 13:48:48 BST 2012]; root of context hierarchy
04-Oct-2012 13:48:48 org.springframework.beans.factory.xml.XmlBeanDefinitionReader loadBeanDefinitions
INFO: Loading XML bean definitions from class path resource [spring/applicationContext.xml]
04-Oct-2012 13:48:48 org.springframework.beans.factory.support.DefaultListableBeanFactory preInstantiateSingletons
INFO: Pre-instantiating singletons in org.springframework.beans.factory.support.DefaultListableBeanFactory@1c92535: defining beans [org.springframework.context.annotation.internalConfigurationAnnotationProcessor,org.springframework.context.annotation.internalAutowiredAnnotationProcessor,org.springframework.context.annotation.internalRequiredAnnotationProcessor,org.springframework.context.annotation.internalCommonAnnotationProcessor,customerService,mongo,mongoTemplate,customerRepository,org.springframework.data.repository.core.support.RepositoryInterfaceAwareBeanPostProcessor#0]; root of factory hierarchy

ALL CUSTOMERS:
506d85b115503d4c92392c79 Mr Bank Customer
City
Personal Account

506d85b115503d4c92392c7a Mr Customer
Town
Business Account


SPECIFIC CUSTOMERS:
506d85b115503d4c92392c7a Mr Customer
Town
Business Account


This is a brief overview of Spring Data from MongoDB but there are many other facets to this project including MappingConverters, Compound Indexes and MVC support. For more info http://static.springsource.org/spring-data/data-mongodb/docs/current/reference/html/

9 comments:

  1. Hi Geraint,

    I am sure you know there another way to use Spring Data MongoDB. By using Spring Roo.

    I am in the process of developing a suite of showcases about it. I have some already up-an-running. If interested you can visit my developers blog @ http://pragmatikroo.blogspot.com/?view=timeslide
    , where I blog about them.

    Thank for bringing Morphia to my attention. Definitely, I am going to try it asap.

    Roogards
    jD

    ReplyDelete
    Replies
    1. Hi jD,

      Thanks for your comment. Yes, I'll get around to blogging about Spring Roo one day!

      Excellent blog by the way.

      cheers
      Geraint

      Delete
  2. Hi Geraint,

    Thanks for your wonderful post, it was very helpful, but I have a small question about "findByNameAndAddressNumberAndAccountsAccountName" method.

    Because in the above code instead of using this method, you have again used findAll() for getting specific customers.

    I tried similar method in my code, but it was not giving me proper results especially from the list.

    For e.g. instead of returning the document specific to accountName in Account class, it was returning the entire list.

    Any ideas??

    ReplyDelete
  3. Hi RK,

    Thanks for pointing that out. I've added a findSpecificCustomer method to the CustomerService which in turns calls the findBy method in the CustomerRepository interface.

    I've updated the Example class. Please try the updated code to see if you get the same output as attached to the end of the post.

    cheers
    Geraint

    ReplyDelete
    Replies
    1. Hi Geraint,

      Thanks for you quick response, I implemented the same, but in my case it is not working,

      My domain objects are,

      import java.util.List;
      import org.springframework.data.mongodb.core.mapping.Document;

      @Document
      public class PracticeQuestion {


      private int userId;
      private List questions;

      // Getters and setters

      }


      public class Question {

      private int questionID;
      private String type;

      // Getters and setters
      }


      My Repository method is,

      @Repository

      public interface PracticeQuestionRepository extends MongoRepository {


      List findByUserIdAndQuestionsQuestionID(int userId, int questionID);

      }


      Service layer code,

      public List getQuestions(int userId, int questionID){
      return practiceTestRepository.findByUserIdAndQuestionsQuestionID(userId, questionID);
      }


      So when i call this,

      List pq = practiceTestService.getQuestions(1,3);

      System.out.println(pq.get(0).getQuestions().get(0).getQuestionID());

      I m still getting the question id as 1 rather than 3.

      Where am I making mistake?

      TIA,

      RK

      Delete
    2. Hi RK,

      Difficult to know without seeing the whole codebase. (If you want to send it to me I can have a look.) How are you setting up the data?

      Having implemented classes based on what you've posted and then executing the below code, it seems to return 3 everytime.

      thanks
      Geraint

      PracticeTestService practiceTestService = context.getBean(PracticeTestService.class);

      // delete all Question records
      practiceTestService.dropPracticeQuestionCollection();

      Question question = new Question();
      question.setQuestionID(3);

      List<Question> questions = new ArrayList<Question>();
      questions.add(question);

      PracticeQuestion practiceQuestion = new PracticeQuestion();
      practiceQuestion.setUserId(1);
      practiceQuestion.setQuestions(questions);

      practiceTestService.insertPracticeQuestion(practiceQuestion);

      List<PracticeQuestion> pq = practiceTestService.getQuestions(1,3);

      System.out.println(pq.get(0).getQuestions().get(0).getQuestionID());

      Delete
    3. Hi Geraint,

      Thanks for spending time on this, but now I have figured out the issue.

      Scenario is like this, suppose if you add one more account to the second customer say,

      accounts = new ArrayList();
      account = new Account();
      account.setAccountName("Business Account");
      accounts.add(account);
      Account account1 = new Account();
      account1.setAccountName("Current Account");
      accounts.add(account1);

      And if we try to retrieve based on "Current Account" then you could get the "Business Account" first in the 0th index.

      System.out.println("\nSPECIFIC CUSTOMERS:");
      List specficCustomers = customerService.findSpecificCustomers(
      "Mr Customer", "101", "Current Account");
      for (Customer foundCustomer : specficCustomers) {
      System.out.println(foundCustomer.getId() + " " + foundCustomer.getName());
      System.out.println(foundCustomer.getAddress().getTown());
      System.out.println(foundCustomer.getAccounts().get(0).getAccountName() + "\n");
      }

      And output is,

      SPECIFIC CUSTOMERS:
      506dd28fe4b00a25efb1f387 Mr Customer
      Town
      Business Account

      Delete
    4. Hi RK,

      Nice one. Yes, the find will eagerly fetch the whole object graph so it'll return the child elements so the first element in the collection may not be one that meets the child element search criteria. Glad its resolved!

      All the best.

      Geraint

      Delete
  4. what if i want an image or video as a element of Customer document?
    appreciate a replay

    ReplyDelete

Note: only a member of this blog may post a comment.