Open source Java projects: Spring Data

Use common Spring queries to access multiple NoSQL data stores

Spring Data provides the boilerplate code and plumbing to enable you to interact with various NoSQL repositories in a Spring-consistent manner. Depending on your needs, you could even find the persistence logic for your entire application defined in a handful of Spring Data interfaces. Get started with Spring Data domain objects and repositories, then learn about two ways to implement Spring queries in Spring Data: by naming convention or using QueryDSL, which ensures type-safe queries that are validated at compile time.

Reflect back on all the persistence code that you've written for Java applications. Given the requirements, can you say that you know every line of code that you need to write? If your answer is yes, and you're just putting off writing the code, then I believe you're better off automating that process. In fact, years ago I built a Maven plug-in (see Resources) that processed a class diagram in XMI format and generated a domain model. The domain model was annotated with Hibernate annotations and included a set of persistence classes. When it came to building a MongoDB persistence strategy I almost fell back on that approach -- but that was before I discovered Spring Data.

Spring Data and Neo4j

One thing that the Spring framework has done very well is to reduce or eliminate boilerplate code. Instead of spending time on generic implementation code, Spring developers spend more time on business logic. Spring provides the configuration and plumbing code.

At SpringOne in 2010, Spring creator Rod Johnson and Neo4j co-founder Emil Eifrem were trying to determine the best way to integrate support for Neo4J into the Spring framework. Their collaboration eventually led to the Neo4J module for Spring Data. Thus Spring Data evolved into an abstraction layer on top of data repositories. Initially focused on NoSQL, it was later extended for JPA and relational databases.

Get started with Spring

If you're unfamiliar with Spring's programming model, start here with Steve's introductory tutorial, "Mastering Spring MVC" (JavaWorld, April 2009).

It's good to keep Spring Data's inception in mind, because NoSQL repositories are quite different from relational databases. Relational databases provide support for SQL queries and solve a specific category of problems, which makes them easy to abstract. But NoSQL repositories specialize in particular features and functionality. MongoDB, for instance, is good at managing dynamic documents; Neo4J is good at representing highly interconnected graphed entities; and Redis is good at representing key/value entries in a cache.

Each NoSQL repository addresses a different problem domain, and each problem domain prefers a specific query style. If Spring Data were to simply abstract all NoSQL repositories you would lose the unique benefits of the individual repositories. So instead Spring Data provides a consistent programming model for interacting with NoSQL repositories, using patterns and models from the Spring framework. As a result you get a consistent way of interacting with different NoSQL repositories, and you're able to leverage each one's individual strengths.

This Open source Java projects profile walks through an example implementation using Spring Data to manage domain-model persistence to MongoDB. I'll start with an overview of state-of-the-art persistence before Spring Data, then we'll dig into Spring Data's architecture, especially its handling of repositories and domain objects. Finally, I'll show you a couple of ways to write queries against your Spring Data domain objects. We'll conclude with a JUnit test case that demonstrates how to use Spring Data to persist data to and from a running MongoDB instance.

MongoDB data persistence without Spring Data

If you're not using Spring Data, then you are at the disposal of a NoSQL data provider's API. For MongoDB, the persistence code would look like what you see in Listing 1.

Listing 1. Accessing MongoDB using the MongoDB API

 // Create a Mongo instance
    Mongo mongo = new Mongo( "localhost" );

    // Get a DB instance to the "informit" database
    DB db = mongo.getDB( "test" );

        // Show all collections in the database
        Set collectionNames = db.getCollectionNames();
        for( String collection : collectionNames )
        {
            System.out.println( "Collection: " + collection );
        }
        Assert.assertNotNull( "Could not obtain a database connection," db );
        
        // Create a new collection
        DBCollection collection = db.createCollection( "users," null );

        // Add something to the collection (simple)
        BasicDBObject doc = new BasicDBObject();
        doc.put( "firstName," "Steven" );
        doc.put( "lastName," "Haines" );
        doc.put( "age," 39 );
        collection.insert( doc );

        // Add another record using a builder
        BasicDBObjectBuilder builder = BasicDBObjectBuilder.start();
    builder.add( "firstName" ,"Michael" );
    builder.add( "lastName" ,"Haines" );
    builder.add( "age," 9 );
        collection.insert( builder.get() );

        // Find all objects
        DBCursor cursor = collection.find();
        while( cursor.hasNext() )
        {
            System.out.println( cursor.next() );
        }

        // Find users with the last name Haines
        builder = BasicDBObjectBuilder.start();
        builder.add( "lastName," "Haines" );
        cursor = collection.find( builder.get() );
        System.out.println( "Users with last name of Haines: " );
        while( cursor.hasNext() )
        {
            System.out.println( cursor.next() );
        }

        // Find users under 10
        builder = BasicDBObjectBuilder.start();
        builder.add( "age," new BasicDBObject( "$lt," 10 ) );
        cursor = collection.find( builder.get() );
        System.out.println( "Find all documents with an age less than 10: " );
        while( cursor.hasNext() )
        {
            System.out.println( cursor.next() );
        }

        // Clean up
        builder = BasicDBObjectBuilder.start();
        builder.add( "lastName," "Haines" );
        collection.remove( builder.get() );

        // Find our objects
        System.out.println( "After delete:" );
        cursor = collection.find();
        while( cursor.hasNext() )
        {
            System.out.println( cursor.next() );
        }

