Sunday, August 7, 2011

Exploiting MongoDB together with Spring Data project: advanced concepts

In previous post we had started discussion about MongoDB and Spring Data projects. In this post I would like to show some advanced features (which could be available in next Spring Data milestone or release as part of core functionality).

First of all, let us extend our MongoService with a method that counts documents in collection which match specific query.
package com.example.mongodb;

import java.util.Arrays;
import java.util.Collection;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.dao.DataAccessException;
import org.springframework.data.document.mongodb.CollectionCallback;
import org.springframework.data.document.mongodb.MongoOperations;
import org.springframework.data.document.mongodb.convert.MongoConverter;
import org.springframework.data.document.mongodb.query.Criteria;
import org.springframework.data.document.mongodb.query.Index;
import org.springframework.data.document.mongodb.query.Index.Duplicates;
import org.springframework.data.document.mongodb.query.Order;
import org.springframework.data.document.mongodb.query.Query;
import org.springframework.stereotype.Service;
import org.springframework.util.Assert;

import com.mongodb.BasicDBObject;
import com.mongodb.DBCollection;
import com.mongodb.MongoException;

@Service
public class MongoService {
    public long countDocuments( final String collection, final Query query ) {  
        return template.executeCommand( 
            "{ " +
                "\"count\" : \"" + collection + "\"," +
                "\"query\" : " + query.getQueryObject().toString() + 
            " }"  ).getLong( "n" );
    }
}
The approach for this particular functionality is to call native MongoDB command count passing the query as a parameter. The returning structure contains number of documents in n property.

Or, in more code-friendly way:
import org.springframework.dao.DataAccessException;
import org.springframework.data.document.mongodb.CollectionCallback;

import com.mongodb.DBCollection;
import com.mongodb.MongoException;

public long countDocuments( final String collection, final Query query ) {  
    return template.execute( collection,
        new CollectionCallback< Long >() {
            @Override
            public Long doInCollection( DBCollection collection ) 
                    throws MongoException, DataAccessException {
                return collection.count( q.getQueryObject() ) );
            }
        }
    );
}

Next useful feature is bulk inserts. Please note, that in current version of MongoDB 1.8.1, when there is a duplicate inside the collection of inserting documents, bulk insert stops on first duplicate and returns so all other documents won't be inserted. Be aware of such behavior. Before moving to code snippet, let me introduce simple class SimpleDocument which we will be persisting to MongoDB:
package com.example.mongodb;

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

@Document( collection = "documents" )
public class SimpleDocument {
    private String id;
    private String name;
    private String content;
 
    public SimpleDocument() { 
    }
 
    public SimpleDocument( final String id, final String name ) {
        this.id = id;
        this.name = name;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getId() {
        return id;
    }

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

    public String getContent() {
        return content;
    }

    public void setContent(String content) {
        this.content = content;
    }
}
Following method inserts all documents as single bulk update:
public void insert( final Collection< SimpleDocument > documents ) {  
    template.insert( documents, SimpleDocument.class );     
}
Another very cool and useful feature to explore is MongoDB's upserts (more about this here http://www.mongodb.org/display/DOCS/Updating): if document matching specific criteria exists, it will be updated, otherwise - new document will be inserted into collection. Here is a code snipped with demonstrates it by following use case: if SimpleDocument with such name exists, it will be updated, otherwise new document will be added to collection:
@Autowired private MongoConverter converter;

public void insertOrUpdate( final SimpleDocument document ) {
    final BasicDBObject dbDoc = new BasicDBObject();
    converter.write( document, dbDoc );
  
    template.execute( SimpleDocument.class, 
        new CollectionCallback< Object >() {
            public Object doInCollection( DBCollection collection ) 
                    throws MongoException, DataAccessException {
                collection.update( 
                    new Query()
                        .addCriteria( new Criteria( "name" ).is( document.getName() ) )
                        .getQueryObject(), 
                    dbDoc,  
                    true, 
                    false 
                );
     
                return null;
            }
        }
    );
}
Please notice usage of converter bean which helps to convert Java class to MongoDB's DBObject.

The last one I would like to show is findAndModify operation which does several things as one atomic sequence:
- find document matching criteria
- perform update
- return updated document (or old one, depending on what are your needs)
public void findAndModify( final Query query, final Update update ) {
    return template.execute( SimpleDocument.class,
        new CollectionCallback< SimpleDocument >() {
            @Override
            public SimpleDocument doInCollection( DBCollection collection ) 
                    throws MongoException, DataAccessException {
                return converter.read( SimpleDocument.class,       
                    collection.findAndModify( 
                        query.getQueryObject(), 
                        null,
                        null,
                        false,
                        update.getUpdateObject(),
                        true,
                        false
                    ) 
                );
            }
        }			
    );
}



For now, those are all interesting use cases I encountered. Honestly, I am very excited about MongoDB and strongly recommend it if it fits your application.