Getting Started With Spring Data MongoDB Tutorial

02.08.2017 by Jens in Spring Boot | Spring Data

Working with Spring Data has the huge benefit that you work with a common interface regardless of the underlying storage medium. Yet, it's still transparent enough. In today's tutorial, we will use it with a MongoDB.

MongoDB was one of the first NoSQL stores. Rather than storing your data in tables and columns with a fixed schema, you store it as a document in JSON. It's a different approach to data storage and neither better or worse than regular SQL. Enough of this intro, let's dive in.

Getting Started and Basics

I assume you are using Spring Boot anyways, so the first, step is to add the starter for Spring Data MongoDB.

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

Next, we must declare which MongoDB instance our application should use. We can do so using regular properties in application.properties.

spring.data.mongodb.uri=mongodb://localhost:27017/tutorial

You can provided additional username and password with the properties:

  • spring.data.mongodb.username
  • spring.data.mongodb.password

Sample Application

I am using the same sample application as in the previous 5pring Data REST tutorial](http://codeboje.de/spring-data-rest-tutorial/). The sample application consists of a simple model of a user with some attributes and another one representing an address. However, we use MongoDB instead of old JPA.

You can find the code on GitHub.

To mark a model for use with Spring Data MongoDB, we must annotate the class with @Document. Spring now knows that it is a model for use with MongoDB. It's the same principle as you have to annotate JPA classes with @Entity.

@Document
public class User {

    @Id
    private String id;

    @Indexed(unique = true)
    private String username;

    @TextIndexed
    private String firstname;

    @TextIndexed
    private String lastname;

    @TextScore
    private Float textScore;

    private Address homeAddress;

    //getter and setter omitted
}

By default, Spring will serialize all not transient fields to the document store and will use the Java field name as document field name too. If you want to deviate from that, you can set a different name using the @Field annotation as we do on Address.zipCode field below.

If you need an index on a certain field, you can add it with the @Indexed annotation and like above, even mark it as a unique field. We will cover @TextIndexed and @TextScore later.

@Document
public class Address {

    @Id
    private String id;

    private String street;

    @Field("zip_code")
    private String zipCode;

    private String city;

    private String country;

    //getter and setter omitted
}

Our next step is to provide a Spring Data repository so we can interact with our data.

public interface UserRepository extends PagingAndSortingRepository<User, String>{

    Page<User> findAllByUsername(@Param("username") String username, Pageable page);
}

It works and behaves exactly the same as any other Spring Data module. You interact with an almost POJO and a repository abstraction.

THe sample application main:

@SpringBootApplication
public class SpringDataMongodbTutorialApplication {

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

However, we are going to explore it with a test.

@RunWith(SpringRunner.class)
@SpringBootTest
public class SpringDataMongodbTutorialApplicationTests {

    @Autowired
    private UserRepository repo;

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

Working with the Repository

As I mentioned before, it behaves the same as with any other Spring Data store. You operate with the almost POJO and the repository abstraction.

To create a new user, we simply use the save method on our UserRepository.

@Test
public void createUserWithAddress() {
    User horst = new User();
    horst.setFirstname("Horst");
    horst.setLastname("Mustermann");
    horst.setUsername("horstm");
    Address address = new Address();
    address.setCity("Frankfurt");
    address.setCountry("Germany");
    address.setStreet("Zeil 3");
    address.setZipCode("60384");

    horst.setHomeAddress(address);

    User horstDb = repo.save(horst);
    assertNotNull(horstDb);
    assertNotNull(horstDb.getId());
    assertEquals(horst.getFirstname(), horstDb.getFirstname());
}

The Address is stored inside the User document in MongoDB.

Queries - Retrieving Data

You can write your queries using the Spring Data method naming schema, by using the Examples API or the @Query annotation. However, the syntax supported on @Query is store dependant. With MongoDB, it's the JSON style.

@Query("{username : ?0}")

?0 is a placeholder for the first method parameter.

For this tutorial, we use the method naming schema.

Using Collection References

In case you want to store one model as a document and just use a reference to it in another one, it works too. For example, if the address would be a separate document, we can add the @DBRef annotation in User on the homeAddress field.

For showing both cases in the sample, I set up a new User class named UserWithRef. It is essentially the same with the only difference of how the homeAddress is handled.

@Document
public class UserWithRef {

    @Id
    private String id;

    private String username;

    private String firstname;

    private String lastname;

    @DBRef
    private Address homeAddress;

    //getter and setter omitted
}

As Address was already annotated with @Docuement we don't need to do anything further on that one. However, we must provide a Repository for both.

public interface UserWithRefRepository extends PagingAndSortingRepository<UserWithRef, String>{

    Page<UserWithRef> findAllByUsername(@Param("username") String username, Pageable page);
}

And the AddressRepository:

public interface AddressRepository extends PagingAndSortingRepository<Address, String>{

    Address findByStreetAndCityAndCountry(String street, String city, String country);
}

In the previous example, we could just create a new Address and add it to the User class. Both would be saved in the same document. However, with the reference, MongoDB requires that the document we reference must be already present in the database.

@Test
public void createUserWithRefAndAddress() {
    UserWithRef horst = new UserWithRef();
    horst.setFirstname("Horst");
    horst.setLastname("Mustermann");
    horst.setUsername("horstm");

    Address address = new Address();
    address.setCity("Frankfurt");
    address.setCountry("Germany");
    address.setStreet("Zeil 1");
    address.setZipCode("60384");

    address = addressRepo.save(address);

    horst.setHomeAddress(address);

    UserWithRef horstDb = refRepo.save(horst);
    assertNotNull(horstDb);
    assertNotNull(horstDb.getId());
    assertEquals(horst.getFirstname(), horstDb.getFirstname());
}

So, in this test, we save the new address before we save the user. If you want to reference an existing one, you'd need to load it first and then attach it to the user.

MongoDB provides a full-text search capability. To use it with Spring, we first must mark the fields to be included in the search. We do this with the @TextIndexed annotation like in the user class above:

@TextIndexed
private String firstname;

@TextIndexed
private String lastname;

@TextScore
private Float textScore;

MongoDB returns a score for each search result. We can make it available for our code with the @TextScore annotation. The field must be a Float o Double. It is also handled as a transient field and not stored in the database.

For doing a full-text search, we must use a repository parameter of type TextCriteria like

List<User> findAllBy(TextCriteria criteria);

Spring Data MongoDB will detect it as a full-text search and make the request accordingly.

TextCriteria provides a fluent API for creating the search like:

TextCriteria.forDefaultLanguage().matching("horst");

In the test add:

@Test
public void searchUser() {
    createUserWithAddress();
    TextCriteria search = TextCriteria.forDefaultLanguage().matching("horst");
    List<User> r = repo.findAllBy(search);

    assertNotNull(r);
    assertFalse(r.isEmpty());
    r.stream().forEach(u -> System.out.println(u.getUsername() + " " + u.getTextScore()));

}

In the example, we are using the default language. To use another for searching, you can declare the document's language in the @Document annotation with the language parameter and also use forLanguage on TextCriteria to set the language.

Embedded MongoDB for Testing

When you write tests, you usually don't want to have a dependency on an external MongoDB server. Gladly, there's a module that provides an embedded MongoDB service for testing purposes.

You can use and activate it with adding the dependency:

<dependency>
    <groupId>de.flapdoodle.embed</groupId>
    <artifactId>de.flapdoodle.embed.mongo</artifactId>
    <scope>test</scope>
</dependency>

This will auto-configure the embedded database, so you can directly use it in your test without setting up anything further.

Conclusion

When you already have worked with Spring Data, you will be probably up and running in a few minutes with using MongoDB in Spring. You are still working with the known Spring Data abstraction layer. However, as for all Spring Data backends, it is important that you learn about the underlying storage system, so you understand what constraints and implications it forces up on you. As always:

It depends

Happy coding

Jens


comments powered by Disqus