Note: The code in Listing 1 originally appeared in the March 2003 InformIT article "Accessing MongoDB from Java."

In this type of persistence implementation you would first create a Mongo instance to connect to the required host and port and then obtain a DB object referencing the instance that you wanted to interact with. The DB would enable you to access collections, which are similar to SQL database tables. You would then create a new collection named users, insert a couple of users, and then exercise some basic query operations. Invoking find(), for instance, would return a cursor referencing all the objects in the collection.

You could refine your queries by configuring a BasicDBObjectBuilder with additional criteria, such as the last name of all users in the database. Upon retrieving a BasicDBObject from the builder you could pass it to the find() method in order to only receive users with a specified last name (like "Haines" in Listing 1). Finally, calling remove() would delete the recently added users from the collection.

The code in Listing 1 isn't necessarily complex, but after writing it I decided to compile it all together into a MongoDB template class -- a project that you can read about here. Spring Data did not exist at the time, but Spring provides several implementations of the Template design pattern, such as JdbcTemplate and RestTemplate, which is now modeled by the new Spring Data MongoTemplate. I have found templates far easier to use than native APIs, so it was a natural progression to using a MongoDB template, as shown in Listing 2.

Listing 2. Using a custom MongoDBTemplate

 public void addUser( final User user )
    {
        // Delegate the insertion operation to the MongoDBTemplate
        template.insert( "users", new MongoDBDBObjectBuilder() {
            @Override
            public void build( BasicDBObjectBuilder builder ) {
                builder.add( "firstName", user.getFirstName() );
                builder.add( "lastName", user.getLastName() );
                builder.add( "age", user.getAge() );
            }
        });
    }

    @Override
    public List<User> findAll()
    {
        return template.findAll( "users", userBuilder );
    }

    @Override
    public List<User> findByLastName( final String lastName )
    {
        return template.find( "users",
            new MongoDBQueryCriteriaBuilder() {
                @Override
                public void build( BasicDBObjectBuilder builder ) {
                    // Add the last name criteria
                    builder.add( "lastName", lastName );
                }
            },
            userBuilder
        );
    }
    
    @Override
    public List<User> findByAgeGreaterThan( final int age )
    {
        return template.find( "users",
            new MongoDBQueryCriteriaBuilder() {
                @Override
                public void build( BasicDBObjectBuilder builder ) {
                    // Add the age criteria
                    builder.add( "age", new BasicDBObject( "$gt", age ) );
                }
            },
            userBuilder
        );
    }  

The template code in Listing 2 greatly simplifies the manual implementation from Listing 1, but it still contains boilerplate code that would be better handled by a framework like Spring Data. Next we'll look at some Spring Data repositories that simplify NoSQL and MongoDB access.

Building a Spring Data repository

The repository is a core concept in Spring Data. If you've written code to access a database through a persistence library like Hibernate, then the idea of a repository should be nothing new. But I think you'll find using a Spring Data repository far simpler than using a standard Spring repository.

The process of building a Spring Data repository is as follows:

  1. Define the domain object you want to persist
  2. Extend a Spring Data interface with one that contains your persistence methods
  3. Configure the Spring framework to scan your source code, find repositories, and generate persistence code
  4. Wire the repository into the class that will use it

We'll start with defining the domain object and extending Spring Data interfaces to create two different repositories, then we'll move on to configuring queries in Spring. Finally, in the example application at the end, we'll configure Spring and wire the repositories into a Spring service.

Define a domain object

Domain objects are simple POJOs that expose fields to be persisted to and from the data store. In Listing 3, I've annotated my domain object with the org.springframework.data.mongodb.core.mapping.Document annotation, but aside from the annotation what you're looking at is just a JavaBean.

Listing 3. Domain object: User.java

package com.geekcap.javaworld.springdata.model;

import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.mapping.DBRef;
import org.springframework.data.mongodb.core.mapping.Document;

import java.util.List;

/**
 * Represents a user in the application
 */
@Document
public class User
{
    @Id
    private String id;
    private String firstName;
    private String lastName;
    private String emailAddress;
    private int age;

    @DBRef
    private List<Address> addresses;

    public User() {
    }

    public User(String firstName, String lastName, String emailAddress) {
        this.firstName = firstName;
        this.lastName = lastName;
        this.emailAddress = emailAddress;
    }

    public String getId() {
        return id;
    }

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

    public String getFirstName() {
        return firstName;
    }

    public void setFirstName(String firstName) {
        this.firstName = firstName;
    }

    public String getLastName() {
        return lastName;
    }

    public void setLastName(String lastName) {
        this.lastName = lastName;
    }

    public String getEmailAddress() {
        return emailAddress;
    }

