Open source Java projects: Spring Data

Use common Spring queries to access multiple NoSQL data stores

1 2 3 4 5 Page 2
Page 2 of 5
{
    "_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:

1 2 3 4 5 Page 2
Page 2 of 5