Spring MongoDB tutorial

Keine Kommentare
MongoDB

Time for a new Spring tutorial - this time we will implement a very simple but powerful data access to MongoDB. Therefore we will use the Spring Data Project which does the most work for us.

As always you can find the complete source code online on GitHub.
In my last post I showed a way how to use Spring Boot for bootstraping a Spring application. This project uses the described mechanisms and for a better understanding I recommend you to read the post if you are not familar with it.

Maven configuration

Let's start with the pom.xml and add the needed dependencies.

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">

  <modelVersion>4.0.0</modelVersion>

  <groupId>com.surpreso</groupId>
  <artifactId>spring-mongo</artifactId>
  <version>0.0.1-SNAPSHOT</version>

  <!-- The packaging format, use war for web projects -->
  <packaging>jar</packaging>

  <properties>
    <!-- The main class to start by executing java -jar -->
    <start-class>com.surpreso.spring_mongo.HelloWorldApplication</start-class>
  </properties>

  <!-- Inherit defaults from Spring Boot -->
  <parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>0.5.0.BUILD-SNAPSHOT</version>
  </parent>

  <dependencies>
    <!-- Spring Boot -->
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter</artifactId>
    </dependency>
    <!-- Spring Boot Test -->
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-test</artifactId>
      <scope>test</scope>
    </dependency>
    <!-- YAML parser -->
    <dependency>
      <groupId>org.yaml</groupId>
      <artifactId>snakeyaml</artifactId>
      <scope>runtime</scope>
    </dependency>
    <!-- @Inject annotation -->
    <dependency>
      <groupId>javax.inject</groupId>
      <artifactId>javax.inject</artifactId>
      <version>1</version>
    </dependency>
    <!-- Spring MongoDB integration -->
    <dependency>
      <groupId>org.springframework.data</groupId>
      <artifactId>spring-data-mongodb</artifactId>
    </dependency>
    <!-- In-memory MongoDB for tests -->
    <dependency>
      <groupId>com.lordofthejars</groupId>
      <artifactId>nosqlunit-mongodb</artifactId>
      <version>0.7.8</version>
      <scope>test</scope>
    </dependency>
    <!-- Google Guava library -->
    <dependency>
      <groupId>com.google.guava</groupId>
      <artifactId>guava</artifactId>
      <version>15.0</version>
    </dependency>
  </dependencies>

  <build>
    <plugins>
      <!-- Use plugin to package as an executable JAR -->
      <plugin>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-maven-plugin</artifactId>
      </plugin>
    </plugins>
  </build>

  <!-- Allow access to Spring milestones and snapshots -->
  <repositories>
    <repository>
      <id>spring-snapshots</id>
      <url>http://repo.spring.io/libs-snapshot</url>
      <snapshots>
        <enabled>true</enabled>
      </snapshots>
    </repository>
    <repository>
      <id>spring-milestones</id>
      <url>http://repo.spring.io/milestone</url>
      <snapshots>
        <enabled>true</enabled>
      </snapshots>
    </repository>
  </repositories>

  <pluginRepositories>
    <pluginRepository>
      <id>spring-snapshots</id>
      <url>http://repo.spring.io/snapshot</url>
    </pluginRepository>
    <pluginRepository>
      <id>spring-milestones</id>
      <url>http://repo.spring.io/milestone</url>
    </pluginRepository>
  </pluginRepositories>

</project>

The new part starts at line 49 where we add the spring-data-mongo artifact which includes the MongoDB Java Driver. To test our code I found a very nice in-memory implementation of MongoDB developed by FourSquare and avaiable on GitHub.

Last we add the Google Guava library which delivers nice helping methods for equals, toHash and toString.

Spring configuration

After setting up the maven project we have to configure the Spring context. To initialize Spring Boot correctly we will use the DefaultConfig class introduced in the last post.

package com.surpreso.spring_mongo.config;

import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;

@Configuration
@EnableAutoConfiguration
@ComponentScan(basePackages = "com.surpreso.spring_mongo")
public class DefaultConfig {

}

Further we will add a MongoDB config which initializes the Spring MongoTemplate. Similar to JdbcTemplate this provides the access to our database.

package com.surpreso.spring_mongo.config;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
import org.springframework.data.mongodb.config.AbstractMongoConfiguration;
import org.springframework.data.mongodb.repository.config.EnableMongoRepositories;
import com.mongodb.Mongo;
import com.mongodb.MongoURI;

@Profile("default")
@Configuration
@EnableMongoRepositories(basePackages = "com.surpreso.spring_mongo.dao")
public class MongoDbConfig extends AbstractMongoConfiguration {

 @Value("${mongo.url}")
 private String url;

 @Value("${mongo.db}")
 private String databaseName;

 @Override
 protected String getDatabaseName() {
  return databaseName;
 }

 @Override
 public Mongo mongo() throws Exception {
  return new Mongo(new MongoURI(url));
 }

 @Override
 protected String getMappingBasePackage() {
  return "com.surpreso.spring_mongo.beans";
 }

}