    public void setEmailAddress(String emailAddress) {
        this.emailAddress = emailAddress;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public List<Address> getAddresses() {
        return addresses;
    }

    public void setAddresses(List<Address> addresses) {
        this.addresses = addresses;
    }
}

As shown in Listing 3, Spring Data's domain objects are POJOs that contain a set of persistent fields. Note that the id is annotated with the @Id annotation, which is specific to MongoDB, and the Address list is annotated by the @DBRef annotation. The @DBRef annotation creates a field in the User collection named addresses, which contains an array of DBRef instances.

After inserting a User into MongoDB, the user, with the address reference, would look like so:

{
    "_id" : ObjectId("522ce1be03640e6bb97d920b"),
    "_class" : "com.geekcap.javaworld.springdata.model.User",
    "firstName" : "Steven",
    "lastName" : "Haines",
    "emailAddress" : "steve@geekcap.com",
    "age" : 41,
    "addresses" : [ DBRef("address", ObjectId("522ce1bd03640e6bb97d920a")) ]
}       

The addresses field in the MongoDB response tells Spring Data that the referenced address can be found in the address collection with the specified ObjectId. Note that if you have data that you want to associate with the domain object, but you don't want it persisted to the data store, you can annotate the field with org.springframework.data.annotation.Transient.

Listing 4 shows the source code for the Address class.

Listing 4. Address.java

package com.geekcap.javaworld.springdata.model;

import org.springframework.data.annotation.Id;

/**
 * A user's address
 */
public class Address
{
    @Id
    private String id;
    private String street1;
    private String street2;
    private String city;
    private String state;
    private String zipcode;

    public Address() {
    }

    public Address(String street1, String street2, String city, String state, String zipcode) {
        this.street1 = street1;
        this.street2 = street2;
        this.city = city;
        this.state = state;
        this.zipcode = zipcode;
    }

    public String getId() {
        return id;
    }

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

    public String getStreet1() {
        return street1;
    }

    public void setStreet1(String street1) {
        this.street1 = street1;
    }

    public String getStreet2() {
        return street2;
    }

    public void setStreet2(String street2) {
        this.street2 = street2;
    }

    public String getCity() {
        return city;
    }

    public void setCity(String city) {
        this.city = city;
    }

    public String getState() {
        return state;
    }

    public void setState(String state) {
        this.state = state;
    }

    public String getZipcode() {
        return zipcode;
    }

    public void setZipcode(String zipcode) {
        this.zipcode = zipcode;
    }
}

Define a Spring Data repository

The next step is to define a repository, which we can do by implementing one of the following Spring Data Repository interfaces:

  • Repository: The base interface that all repositories must extend. This is a marker interface that identifies your interface as a Spring Data repository. It does not provide any generated method implementations. You are able to define methods like findByName() and findByCityAndState() and Spring Data is smart enough to figure out what you are looking for (such as a user's name in the first method and the user's city and state in the second method) and generate implementations of each method.
  • CrudRepository: Extends Repository and comes complete with CRUD functionality. By extending this interface you'll automatically pick up methods like findOne(ID), findAll(), save(), delete(), and deleteAll() and Spring Data will generate implementations of each method for you.
  • PagingAndSortingRepository: Extends the CrudRepository and adds support for paging and sorting results. If you have a lot of results in your query, you can create a Pageable instance that defines a page size and a page number and Spring Data will retrieve that specific page from the data store. Additionally, Spring Data defines a Sort class that can be used to order the data from the query.

For this example we'll create two repositories:AddressRepository extends CrudRepository, while UserRepository extends PagingAndSortingRepository with a couple of custom search methods.

Create AddressRepository

Listing 5 shows the source code for AddressRepository.

Listing 5. AddressRepository.java

package com.geekcap.javaworld.springdata.repository;

import com.geekcap.javaworld.springdata.model.Address;
import org.springframework.data.repository.CrudRepository;

/**
 * Manages Addresses
 */
public interface AddressRepository extends CrudRepository<Address,String>
{
}

There's nothing especially impressive about Listing 5 -- except perhaps the fact that you get all this code for free! If you take a look at the JavaDoc for the CrudRepository interface, you'll see that AddressRepository inherits the following methods:

public interface CrudRepository<T,ID extends Serializable> extends Repository<T,ID>{
    long count();
    void delete(ID id); 
    void delete(Iterable<? extends T> entities); 
    void delete(T entity); 
    void deleteAll(); 
    boolean exists(ID id); 
    Iterable<T> findAll(); 
    T findOne(ID id); 
    Iterable<T> save(Iterable<? extends T> entities); 
    T save(T entity);
}

AddressRepository extends CrudRepository, passing it the type Address and the ID (or primary key) of type String. Under the hood, Spring Data will dynamically create a class that implements this interface and provides implementations of each of its methods.

Create UserRepository

Listing 6 shows the source code for the UserRepository interface.

Listing 6. UserRepository.java

package com.geekcap.javaworld.springdata.repository;

import com.geekcap.javaworld.springdata.model.User;
import org.springframework.data.repository.PagingAndSortingRepository;

import java.util.List;

/**
 * Spring Data Repository for managing users
 */
public interface UserRepository extends PagingAndSortingRepository<User,String>
{
    public User findByEmailAddress( String emailAddress );
    public List<User> findByLastName( String lastName );
}

Paging and sorting

Note that UserRepository extends PagingAndSortingRepository, which itself extends CrudRepository, adding the following methods:

public interface PagingAndSortingRepository<T,ID extends Serializable> extends CrudRepository<T,ID>, Repository<T,ID> {
    Page<T> findAll(Pageable pageable) 
    Iterable<T> findAll(Sort sort)
}

The Pageable interface, which is implemented by the PageRequest class, allows you to specify a page size and a page number to request from the data store, for example:

