Introduction

Using Spring Data and Spring Boot, it is possible to very quickly make applications which perform CRUD operations and perform complex searches on database. In the following post I will be demonstrating how to:

  • Create a domain object that can be stored by Spring Data in a mongodb collection
  • Create and configure a repository to perform operations on the database
  • Enhance the domain object to include geo spatial data
  • Enhance the repository to search on geo spatial data

If you do not have mongodb installed then you can find instructions on how to do so here.

Mongo is a NOSQL document store, which means that the documents which you store in it do not have to conform to any particular schema or structure. This does not fit completely with the stricter structure enforced by Java. Spring data gets around this by including a field called _class with each document it persists. This is then used to map the document back into a domain object.

The Data

The data for this example is taken from google maps, and is a small sample of some of the pubs near me. The rating is a simple mark out of 5 that I have given them, with -1 indicating a pub I haven’t visited. As a small disclaimer, the rating is not a full review, and just my opinion based on a few visits.

Name Rating Latitude Longitude
George Canning 3 51.4678685 -0.0860632
The Cherry Tree -1 51.461512 -0.078988
The Fox on the Hill 3 51.4651705 -0.0895804
The Flying Pig 5 51.461744 -0.070394
The East Dulwich Taven 4 51.460463 -0.07513

Maven Config

To include spring boot and spring data with mongodb integration I created a pom with the following dependancies:

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-mongodb</artifactId>
    </dependency>
</dependencies>

<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>io.spring.platform</groupId>
            <artifactId>platform-bom</artifactId>
            <version>1.0.0.RELEASE</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>

Domain Object

Start off with a very simple domain object that will hold an ID, a name and a rating. The Id will be automatically generated by mongo when the object is persisted, so our constructor will just need to take a name and rating.

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

@Document(collection = "pubs")
public class Pub {

    @Id
    private String id;

    private String name;

    private int rating;

    private Pub() {}

    public Pub(String name, int rating) {
        this.name = name;
        this.rating = rating;        
    }

    public String getId() {
        return id;
    }

    public String getName() {
        return name;
    }

    public int getRating() {
        return rating;
    }

    @Override
    public String toString() {
        return "Pub{" +
                "id='" + id + '\'' +
                ", name='" + name + '\'' +     
                ", rating=" + rating +
                '}';
    }
}

This is a fairly simple POJO, with two minor exceptions, firstly the @document(collection = “pubs”) annotation which tells spring data that this is a mongodb document and that is should be stored in a collection called pubs. The second is the @id annotation which is a standard spring data annotation used to denote which field to use to identify an object.

Repository

The Spring Data Repository interface can then be used to create a simple repository to interact with the data store.

public interface PubRepository extends MongoRepository<Pub, String> {
}

This will provide all of the database interactions included with the MongoRepository, this includes basic CRUD operations. The generics tell the base mongo repository to return Pub domain objects, identified by a String id.

Using Spring Data the repository can then be enhanced with some basic find methods.

public interface PubRepository extends MongoRepository<Pub, String> {
    Pub findByName(String name);
    
    List<Pub> findByRatingGreaterThan(int rating);
    
    List<Pub> findByRatingLessThan(int rating);
}

These are some very simple find methods which should be self explanatory by their names.

Configuration

Using the Spring Boot configuration annotations a simple java class can be used to create the config for the application for now.

import com.mongodb.MongoClient;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.mongodb.MongoDbFactory;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.core.SimpleMongoDbFactory;
import org.springframework.data.mongodb.repository.config.EnableMongoRepositories;

@Configuration
@EnableMongoRepositories
public class Config {

    @Bean
    public MongoDbFactory mongoDbFactory() throws Exception {
        return new SimpleMongoDbFactory(new MongoClient(), "geoSearch");
    }

    @Bean
    public MongoTemplate mongoTemplate() throws Exception {
        return new MongoTemplate(mongoDbFactory());
    }

} 

This should be fairly self explanatory, it just creates a new client, connecting to the database geoSearch. It then uses this client to create the template that will be used by any repositories in this project, so in this case just the pub repository. That means any data will be saved in the pubs collection in the geoSearch database.

