Using the spring-data project and the mongodb adapter specifically
In this blog post I want to share some of my experiences with the spring-data project. At Orange11 we have been working on a sample for the Axon Framework. We have created the Axon Trader. For the trader we use the MongoDB based event store. Therefore I also wanted to store the data for the query side in Mongodb. I was looking for the easiest way to store and retrieve documents from a mongodb instance. I decided to pick the spring-data mongodb library and I must admit that I do not regret that choice.
Background
The Axon framework is the CQRS framework for java. CQRS is an architectural pattern that tells you to separate the commands that modify data from the queries that retrieve data. Of course this is not the most extensive explanation, refer to the Axon framework documentation for more information.
With the trader we wanted to create a sample application to demonstrate the possibilities with the Axon framework. One of the things we wanted to show is the integration with Mongodb. We have created an mongodb based event store and we are working on a native serializer to store the events in Mongodb in a native format. This way we have covered the Command part for the application using the standard Axon framework infrastructure.
Since we have Mongodb we also want the query side to use Mongodb. Of course I want to keep it as simple as possible. One of the characteristics of CQRS is that we query side is screen or functionality based. So very easy queries, not to many joins and groupBy structures. This is ideal for the spring-data project.
Introducing spring-data mongodb
Spring data is a project that makes it easier to connect from a spring based application to multiple data storage engines. Implementation exist for JPA, Redis, Neo4j, MongoDB and others. The idea is that you can use very generic interfaces and you do not have to implement the methods themselves. Just add a method to the interface by using some conventions and the implementations are generated on the fly.
You can find more information about spring-data mongodb at the following location
http://www.springsource.org/spring-data/mongodb
http://static.springsource.org/spring-data/data-mongodb/docs/current/reference/html/
In short, spring-data provides a blueprint for a repository class. You just have to create JavaBeans representing the stored information and special Repository classes. The repository classes are interfaces that extend spring-data provided interfaces. Next to extending them we make them type safe using the generics of the provided interface. We make our repository interfaces extend the PagingAndSortingRepository. This repository extends the CrudRepository. Together they provide crud methods and the basis for paginated query methods. More on these specific methods will follow in the next chapters. But before we go there, first we are going to discuss the infrastructure of dealing with Mongo in the trader.
Mongo configuration in Spring
One of the requirements for the sample is that it should work in a local running tomcat instance. It should also work on something like CloudFoundry. We make use of the new Spring profile feature to make the switch. In the persistence-infrastructure-context.xml I have the following beans:
<beans profile="default"> <mongo:db-factory id="mongoDbFactory" dbname="axontrader" host="127.0.0.1" port="27017"/> </beans> <beans profile="cloud"> <cloud:mongo-db-factory id="mongoDbFactory"/> </beans>
The trick is in determining which profile to use. That can be found in the following class implementing the ApplicationContextInitializer.
public class CloudApplicationContextInitializer implements ApplicationContextInitializer<ConfigurableApplicationContext> { private final static Logger logger = LoggerFactory.getLogger(CloudApplicationContextInitializer.class); @Override public void initialize(ConfigurableApplicationContext applicationContext) { CloudEnvironment env = new CloudEnvironment(); if (env.getInstanceInfo() != null) { logger.info("Cloud API: {}", env.getCloudApiUri()); applicationContext.getEnvironment().setActiveProfiles("cloud"); } else { logger.info("Activating the default profile within the application context."); applicationContext.getEnvironment().setActiveProfiles("default"); } } }
We check if we have a cloud environment, if not we set the default profile. If a cloud is available, we use the cloud profile.
All the query classes are located in the query module of the Trader. The package structure is important, this is used in the spring configuration to locate all repositories.
<mongo:repositories base-package="**.query.*.repositories" mongo-template-ref="mongoSpringTemplate"/>
Querying for data
Data is obtained in the form of JavaBeans. Therefore we create beans reflecting what we need on the query side. An important annotation to remember is the @Id annotation. Using this annotation you can specify the field that is used in Mongo as the identifier. This is not required for mongo, but it makes life easier. The next code block shows you all the code we need to interact with mongodb from the query side perspective.
public interface PortfolioQueryRepository extends PagingAndSortingRepository<PortfolioEntry, String> { PortfolioEntry findByUserIdentifier(String userIdentifier); }
The basic queries are provided by Spring-data: findAll, findOne and exists. Next to those basic queries we have findAll with a Sort and one that supports pagination. The really cool part is that we can specify our own queries. An example is contained in the PortfolioEntryRepository, this contains the method findOneByUserIdentifier. Spring-data understands this method and creates the query for us that finds a PortfolioEntry based on the provided UserIdentifier.
Storing data
Good that we have an easy way of obtaining data or documents from Mongodb. But how did this data get there? Of course we need to store data as well. We use the EventListener mechanism of Axon to get notified in case of changes. The most basic listener that we have is the COmpanyEventListener. This has one event to lister for, the CompanyCreatedEvent. When this event is received, we use the CompanyEntryRepository to store a new company. The following code block shows the code. As you can see it is very straightforward. All you need to do is create a JavaBean and use the save method on the Repository implementation.
@Component public class CompanyListener { private CompanyQueryRepository companyRepository; @EventHandler public void handleCompanyCreatedEvent(CompanyCreatedEvent event) { CompanyEntry companyEntry = new CompanyEntry(); companyEntry.setIdentifier(event.getCompanyIdentifier().asString()); companyEntry.setValue(event.getCompanyValue()); companyEntry.setAmountOfShares(event.getAmountOfShares()); companyEntry.setTradeStarted(true); companyEntry.setName(event.getCompanyName()); companyRepository.save(companyEntry); } @Autowired public void setCompanyRepository(CompanyQueryRepository companyRepository) { this.companyRepository = companyRepository; } }
Other methods are available that help you to save multiple objects at once and to remove objects. This is really all there is to it.
Utilities
One of the other important spring components that we use id the MongoTemplate class. If you are used to spring you have probably met classes like: JdbcTemplate and JmsTemplate. The goal is equal for the MongoTemplate class. Make it easier to interact with Mongo. I used some of the methods in the MongoTemplate to create a data viewer on the Mongodb.
Within the Trader you can go to /data/collections. You’ll be presented with a list of available collections in the current database. The following code block shows how you can do it.
@RequestMapping(value = "/collections", method = RequestMethod.GET) public String collections(Model model) { Set<String> collectionNames = springTemplate.getCollectionNames(); model.addAttribute("collections", collectionNames); return "data/collections"; }
The most important part is the springTemplate.getCollectionNames(). This method is used to get the names of all collections (really!). The next step is to browse the data of one collection. We make use of the pagination feature that is available. The following code block shows how this works.
@RequestMapping(value = "/collection/{id}", method = RequestMethod.GET) public String collection(@PathVariable("id") String collectionName, @RequestParam(value = "page", defaultValue = "1") int pageNumber, @RequestParam(value = "itemsperpage", defaultValue = "5") int itemsPerPage, Model model) { DBCursor dbCursor = springTemplate.getCollection(collectionName).find(); List<DBObject> dbObjects = dbCursor.skip((pageNumber - 1) * itemsPerPage).limit(itemsPerPage).toArray(); List<Map> items = new ArrayList<Map>(dbCursor.length()); for (DBObject dbObject : dbObjects) { items.add(dbObject.toMap()); } model.addAttribute("items", items); int totalItems = dbCursor.count(); int numPages = ((int) Math.floor(totalItems / itemsPerPage)) + 1; model.addAttribute("numPages", numPages); model.addAttribute("page", pageNumber); model.addAttribute("itemsPerPage", itemsPerPage); model.addAttribute("collectionName", collectionName); return "data/collection"; }
Concluding and next steps
That is about it, this was what I could tell. Of course more is possible check the mentioned reference manual. To my opinion the amount of code required to store and retrieve data from the mongodb store is limited to a few lines of code using the mongodb spring-data project.
In the future we want to make the integration with more more tight than it is today. We want to help out more with indexes, a better Mongodb serialization technique and a better mapping of our problem to the documents that are the basis for the Mongodb store.
I hope this article was useful, see you next time.