     Pageable pageable = new PageRequest( pageNumber, pageSize );
        Page<User> page = userRepository.findAll( pageable );

If the data store contains 100 documents resulting from a particular query, then requesting page 1 (0-based index) with a page size of 10 will return documents 11 through 20. These records are wrapped by Page objects. The Page class provides information about the page (page number, page size, total number of pages, and so forth) as well as a getContent() method that returns a List containing the results of the query.

The Sort class allows you to specify a field (or fields) to sort on and the direction in which to sort (ascending or descending). The following code snippet demonstrates how to use the Sort class:

     Sort sort = new Sort( Sort.Direction.DESC, "age" );
        List<User> users = new ArrayList<User>();
        for( User user : userRepository.findAll( sort ) )
        {
            users.add( user );
        }
        return users;

In this example we execute findAll() to retrieve all Users but sort them in descending order, from oldest to youngest.

Automated queries

You may have noticed that in the previous sections I defined some methods for queries that I did not need to write, such as findByEmailAddress(). Spring Data was able to infer what I wanted to do. Spring Data's naming convention provides automatic support for generating queries. In this naming convention the following rules apply:

  • GreaterThan: returns all records whose specified property is greater than the method parameter. For example, findByAgeGreaterThan(int age).
  • LessThan: returns all records whose specified property is less than the method parameter. For example, findByAgeLessThan(int age).
  • Between: returns all records whose specified property is between two values. For example, findByAgeBetween(int from, int to).
  • IsNotNull, NotNull: returns all records whose specified property is not null. For example, findByLastNameNotNull().
  • IsNull, Null: returns all records whose specified property is null. For example, findByLastNameNull().
  • Like: returns all records whose specified property contains the specified method parameter. For example, findByFirstNameLike(String value).
  • Not: returns all records whose specified property does not match the specified method parameter. For example, findByFirstNameNot(String value).
  • Near: returns all records near a point (if using geospatial database features). For example, findByAddressNear(Point point).
  • Within: returns all records within a specified circle or box (if using geospatial database features). For example, findByAddressWithin(Circle circle).

In addition to this custom naming convention for defining methods, Spring Data gives you the @Query annotation, which makes it easier to write advanced MongoDB queries. For instance, within @Query you can define MongoDB aggregations like these:

 @Query( "{ age: { \"$gte\" : ?0 } }")
    public List<User> findByAgeOver( int age );

    @Query( "{ age: { \"$gte\" : ?0, \"$lte\" : ?1 } }")
    public List<User> findByAgeBetween( int lower, int upper ); 

The first example checks to see if the user's age is greater than or equal to ($gte) the first parameter (?0); the second example checks to see if the user's age is between two values. More specifically, it checks to see that the age is greater than or equal to the lower value and less than or equal to the upper value. (A primer on MongoDB aggregations is beyond the scope of this article, but see the MongoDB reference material for an excellent article describing how SQL queries map to MongoDB JSON queries.)

Advanced querying with QueryDSL

While you'll be able to resolve most queries using the Spring Data naming conventions or with MongoDB aggregates, there will come a time when you need additional querying capabilities. For this, Spring Data has integrated QueryDSL, which provides advanced querying options.

According to its reference material, QueryDSL is intended to be used for "enabling the construction of type-safe SQL-like queries for multiple backends, such as JPA, MongoDB, and SQL." In order to use QueryDSL you configure a build plug-in in your Maven POM file, which generates a set of classes from the domain model.

The plug-in generates a set of "Q" versions of your classes, such as QUser and QAddress, which are Predicates (in QueryDSL terms) and can be used by any repository that implements QueryDslPredicateExecutor. The QueryDslPredicateExecutor interface adds the following methods:

package org.springframework.data.querydsl;

import com.mysema.query.types.Predicate;
import com.mysema.query.types.OrderSpecifier;

public interface QueryDslPredicateExecutor<T> {
    long count(Predicate predicate) 
    Iterable<T> findAll(Predicate predicate) 
    Iterable<T> findAll(Predicate predicate, OrderSpecifier<?>... orders) 
    Page<T> findAll(Predicate predicate, Pageable pageable) 
    T findOne(Predicate predicate)
}

The above methods and the generated query classes enable you to write queries like this one:

QUser user = new QUser( "user" );
Iterable<User> users = userRepository.findAll( user.lastName.contains( lastName ).and( user.age.gt( age ) ) );

Advanced querying is a pretty exciting feature in Spring Data, so let's take a minute to explore it further.

Anatomy of a generated query class

Listing 7 shows the source code for the QUser class that is generated from the User class In Listing 6.

Listing 7. QUser.java

package com.geekcap.javaworld.springdata.model;

import static com.mysema.query.types.PathMetadataFactory.*;

import com.mysema.query.types.path.*;

import com.mysema.query.types.PathMetadata;
import javax.annotation.Generated;
import com.mysema.query.types.Path;
import com.mysema.query.types.path.PathInits;

/**
 * QUser is a Querydsl query type for User
 */
@Generated("com.mysema.query.codegen.EntitySerializer")
public class QUser extends EntityPathBase<User> {

    private static final long serialVersionUID = -87909412;