Using the Repository

This can then be used to autowire the repository into another class and make use of it from there, for example:

@Autowired
private PubRepository repository;

repository.deleteAll();

repository.save(new Pub("George Canning", 3));
repository.save(new Pub("The Cherry Tree", -1,));
repository.save(new Pub("The Fox on the Hill", 3));
repository.save(new Pub("The Flying Pig", 5));
repository.save(new Pub("The East Dulwich Tavern",4));

System.out.println("Pubs found with findAll():");
System.out.println("-------------------------------");
for (Pub pub : repository.findAll()) {
    System.out.println(pub);
}
System.out.println();

System.out.println("Pubs found with findByRatingGreaterThan(3):");
System.out.println("-------------------------------");
for (Pub pub : repository.findByRatingGreaterThan(3)) {
    System.out.println(pub);
}
System.out.println();

System.out.println("Pubs found with findByRatingLessThan(0):");
System.out.println("-------------------------------");
for (Pub pub : repository.findByRatingLessThan(0)) {
    System.out.println(pub);
}       
System.out.println();

System.out.println("Pub found with findByFirstName('The Flying Pig'):");
System.out.println("--------------------------------");
System.out.println(repository.findByName("The Flying Pig"));

This uses both some of the built in methods of the repository, such as save, deleteAll and findAll as well as some of the method that were added by the pub repository. Running this should, if mongo is running, produce the expected output.

Adding the Geo Spatial Data

Now that there is a working repository as well as a domain object to persist, this can be enhanced to hold the location of each of the pubs on the list. This is done by holding the longitude and latitude, as double values in an array and marking that array as a @@GeoSpatialIndexed. This will automatically set up the indexing of the field in mongodb and allow it to be searchable using geo spatial search methods.

@GeoSpatialIndexed
 private double[] location;

The constructor also needs to be updated to take the latitude and longitude and initiate the array.

public Pub(String name, int rating, double latitude, double longitude) {
    this.name = name;
    this.rating = rating;
    this.location = new double[2];
    location[0] = latitude;
    location[1] = longitude;
}

Searching on Geo Spatial data

Now that the pub object can hold its own location, and this is persistable in the database, the repository can be updated to include a search for any pubs that are within a sphere of a given distance, for example searching for all pubs within 1K of ‘51.4634836,-0.0841914’ (roughly where I live).

This is done by updating the repository with the following:

    GeoResults<Pub> findByLocationNear(Point location, Distance distance);

The search takes a point, which is the locations to start searching from and a Distance which contains the the units to use for the measurement and the distance. A GeoSearchResults object is returned which is a collection of GeoSearchResult objects. This contains the content object, in this case a Pub object and a distance from the provided point in the given units.

This can then be invoked with some enhanced test data.

repository.deleteAll();

repository.save(new Pub("George Canning", 3, 51.4678685, -0.0860632));
repository.save(new Pub("The Cherry Tree", -1, 51.461512, -0.078988));
repository.save(new Pub("The Fox on the Hill", 3, 51.4651705, -0.0895804));
repository.save(new Pub("The Flying Pig", 5, 51.461744, -0.070394));
repository.save(new Pub("The East Dulwich Tavern",4, 51.460463, -0.07513));

System.out.println("Pubs found within 1K of '51.4634836,-0.0841914':");
System.out.println("--------------------------------");
for (GeoResult<Pub> pub : repository.findByLocationNear(new Point(51.4634836, -0.0841914), new Distance(1, Metrics.KILOMETERS))) {
    System.out.println(pub.getContent());
}
System.out.println();

Which should produce the following results:

Pubs found within 1K of '51.4634836,-0.0841914':
--------------------------------
Pub{id='53b9242b30046ad4a4b447fc', name='George Canning', location=[51.4678685, -0.0860632], rating=3}
Pub{id='53b9242b30046ad4a4b447fd', name='The Cherry Tree', location=[51.461512, -0.078988], rating=-1}
Pub{id='53b9242b30046ad4a4b447fe', name='The Fox on the Hill', location=[51.4651705, -0.0895804], rating=3}

The full source code form this demonstration can be found here