Travel NoSQL Application - Polyglot NoSQL with SpringData on Neo4J and MongoDB

 In this Fuse day, Tikal Java group decided to continue its previous Fuse research for NoSQL, but this time from a different point of view – SpringData and Polyglot persistence. We had two goals in this Fuse day: try working with more than one NoSQL in the same application, and also taking advantage of SpringData data access abstractions for NoSQL databases. We decided to take MongoDB and Neo4J as document DB, and Neo4J as graph database and put them behind an existing, classic and well known application – Spring Travel Sample application.

 

Spring Travel Application

 

This is a kind of “hello world” application for Spring users, which demonstrates a very simple hotel bookings application, using Spring framework with JPA to access RDBMS. As said, our goal was to replace the RDBMS behind this app, with the two NoSQL above. Obviously this wasn't done for architecture reasons, since RDBMS can serve perfectly for this simple application. We merely wanted to understand, how hard is to migrate a simple Spring/JPA application to SpringData/NoSQL, and how hard is to use more than one NoSQL on the same project (while gaining some hand-on experience with these technologies).

 

We split our work into three phases:

 

Phase 1 – Integrate Neo4J to Travel Application

<dependency>
<groupId>org.springframework.data</groupId>
   <artifactId>spring-data-neo4j</artifactId>
   <version>2.1.0.RELEASE</version>
</dependency>
<dependency>			   
   <groupId>org.springframework.data</groupId>	  
   <artifactId>spring-data-neo4j-rest</artifactId>
   <version>2.1.0.RELEASE</version>
</dependency>

 

 

 

Next, we converted the simple domain objects to Graph model. In graph model you have two constructs : Nodes and Relationships. Both nodes and relationships can have properties, so there is somewhat less impedance mismatch to Object Oriented world , compare to RDBMS-to-OO one.

We converted the Hotel and User entities to graph Nodes and the Booking entity to Relationship typed “BOOKD”. We also changed the JPA @ID annotation to @GraphID and marked some of the properties as @Index. The @Index enables fast search by Neo4J for nodes by these properties - Behind the scenes Neo4J uses Lucene fast full text search index for that matter. Here are some code snippets from the the entities:

//@Entity
@NodeEntity
public class Hotel implements Serializable {
	@GraphId
	private Long id;
	@Indexed(indexType = IndexType.FULLTEXT, indexName = "search")
	private String name;
	...
}

 

//@Entity
//@Table(name = "Customer")
@NodeEntity
public class User implements Serializable {
	@GraphId
	private Long id;	
	@Indexed
	private String username;
	private String password;
	@RelatedToVia(type="BOOKED")
	List<Booking> bookings;
	…
}

 

//@Entity
@RelationshipEntity(type="BOOKED")
public class Booking implements Serializable {
	@GraphId
	private Long id;
	@StartNode
	private User user;
	@EndNode
	private Hotel hotel;
	private String creditCard;
	...
}

 

 

 

Next step, was to define SpringData Repositories. We created two interfaces: HotelRepository and UserRepository. Both interfaces extends SpringData's GraphRepority interface. We were happy for the no-need to implement the interfaces - They are been implemented automatically by SpringData using method naming conventions :)

Next, we had to integrate the new interfaces into a new BookingService implementation of the existing BookingService interface. This new implementation uses the new created repositories above, to implement the expected behavior. Thus , we kept the MVC layer and GUI as is.

Last step, was creating a small Spring configuration file, which defines the GraphDataService bean and our base package for the repositories interfaces above:

 

<beans...>	
	<context:spring-configured/>
	<context:annotation-config/>
	<context:component-scan base-package="org.springframework.samples.travel" />
	<neo4j:config graphDatabaseService="graphDatabaseService"/>
	<bean id="graphDatabaseService" 			 			   
                         class="org.springframework.data.neo4j.rest.SpringRestGraphDatabase">
		<constructor-arg value="http://localhost:7474/db/data/" index="0"/>
	</bean>
	<neo4j:repositories base-package="org.springframework.samples.travel"/>
</beans>

 

 

 

That's all for this phase – After running neo4j server, and deploying the application on Tomcat, we had the same Travel application – but this time working against Neo4J Graph DB.

 

Step 2 – Integrate MongoDB to Travel Application

In this phase we wanted to overload the Travel application with yet another NoSQL – MongoDB. MongoDB is very popular document datastore, which keeps its documents aggregated in one or more collections inside the DB. MongoDB has a flexible query language (much more flexible than other NoSQL). Nevertheless, for complex operations, like grouping results, you may need to write a map-reduce program in MongoDB, and this is exactly what we had to do here.

Again, we started with Maven dependencies artifacts, and we spent some time to understand, we must take the last dependencies in integrate also with Neo4J dependencies without clashes.

Then, we created a new domain object called BookingDoc, containing the booking-id and userId:

 

 

@Document
public class BookingDoc implements Serializable {
	@Id
	private String id;
	private Long userId;
}

 

 

As you can see, this domain class is annotated with @Document to denote its been persistent as a document in MongoDB, and @Id is the document id.