    public static final QUser user = new QUser("user");

    public final ListPath<Address, QAddress> addresses = this.<Address, QAddress>createList("addresses", Address.class, QAddress.class, PathInits.DIRECT);

    public final NumberPath<Integer> age = createNumber("age", Integer.class);

    public final StringPath emailAddress = createString("emailAddress");

    public final StringPath firstName = createString("firstName");

    public final StringPath id = createString("id");

    public final StringPath lastName = createString("lastName");

    public QUser(String variable) {
        super(User.class, forVariable(variable));
    }

    @SuppressWarnings("all")
    public QUser(Path<? extends User> path) {
        super((Class)path.getType(), path.getMetadata());
    }

    public QUser(PathMetadata<?> metadata) {
        super(User.class, metadata);
    }

}

Note that all of the fields in "Q" classes are exposed and can be used to perform things like string and number comparisons.

Advantages of QueryDSL

There are three main benefits to using QueryDSL:

  • Queries are written in terms of object relationships, which come naturally to Java programmers.
  • All queries are type-safe, ensuring that improperly written queries will be caught by the compiler.
  • IDE code completion works, so you don't have to memorize the nuances of the query library.

Spring Data demo

Enough background information, let's see Spring Data in action! The sample program in this section contains the following:

  • Two domain classes, User and Address
  • Two corresponding repositories, UserRepository and AddressRepository
  • A service, UserService, and its implementation, UserServiceImpl
  • A JUnit test case to exercise and test the service

Note that in order to run the unit test (which might better be called an integration test since it is talking directly to the database) you'll need to install and run MongoDB on your local machine, listening on the standard 27017 port.

Project POM

Listing 8 shows the Maven POM file for the project.

Listing 8. POM.xml for the Spring Data demo


  4.0.0

  com.geekcap.javaworld
  spring-data-example
  1.0-SNAPSHOT
  jar

  spring-data-example
  http://maven.apache.org

    
        3.2.1.RELEASE
        1.2.0.RELEASE
        1.6
        2.9.0
    

  

      
      
          org.springframework
          spring-core
          ${spring.version}
          
              
                  commons-logging
                  commons-logging
              
          
      
      
          org.springframework
          spring-beans
          ${spring.version}
      
      
          org.springframework
          spring-context
          ${spring.version}
      
      
          org.springframework
          spring-aop
          ${spring.version}
      

      
      
          org.springframework.data
          spring-data-mongodb
          ${spring.data.mongodb.version}
      
      
          org.springframework
          spring-tx
          ${spring.version}
      
      
          org.springframework
          spring-expression
          ${spring.version}
      

      
      
          com.mysema.querydsl
          querydsl-mongodb
          ${querydsl.version}
      

      
      
          junit
          junit
          4.11
          test
      
      
          org.springframework
          spring-test
          ${spring.version}
          test
      

  

    
        
            
                org.apache.maven.plugins
                maven-compiler-plugin
                
                    ${java.version}
                    ${java.version}
                
            

            
                com.mysema.maven
                apt-maven-plugin
                1.0.8
                
                    
                        generate-sources
                        
                            process
                        
                        
                            target/generated-sources
                            org.springframework.data.mongodb.repository.support.MongoAnnotationProcessor
                        
                    
                
            
        
    

The POM in Listing 8 imports the core Spring classes, Spring Data, Spring Data's support for MongoDB, and QueryDSL, among other things. Be sure to notice the com.mysema.maven plug-in. This plug-in generates the QueryDSS classes previously described.

Execute the following command to build the project:

mvn clean install

Application context file

Next, look at the applicationContext.xml file in Listing 9.

Listing 9. applicationContext.xml

<?xml version="1.0" encoding="UTF-8"?>



    
    

    
    

    
        
        
    

    
    

    
    


The applicationContext.xml file runs a basic component scan to find the requested Spring components. It will search for annotated services, components, repositories, and so on. It then builds a MongoTemplate, wiring in a MongoDB Factory. Next, it uses the mongo:mapping-converter to scan the domain objects, which are annotated with the MongoDB @Document annotation. Finally, it uses the mongo:repositories to scan the repositories, enabling Spring Data to create implementations of the repository interfaces.

With these directives defined, the application context (a bean factory) contains implementations of the Spring Data repository interfaces for our domain objects.

Extending UserRepository

The domain objects that we defined back in Listings 3 and 4, and the AddressRepository from Listing 6 will remain the same for this demo. In Listing 10 I've updated UserRepository (from Listing 7) with support for QueryDSL.

Listing 10. UserRepository.java extended for QueryDSL

package com.geekcap.javaworld.springdata.repository;

import com.geekcap.javaworld.springdata.model.User;
import org.springframework.data.mongodb.repository.Query;
import org.springframework.data.querydsl.QueryDslPredicateExecutor;
import org.springframework.data.repository.PagingAndSortingRepository;

import java.util.List;

/**
 * Spring Data Repository for managing users
 */
public interface UserRepository  extends PagingAndSortingRepository<User,String>, QueryDslPredicateExecutor<User>
{
    public User findByEmailAddress( String emailAddress );
    public List<User> findByLastName( String lastName );

    @Query( "{ age: { \"$gte\" : ?0 } }")
    public List<User> findByAgeOver( int age );