This configuration extends the Spring AbstractMongoConfiguration, doing the most work for us.
In the test environment we want to use the in-memory MongoDB version and therefore we annotate the configuration with @Profile("default"). This instructs Spring to only load the configuration when there exists no profile.
A further new annotation is @EnableMongoRepositories(basePackages = "com.surpreso.spring_mongo.dao") which tells the spring-data-mongo project where to look for MongoDB repositories.
The getMappingBasePackage method simply defines our bean package.
The values for the database name and url are stored in the application.yml.

mongo:
  url: mongodb://127.0.0.1:27017
  db: foo

For the test environment exists an own configuration.

package com.surpreso.spring_mongo.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
import com.foursquare.fongo.Fongo;
import com.mongodb.Mongo;

@Profile("test")
@Configuration
public class MongoDbTestConfig extends MongoDbConfig {

 @Override
 public Mongo mongo() throws Exception {
  return new Fongo("foo test server").getMongo();
 }

}

This one extends the default MongoDB configuration but returns the in-memory implementation.

Beans & Repositories

In the defined mapping package we create a very simple bean called Foo having only an ID and a name property.

package com.surpreso.spring_mongo.beans;

import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.mapping.Document;
import com.google.common.base.Objects;

@Document
public class Foo {

 @Id
 private Long id;
 private String name;

 public Foo() {
  super();
 }

 public Long getId() {
  return id;
 }

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

 public String getName() {
  return name;
 }

 public void setName(String name) {
  this.name = name;
 }

 @Override
 public boolean equals(Object obj) {
  if (obj == null) {
   return false;
  }
  if (!getClass().isInstance(obj)) {
   return false;
  }
  Foo foo = (Foo) obj;
  return Objects.equal(getId(), foo.getId())
    && Objects.equal(getName(), foo.getName());
 }

 @Override
 public int hashCode() {
  return Objects.hashCode(getId(), getName());
 }

 @Override
 public String toString() {
  return Objects.toStringHelper(this).addValue(getId())
    .addValue(getName()).toString();
 }

}

The bean gets annotated by @Document so Spring will map it to JSON, the Default MongoDB Format, on the fly. The @Id annotation for the id property defines the primary key for the document in the collection.
We will add equals, hashCode and toString methods for our tests and application output.
The implementation of the FooRepository will be done by Spring and we only have to define an interface giving the framework the needed information.

package com.surpreso.spring_mongo.dao;

import org.springframework.data.mongodb.repository.MongoRepository;
import org.springframework.stereotype.Repository;
import com.surpreso.spring_mongo.beans.Foo;

@Repository
public interface FooRepository extends MongoRepository<Foo, Long> {

}

Sweeeeet! That's all. Now we have a working data access for our Foos in MongoDB.

Testing

I know, we should not test the functionality of Spring if it implements the FooRepository in the right way, but let's do it to see if our fake MongoDB gets initialized and the data access works.

package com.surpreso.spring_mongo.dao;

import static org.junit.Assert.assertEquals;
import javax.inject.Inject;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.boot.test.SpringApplicationContextLoader;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

import com.surpreso.spring_mongo.beans.Foo;
import com.surpreso.spring_mongo.config.DefaultConfig;

@ActiveProfiles("test")
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = { DefaultConfig.class }, loader = SpringApplicationContextLoader.class)
public class FooRepositoryTests {

 @Inject
 FooRepository repo;

 @Test
 public void test_basicOperations() throws Exception {
  // check if collection is empty
  assertEquals(0, repo.count());
  // create new document
  Foo foo = new Foo();
  foo.setId(1l);
  foo.setName("Foo 1");
  repo.save(foo);
  // store new document
  repo.save(foo);
  // check if document stored
  assertEquals(1, repo.count());
  // check stored document
  assertEquals(foo, repo.findOne(1l));
 }

}

The test context initialization is familiar to my last post and the test itself only perfoms some Basic operations on the database. Using the in-memory MongoDB you can run this test without a working MongoDB Environment.

Deployment & Execution

Last we will implement an application which uses a real MongoDB Environment (install instructions).

package com.surpreso.spring_mongo;

import javax.inject.Inject;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.context.annotation.Import;
import com.surpreso.spring_mongo.beans.Foo;
import com.surpreso.spring_mongo.config.DefaultConfig;
import com.surpreso.spring_mongo.dao.FooRepository;

@Import(DefaultConfig.class)
public class HelloWorldApplication implements CommandLineRunner {

 @Inject
 private FooRepository repo;

 public static void main(String... args) {
  SpringApplication.run(HelloWorldApplication.class, args);
 }

 public void run(String... args) throws Exception {
  // get size of the collection
  long count = repo.count();
  // create new document
  Foo foo = new Foo();
  foo.setId(count + 1);
  foo.setName("Foo " + foo.getId());
  // add new document to collection
  repo.save(foo);
  // output collection info
  System.out.println("= Found " + repo.count()
    + " documents in Foo collection");
  for (Foo f : repo.findAll()) {
   System.out.println("+ " + f);
  }
 }

}

The application just creates a new document in your collection and outputs all stored records. With each run of the program there will be one Foo more in the database.
How to deploy and run the program have a look at the last part of my previous post.

Keine Kommentare :

Kommentar veröffentlichen