Next step, we created a BookingDocRepository interface, which extends SpringData's MongoRepository base interface. Again no implementation here – we have the auto-generated one from the SpringData on application startup :)

Here we have more complicated use case than in Neo4J: In addition to the basic inherited methods from MongoRepository, we wanted to create a custom implementation. This custom implementation method, could NOT be created automatically by naming conventions, as we saw before. The custom method should invoke a map reduce functionality inside the MongoDB. This map-reduce calculates the bookings count for a given user. For that, we had to create yet another interface, BookingDocCustomRepository:

 

public interface BookingDocCustomRepository {
	long countBookingsForUser(long userId);
}

 

 

This new custom interface is also a base interface for our BookingDocRepository as follow:

public interface BookingDocRepository 
                   extends MongoRepository<BookingDoc, Serializable>,BookingDocCustomRepository {
}

 

 

We created a new implementation repository BookingDocRepositoryImpl, which implements the map-reduce method to calculate the bookings count for the user. Thus, the result on runtime is a “mixed-in” implementation – Containing our custom implementation, and the SpringData generated code. Our custom implementation uses SpringData's MongoTemplate to invoke the map-reduce code in the DB:

public class BookingDocRepositoryImpl implements BookingDocCustomRepository{
	
	@Autowired
	private MongoTemplate mongoTemplate;

	@Override
	public long countBookingsForUser(long userId) {
		String mapFunc = String.format("function() {" +
							"if(this.userId == %d) " +
								"emit(this.userId, 1);" +
							"}",userId);
		String reduceFunc = "function(k,vals) {" +
					"var sum=0;"+
					"for(var i in vals) sum += vals[i];"+
					"return sum;"+
				"}";
		    
		MapReduceResults<BookingDoc> result = mongoTemplate.mapReduce("bookingDoc", mapFunc, reduceFunc, BookingDoc.class);
		BasicDBList list = (BasicDBList) result.getRawResults().get("results");
		if(list.isEmpty())
			return 0;
		DBObject object = (DBObject) list.get(0);
		Double d = (Double) object.get("value");
		return (long)d.doubleValue();
		
	}
}

 

 

As you can see, the map method emits tuples of userId and 1, for every booking associated with the input userId. On the other hand, the reduce method accepts the userId and the list of 1, and sum the 1 values. The reduce returns a list of tuples. Nevertheless, since we have only one user as the output of the map method, we have only one tuple in the list. The tuple's “value” is the booking count for given user.

Last, we added Spring configuration for MongoDB. This includes the DB configuration, MongoTemplate and our MongoDB's repositories base package:

<beans...>
	<context:annotation-config />
	<util:properties id="appConfig" location="classpath:META-INF/spring/application.properties"/>
	<context:property-placeholder properties-ref="appConfig"/>
	<mongo:mongo host="${app.config.mongo.host}" port="${app.config.mongo.port}" />
	<mongo:repositories base-package="org.springframework.samples.travel.mongo.repository" mongo-template-ref="mongoTemplate"/>
	<bean id="userCredentials" class="org.springframework.data.authentication.UserCredentials">
		<constructor-arg name="username" value="${app.config.mongo.user}" />
    	<constructor-arg name="password" value="${app.config.mongo.password}" />
	</bean>
	<bean id="mongoTemplate" class="org.springframework.data.mongodb.core.MongoTemplate">
		<constructor-arg ref="mongo" />
		<constructor-arg name="databaseName" value="${app.config.mongo.databasename}" />
		<constructor-arg ref="userCredentials" />
	</bean>
</beans>

 

 

Phase 3 – Put it all together

We updated the BookingService implementation code to save the BookingDoc to MongoDB right after saving the Booking to Neo4J, using our new repositories:

public Booking createBooking(Long hotelId, String username) {
		Hotel hotel = hotelRepository.findOne(hotelId);
		User user = userRepository.findByUsername(username);
		Booking booking = new Booking(hotel, user);
		neo4jOps.save(booking);
		bookingDocRepository.save(new BookingDoc(user.getId(), booking.getCreditCard()));
		return booking;
	}

 

 

We also added simple method that calculates the booking count in the service that delegates it to our repository:

@Override
	public long countBookingsForUser(long userId){
		return bookingDocRepository.countBookingsForUser(userId);
	}

 

 

That's it – you have a running Spring travel application using two NoSQL storages with SpringData.

 

Conclusion

We had much pleasure playing with NoSQL and SpringData in this Tikal Fuse day. We faced the nice SpringData features, neglecting the old “GenericDao” pattern. We were also happy to drop DAO implementations classes (typically dozens empty useless in a regular applications), and use SpringData's “query by convention” and annotations.

We have the confidence now,we can really integrate more than one NoSQL in the same project with SpringData, by making sure the right Maven dependencies, making it as a Polyglot Persistence application. You can download the Travel NoSQL application from Tikal GitHub.

 

 

 

 

 

 

 

 

 

Thank you for your interest!

We will contact you as soon as possible.

Send us a message

Oops, something went wrong
Please try again or contact us by email at info@tikalk.com