Search This Blog

Friday, June 29, 2012

Spring Data MongoDB Example

Sometime ago I had Blogged about using Morphia with Mongo DB. Since then I have come across the Spring Data project and wanted to take their API for Mongo  on a ride. So this BLOG is duplicating the functionality of what was present in the Morphia one with the difference that it uses Spring Data and demonstrates Mongo Map-Reduce as well. As most of my recent Blogs that use Spring, I am going to be using a pure JavaConfig approach to the example.

 1. Setting up Spring Mongo 


The Spring API provides an abstract Spring Java Config class, org.springframework.data.mongodb.config.AbstractMongoConfiguration. This class requires the following methods to be implemented, getDatabaseName() and mongo() which returns a Mongo instance. The class also has a method to create a MongoTemplate. Extending the mentioned class, the following is a Mongo Config:
@Configuration
@PropertySource("classpath:/mongo.properties")
public class MongoConfig extends AbstractMongoConfiguration {
 
  private Environment env;  
 
  @Autowired
  public void setEnvironment(Environment environment) {
    this.env = environment;
  }

  @Bean
  public Mongo mongo() throws UnknownHostException, MongoException {
    // create a new Mongo instance
    return new Mongo(env.getProperty("host"));
  }

  @Override
  public String getDatabaseName() {
    return env.getProperty("databaseName");
  }
}

2. Model Objects and Annotations 


As per my former example, we have four primary objects that comprise our domain. A Product in the system such as an XBOX, WII, PS3 etc. A Customer who purchases items by creating an Order. An Order has references to LineItem(s) which in turn have a quantity and a reference to a Product for that line.

2.1 The Order model object looks like the following:

// @Document to indicate the orders collection
@Document(collection = "orders")
public class Order {
  // Identifier
  @Id
  private ObjectId id;

  // DB Reference to a Customer. This is a Link to a Customer from the Customer collection
  @DBRef
  private Customer customer;

  // Line items are part of the Order and do not exist independently of the order
  private List<LineItem> lines;
 ...
}
The identifier of a POJO can be ObjectId, String or BigInteger. Note that Orders is its own rightful mongo collection however, as LineItems do not exist without the context of an order, they are embedded. A Customer however might be associated with multiple orders and thus the @DBRef annotation is used to link to a Customer.

3. Implementing the DAO pattern 


One can use the Mongo Template directly or extend or compose a DAO class that provides standard CRUD operations. I have chosen the extension route for this example. The Spring Mongo API provides an interface org.springframework.data.repository.CrudRepository that defines methods as indicated by the name for CRUD operations. An extention to this interface is the org.springframework.data.repository.PagingAndSortingRepository which provides methods for paginated access to the data. One implementation of these interfaces is the SimpleMongoRepository which the DAO implementations in this example extend: 
// OrderDao interface exposing only certain operations via the API
public interface OrderDao {
  Order save(Order order);

  Order find(ObjectId orderId);

  List<Order> findOrdersByCustomer(Customer customer);

  List<Order> findOrdersWithProduct(Product product);
}

public class OrderDaoImpl extends SimpleMongoRepository<Order, ObjectId> implements OrderDao {

  public OrderDaoImpl(MongoRepositoryFactory factory, MongoTemplate template) {
    super(new MongoRepositoryFactory(template).<Order, ObjectId>getEntityInformation(Order.class), template);
  }

  @Override
  public List<Order> findOrdersByCustomer(Customer customer) {
    // Create a Query and execute the same
    Query query = Query.query(Criteria.where("customer").is(customer));

    // Note the equivalent of Hibernate where one would do getHibernateTemplate()...
    return getMongoOperations().find(query, Order.class);
  }

  @Override
  public List<Order> findOrdersWithProduct(Product product) {
   // Where the lines matches the provided product
    Query query = Query.query(Criteria.where("lines.product.$id").is(product));
    return getMongoOperations().find(query, Order.class);
  }
}
One of the quirks that I found is that I was not able to use Criteria.where("lines.product").is(product) but had to instead resort to using the $id. I believe this is a BUG and will be fixed. Another peculiarity I found between Mongo 1.0.2.RELEASE and the milestone of 1.1.0.M1 was in the save() method of SimpleMongoRepository:
//1.0.2.RELEASE
public <T> T save(T entity) {
}

// 1.1.0.M1
public <S extends T> S save(S entity) {
}
Although the above will not cause a Runtime error upon upgrading due to erasure, it will force a user to have to override the save() or similar methods during compile time. If upgrading from 1.0.2.RELEASE to 1.1.0.M1, you will have to add the following to the OrderDaoImpl in order for it to compile:
@Override  
@SuppressWarnings("unchecked")
public Order save(Order order) {
   return super.save(order);
}

4. Configuration for the DAO's


