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.
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:
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.
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:
A Java Config is set up that wires up the DAO's
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.
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:
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
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!
Enjoy!
1 comment:
I want to create BaseMongoDAO Support class without parametrized constructor that how can I populate metadata and mongoperations ?So that I can just extends this support class and I use the Functionality
Post a Comment