    @Query( "{ age: { \"$gte\" : ?0, \"$lte\" : ?1 } }")
    public List<User> findByAgeBetween( int lower, int upper );
}   

Listing 10 adds support for the QueryDslPredicateExecutor and shows a couple of MongoDB aggregate query examples using the @Query annotation.

A query service

Listings 11 and 12 show the source code for the demo's UserService and UserServiceImpl, respectively.

Listing 11. UserService.java

package com.geekcap.javaworld.springdata.service;

import com.geekcap.javaworld.springdata.model.User;

import java.util.List;

/**
 * Defines operations that can be performed on a User
 */
public interface UserService
{
    public enum SortOrder {
        ASCENDING, DESCENDING
    }

    public void addUser( User user );
    public User getUser( String id );
    public User findUserByEmailAddress( String emailAddress );
    public List<User> findUsersByLastName( String lastName );
    public List<User> findUsers();
    public List<User> findUsers( int pageNumber, int pageSize );
    public List<User> findUsersSortByAge( SortOrder sortOrder );
    public List<User> findUsersByAgeOver( int age );
    public List<User> findUsersWithAgeBetween( int lower, int upper );
    public List<User> findByLastNameOverAge( String lastName, int age );
    public void removeUser( String id );
}

Listing 12. UserServiceImpl.java

package com.geekcap.javaworld.springdata.service;

import com.geekcap.javaworld.springdata.model.Address;
import com.geekcap.javaworld.springdata.model.QUser;
import com.geekcap.javaworld.springdata.model.User;
import com.geekcap.javaworld.springdata.repository.AddressRepository;
import com.geekcap.javaworld.springdata.repository.UserRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.stereotype.Service;

import java.util.ArrayList;
import java.util.List;

/**
 * Implementation of the UserService
 */
@Service
public class UserServiceImpl implements UserService
{
    @Autowired
    private UserRepository userRepository;

    @Autowired
    private AddressRepository addressRepository;

    @Override
    public void addUser( User user )
    {
        // See if we need to add addresses
        if( user.getAddresses() != null )
        {
            for( Address address : user.getAddresses() )
            {
                Address createdAddress = addressRepository.save( address );
                address.setId( createdAddress.getId() );
            }
        }

        userRepository.save( user );
    }

    @Override
    public User getUser(String id)
    {
        return userRepository.findOne( id );
    }

    @Override
    public User findUserByEmailAddress(String emailAddress)
    {
        return userRepository.findByEmailAddress( emailAddress );
    }

    @Override
    public List<User> findUsersByLastName( String lastName )
    {
        return userRepository.findByLastName( lastName );
    }


    @Override
    public List<User> findUsers()
    {
        List<User> users = new ArrayList<User>();
        for( User user : userRepository.findAll() )
        {
            users.add( user );
        }
        return users;
    }

    @Override
    public List<User> findUsersSortByAge( SortOrder sortOrder )
    {
        // Create the Sort object
        Sort sort = null;
        if( sortOrder == SortOrder.ASCENDING )
        {
            sort = new Sort( Sort.Direction.ASC, "age" );
        }
        else
        {
            sort = new Sort( Sort.Direction.DESC, "age" );
        }

        // Query for all users, sorted by age
        List<User> users = new ArrayList<User>();
        for( User user : userRepository.findAll( sort ) )
        {
            users.add( user );
        }
        return users;

    }

    @Override
    public List<User> findUsersByAgeOver( int age )
    {
        return userRepository.findByAgeOver( age );
    }

    @Override
    public List<User> findUsersWithAgeBetween( int lower, int upper )
    {
        return userRepository.findByAgeBetween( lower, upper );
    }

    @Override
    public List<User> findByLastNameOverAge( String lastName, int age )
    {
        QUser user = new QUser( "user" );
        List<User> users = new ArrayList<User>();
        for( User u : userRepository.findAll( user.lastName.contains( lastName ).and( user.age.gt( age ) ) ) )
        {
            users.add( u );
        }
        return users;
    }

    @Override
    public List<User> findUsers( int pageNumber, int pageSize )
    {
        // Create a Pageable object for the requested page number and page size
        Pageable pageable = new PageRequest( pageNumber, pageSize );

        // Retrieve a page of users
        Page<User> page = userRepository.findAll( pageable );

        // Returns the list of users
        return page.getContent();
    }

    @Override
    public void removeUser(String id)
    {
        userRepository.delete( id );
    }
}

The UserRepository and AddressRepository instances are wired-in using the @Autowired annotation. Most methods simply pass through to one of the repository methods. The findUsersSortByAge() method demonstrates how to build and use a Sort object to sort returned users by age. The findUsers(pageNumber,pageSize) method demonstrates how to page query results using a PageRequest object. Finally, the findByLastNameOverAge() method demonstrates how to use QueryDSL by creating a QUser object and using both a String and a number comparison.

The only other method that deserves explanation is the addUser() method. Because a relationship is defined between the User class and its list of Address instances, we cannot simply save the User with the addresses. Instead, we have to save the Address objects individually (the save() call returns a copy of the created Address) and update the Address's ID before saving the User.

JUnit demo

Listing 13 presents a JUnit class that demonstrates how to exercise our UserService and its corresponding repositories.

Listing 13. UserServiceTest.java

package com.geekcap.javaworld.springdata.service;

import com.geekcap.javaworld.springdata.model.Address;
import com.geekcap.javaworld.springdata.model.User;
import com.geekcap.javaworld.springdata.repository.AddressRepository;
import com.geekcap.javaworld.springdata.repository.UserRepository;
import org.junit.*;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

import java.util.ArrayList;
import java.util.List;

/**
 * "Unit Test" for the user service; in quotes because this is more of an integration test than a unit test because
 * we're calling a real repository and talking to a real MongoDB instance
 */
public class UserServiceImplTest
{
    private static ApplicationContext applicationContext;
    private static UserService userService;
    private static UserRepository userRepository;
    private static AddressRepository addressRepository;