A Java Config is set up that wires up the DAO's
@Configuration
@Import(MongoConfig.class)
public class DaoConfig {
  @Autowired
  private MongoConfig mongoConfig;

  @Bean
  public MongoRepositoryFactory getMongoRepositoryFactory() {
    try {
      return new MongoRepositoryFactory(mongoConfig.mongoTemplate());
    }
    catch (Exception e) {
      throw new RuntimeException("error creating mongo repository factory", e);
    }
  }

  @Bean
  public OrderDao getOrderDao() {
    try {
      return new OrderDaoImpl(getMongoRepositoryFactory(), mongoConfig.mongoTemplate());
    }
    catch (Exception e) {
      throw new RuntimeException("error creating OrderDao", e);
    }
  }
  ...
}

5. Life Cycle Event Listening 


The Order object has the following two properties, createDate and lastUpdate date which are updated prior to persisting the object. To listen for life cycle events, an implemenation of the org.springframework.data.mongodb.core.mapping.event.AbstractMongoEventListener can be provided that defines methods for life cycle listening. In the example provide we override the onBeforeConvert() method to set the create and lastUpdateDate properties.
public class OrderSaveListener extends AbstractMongoEventListener<Order> {
  /**
   * This method is responsible for any code before updating to the database object.
   */
  @Override
  public void onBeforeConvert(Order order) {
    order.setCreationDate(order.getCreationDate() == null ? new Date() : order.getCreationDate());
    order.setLastUpdateDate(order.getLastUpdateDate() == null ? order.getCreationDate() : new Date());
  }
}

6. Indexing 


The Spring Data API for Mongo has support for Indexing and ensuring the presence of indices as well. An index can be created using the MongoTemplate via:
mongoTemplate.ensureIndex(new Index().on("lastName",Order.ASCENDING), Customer.class);

7. JPA Cross Domain or Polyglot Persistence


If you wish to re-use your JPA objects to persist to Mongo, then take a look at the following article for further information about the same.
 http://www.littlelostmanuals.com/2011/10/example-cross-store-with-mongodb-and.html

8. Map Reduce


The MongoTemplate supports common map reduce operations. I am leaning on the basic example from the Spring Data site and enhancing it to work with the comments example I have used in all my M/R examples in the past. A collection is created for Comments and it contains data like:
{ "_id" : ObjectId("4e5ff893c0277826074ec533"), "commenterId" : "jamesbond", "comment":"James Bond lives in a cave", "country" : "INDIA"] }
{ "_id" : ObjectId("4e5ff893c0277826074ec535"), "commenterId" : "nemesis", "comment":"Bond uses Walther PPK", "country" : "RUSSIA"] }
{ "_id" : ObjectId("4e2ff893c0277826074ec534"), "commenterId" : "ninja", "comment":"Roger Rabit wanted to be on Geico", "country" : "RUSSIA"] }

The map reduce works of JSON files for the mapping and reducing functions. For the mapping function we have mapComments.js which only maps certain words:
function () {
   var searchingFor = new Array("james", "2012", "cave", "walther", "bond");
   var commentSplit = this.comment.split(" ");
 
    for (var i = 0; i < commentSplit.length; i++) {
      for (var j = 0; j < searchingFor.length; j++) {
        if (commentSplit[i].toLowerCase() == searchingFor[j]) {
          emit(commentSplit[i], 1);
        }
      }
    }
}
For the reduce operation, another javascript file reduce.js:
function (key, values) {
    var sum = 0;
    for (var i = 0; i < values.length; i++) {
        sum += values[i];
    }
    return sum;
}
The mapComment.js and the reduce.js are made available in the classpath and the M/R operation is invoked as shown below:
public List<ValueObject> mapReduce() {
    MapReduceResults<ValueObject> results =  getMongoOperations().mapReduce("comments", "classpath:mapComment.js" , "classpath:reduce.js", ValueObject.class);

    return Lists.<ValueObject>newArrayList(results);    
}
Upon executing the map reduce, one would see results like:
ValueObject [id=2012, value=119.0]
ValueObject [id=Bond, value=258.0]
ValueObject [id=James, value=241.0]
ValueObject [id=Walther, value=134.0]
ValueObject [id=bond, value=117.0]
ValueObject [id=cave, value=381.0]


Conclusion

As always, the Spring folks keep impressing me with their API. Even with the change to their API, they preserved binary backward compatibility thus making an upgrade easy. The MongoTemplate supports common M/R operations, sweet! I have not customized the M/R code to my liking but its only a demo after all.
I quite liked the API, it is intuitive and easy to learn. I clearly have not explored all the options but then I am not really using Mongo at work to do the same ;-)

Example


Download the example from here. It is a maven project that you can either import into Eclipse or simply run mvn test from the command line to see the simple unit tests in action. The tests themselves make use of an embedded mongo instance courtesy of https://github.com/michaelmosmann/embedmongo.flapdoodle.de.
Enjoy!