    @BeforeClass
    public static void beforeClass()
    {
        // Load our application context
        applicationContext = new ClassPathXmlApplicationContext( "applicationContext.xml" );

        // Load the user repository for manually changing the user records
        userRepository = ( UserRepository )applicationContext.getBean( "userRepository" );
        addressRepository = ( AddressRepository )applicationContext.getBean( "addressRepository" );

        // Load the UserServiceImpl service
        userService = ( UserService )applicationContext.getBean( "userServiceImpl" );
    }

    @AfterClass
    public static void afterClass()
    {
        // Delete test users
        userRepository.deleteAll();
    }

    @Before
    public void before()
    {
        userRepository.deleteAll();
    }

    @Test
    public void testAddUser()
    {
        // Create a user
        User user = new User( "Steven", "Haines", "steve@geekcap.com", 41 );

        // Insert it into the repository
        userService.addUser( user );

        // Check to see if its there
        User repositoryUser = userService.findUserByEmailAddress( "steve@geekcap.com" );
        Assert.assertNotNull( repositoryUser );
        Assert.assertEquals( "The user's first name is not correct", "Steven", repositoryUser.getFirstName() );
        Assert.assertEquals( "The user's last name is not correct", "Haines", repositoryUser.getLastName() );

        // Remove the user
        userService.removeUser( repositoryUser.getId() );
    }

    @Test
    public void testAddUserWithAddress()
    {
        // Create a user
        User user = new User( "Steven", "Haines", "steve@geekcap.com", 41 );
        Address address = new Address( "123 Some Street", "", "My City", "CA", "90210" );
        List<Address> addresses = new ArrayList<Address>();
        addresses.add( address );
        user.setAddresses( addresses );

        // Insert it into the repository
        userService.addUser( user );

        // Check to see if its there
        User repositoryUser = userService.findUserByEmailAddress( "steve@geekcap.com" );
        Assert.assertNotNull( repositoryUser );
        Assert.assertEquals( "The user's first name is not correct", "Steven", repositoryUser.getFirstName() );
        Assert.assertEquals( "The user's last name is not correct", "Haines", repositoryUser.getLastName() );
        Assert.assertNotNull( "Address is null", repositoryUser.getAddresses() );
        Assert.assertEquals( "The wrong number of addresses", 1, repositoryUser.getAddresses().size() );

        Address repositoryAddress = repositoryUser.getAddresses().get( 0 );
        Assert.assertEquals( "Street 1 is wrong", "123 Some Street", repositoryAddress.getStreet1() );
        Assert.assertEquals( "Street 2 is wrong", "", repositoryAddress.getStreet2() );
        Assert.assertEquals( "City is wrong", "My City", repositoryAddress.getCity() );
        Assert.assertEquals( "State is wrong", "CA", repositoryAddress.getState() );
        Assert.assertEquals( "Zipcode is wrong", "90210", repositoryAddress.getZipcode() );

        // Remove the user
        userService.removeUser( repositoryUser.getId() );
    }


    @Test
    public void testFindAllUsers()
    {
        // Add 10 users
        for( int i=0; i<10; i++ )
        {
            userService.addUser( new User( "Test", "User", "test" + i + "@test.com", 18 ) );
        }

        // Query for all users
        List<User> users = userService.findUsers();
        Assert.assertNotNull( "The user list was null", users );
        Assert.assertEquals( "findUsers() did not return the correct number of users", 10, users.size() );
    }

    @Test
    public void testFindUsersNByLastName()
    {
        // Add 10 users
        for( int i=0; i<10; i++ )
        {
            String lastName = "Smith";
            if( i%2 == 0 )
            {
                lastName = "Jones";
            }
            userService.addUser(new User("Test", lastName, "test" + i + "@test.com", 18));
        }

        // Query for all users
        List<User> users = userService.findUsersByLastName( "Jones" );
        Assert.assertNotNull( "The user list was null", users );
        Assert.assertEquals( "findUsersByLastName() did not return the correct number of users with the last name of Jones", 5, users.size() );
    }

    @Test
    public void testPagedFindAllUsers()
    {
        // Add 10 users
        for( int i=0; i<10; i++ )
        {
            userService.addUser(new User("Test", "User", "test" + i + "@test.com", 18));
        }

        // Query for all users
        List<User> users = userService.findUsers( 0, 5 );
        Assert.assertNotNull( "The first page of users was null", users );
        Assert.assertEquals( "findUsers() did not return the correct number of users", 5, users.size() );

        users = userService.findUsers( 1, 5 );
        Assert.assertNotNull( "The second page of users was null", users );
        Assert.assertEquals( "findUsers() did not return the correct number of users", 5, users.size() );

        users = userService.findUsers( 2, 5 );
        Assert.assertNotNull( "The third page of users was null", users );
        Assert.assertEquals( "findUsers() did not return the correct number of users", 0, users.size() );
    }

    @Test
    public void testFindAllUsersSortByAgeAscending()
    {
        // Add 10 users
        for( int i=0; i<10; i++ )
        {
            userService.addUser( new User( "Test", "User", "test" + i + "@test.com", 18+i ) );
        }

        // Query for all users
        List<User> users = userService.findUsersSortByAge( UserService.SortOrder.ASCENDING );
        Assert.assertNotNull( "The list of users was null", users );
        Assert.assertEquals( "findUsers() did not return the correct number of users", 10, users.size() );

        int lastAge = 0;
        for( User user : users )
        {
            Assert.assertTrue( "User is not in age ascending order", lastAge < user.getAge() );
            lastAge = user.getAge();
        }
    }

    @Test
    public void testFindAllUsersSortByAgeDescending()
    {
        // Add 10 users
        for( int i=0; i<10; i++ )
        {
            userService.addUser( new User( "Test", "User", "test" + i + "@test.com", 18+i ) );
        }

        // Query for all users
        List<User> users = userService.findUsersSortByAge( UserService.SortOrder.DESCENDING );
        Assert.assertNotNull( "The list of users was null", users );
        Assert.assertEquals( "findUsers() did not return the correct number of users", 10, users.size() );

        int lastAge = 100;
        for( User user : users )
        {
            Assert.assertTrue( "User is not in age ascending order", lastAge > user.getAge() );
            lastAge = user.getAge();
        }
    }

    @Test
    public void testFindUsersOverAge18()
    {
        // Add 10 users
        for( int i=0; i<10; i++ )
        {
            userService.addUser( new User( "Test", "User", "test" + i + "@test.com", 13+i ) );
        }

        // Query for all users
        List<User> users = userService.findUsersByAgeOver( 18 );
        Assert.assertNotNull( "The list of users was null", users );
        Assert.assertEquals( "findUsers() did not return the correct number of users", 5, users.size() );

        for( User user : users )
        {
            Assert.assertTrue( "Age is not over 18", user.getAge() >= 18 );
        }
    }

    @Test
    public void testFindUsersBetween18and35()
    {
        // Add 10 users
        for( int i=0; i<30; i++ )
        {
            userService.addUser( new User( "Test", "User", "test" + i + "@test.com", 15+i ) );
        }

        // Query for all users
        List<User> users = userService.findUsersWithAgeBetween( 18, 35 );
        Assert.assertNotNull( "The list of users was null", users );
        Assert.assertEquals( "findUsers() did not return the correct number of users", 18, users.size() );

        for( User user : users )
        {
            Assert.assertTrue( "Age is not between 18 and 35", ( user.getAge() >= 18 && user.getAge() <= 35 ) );
        }
    }

    @Test
    public void testFindUsersByLastNameOverAge()
    {
        // Add 10 users
        for( int i=0; i<30; i++ )
        {
            String lastName = "User";
            if( i%2 == 0 )
            {
                lastName = "Search";
            }

            userService.addUser( new User( "Test", lastName, "test" + i + "@test.com", 15+i ) );
        }

        // Query for all users
        List<User> users = userService.findByLastNameOverAge( "Search", 25 );
        Assert.assertNotNull( "The list of users was null", users );
        Assert.assertEquals( "findUsers() did not return the correct number of users", 9, users.size() );

        for( User user : users )
        {
            Assert.assertEquals( "Last name is not correct", "Search", user.getLastName() );
            Assert.assertTrue( "Age is not over 25", user.getAge() > 25 );
        }
    }
}

Listing 13 is pretty well documented. When you review it, be sure that you understand what it is testing and how it is testing it. If you want to add additional queries to the UserRepository, you can use this test class to validate those queries. Also note that you can obtain a reference to the UserRepository itself to test your queries, rather than having to wrap the call with the UserService. See the code in the beforeClass() method for a demonstration.

In conclusion

Spring Data provides an abstraction layer on top of multiple back-end data stores, which include JPA-based SQL stores as well as NoSQL stores like MongoDB and Neo4j. The beauty of using Spring Data is that you define interfaces that contain the queries you want to execute and Spring Data generates the implementations of those methods for you.

While Spring Data abstracts the mechanism for accessing data stores, the details of accessing a specific data store differ between data store implementations. So the Spring Data code that you write to access MongoDB will be different from the code you'd write to access Neo4j. Rather than providing support for the lowest common denominator across data stores, and hence losing the power that each data store provides, Spring Data provides a standard programming model for accessing the features of each data store, giving you the best of all worlds.

Steven Haines is a technical architect at Piksel, currently working onsite at Disney in Orlando. He is the founder of geekcap.com, an online education website, and has written hundreds of Java-related articles as well as three books: Java 2 From Scratch, Java 2 Primer Plus, and Pro Java EE Performance Management and Optimization. He lives with his wife and two children in Apopka, Florida.

Learn more about this topic

Join the discussion
Be the first to comment on this article. Our Commenting Policies