MongoDB with Spring Data

In this article I show how connect a Spring Boot application with MongoDB. I will show how to configure a Spring Boot application to be connected to a single noSQL database as MongoDB. And I will show how to use Spring Data to request a MongoDB database.

Content:

  • NoSQL Architecture
  • MongoDB
  • MongoDB with Spring Boot
  • Entities
  • Spring Data Repositories
  • Querying the Repositories

For a detailed explanation, watch the following video.

Go to this repository for the code of the article.

NoSQL Architecture

Instead of using a relational database with my Spring Boot application, like Postgres or MySQL, I will use MongoDB. MongoDB is a noSQL database. A database which is document oriented. Instead of having my data structured in rows and columns, I have JSON documents.

This makes my schema very flexible. As each document can have a different structure. The objective is that instead of having to create multiple joins to obtain my desired output, I can have the needed information in a single document.

With this kind of structure, the read operations faster, as I don’t have to query many tables to obtain the necessary information to be displayed. Nevertheless, the write operations can be slower, as the nested documents will be duplicated in several documents. So, to update a single field of a nested document, I need to edit multiple documents.

In this case, I can use multiple databases: one for write operations and one for read operations. This is called the CQRS pattern, Common Query Responsibility Segregation. Have a relational database, such as Postgres, for the write operations, and a noSQL database, as MongoDB, for the read operations. And have a mechanism to make updates from one database to the other. I won’t to enter into the details of this mechanism, as it’s out of the scope of this article.

MongoDB

For the current article, I will use MongoDB as the only database of my application. I will start a Docker container with Mongo inside to perform my tests.

> docker run -d -p 27017:27017 -e MONGO_INITDB_ROOT_PASSWORD=secret -e MONGO_INITDB_ROOT_USERNAME=mongoadmin mongo:5.0.6 --bind_ip_all
> mongosh "mongodb://mongoadmin:secret@127.0.0.1:27017/socialnetworkdb?authSource=admin" 
[mongo] > db.createUser({user: "sergio", pwd: "ser", roles: [{role: "dbOwner", db: "socialnetworkdb"}]}) 
> mongosh "mongodb://sergio:ser@127.0.0.1:27017/socialnetworkdb"

I need the bind_ip_all option to tell the Mongo server to accept requests from my host. Otherwise, only internal requests are allowed. Then, I connect to my Mongo server with the admin user I’ve indicated, to create a regular user. A regular user to be used from my Spring Boot application.

MongoDB with Spring Boot

Let’s start adding the needed Maven dependencies.

        <dependency>
            <groupId>org.springframework.data</groupId>
            <artifactId>spring-data-mongodb</artifactId>
            <version>3.3.2</version>
        </dependency>
        <dependency>
            <groupId>org.mongodb</groupId>
            <artifactId>mongodb-driver-sync</artifactId>
        </dependency>

I will need the Mongo driver to talk with the database, and the Spring Data connector to allow me to use the repositories.

Let’s now configure my Spring Boot application to be connected to the Mongo database. I will create a bean that extends an abstract configuration of a Mongo client.

@Configuration
@EnableMongoRepositories(basePackages = "com.sergio.socialnetwork.repositories")
public class MongoClientConfig extends AbstractMongoClientConfiguration {

    @Override
    protected String getDatabaseName() {
        return "socialnetworkdb";
    }

    @Override
    protected void configureClientSettings(MongoClientSettings.Builder builder) {
        builder.credential(MongoCredential.createCredential("sergio", "socialnetworkdb", "ser".toCharArray()))
                .applyToClusterSettings(settings -> settings.hosts(Collections.singletonList(new ServerAddress("localhost", 27017))));
    }

}

The abstract class AbstractMongoClientConfiguration already contains some default configuration. I will extend it to use some custom configuration, such as the database name and the credentials.

An important consideration with Mongo, is that the IDs used in the Mongo entities are strings and not numbers. I must adapt all of my entities and DTOs to use IDs as strings and no more as numbers.

Entities

I will now create some objects, some entities, which will reflect the database schema. I will create the User entity to map into a user’s collection. It will contain the fields: first name, last name, login, password, created date and a list of friends. Those friends are also users located in the same collection. I will also create the Message entity. It will contain the fields: content, created date and user. The user field can’t be a link to the user’s collection, as Mongo doesn’t accept links or foreign keys. Instead, the user’s field is just a sample of the user object which is present in the user’s collection. It will just contain the ID, first name and last name.

@Document(collection = "app_users")
public class MongoUser {

    private String id;
    private String firstName;
    private String lastName;
    private String login;
    private String password;
    private List<MongoUser> friends;
    private LocalDateTime createdDate;

    // constructors
    // getters and setters

    public MongoUser buildShareableUser() {
        return new MongoUser(this.id, this.firstName, this.lastName);
    }
}
@Document(collection = "app_messages")
public class MongoMessage {

    private String id;
    private String content;
    private MongoUser user;
    @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm")
    private LocalDateTime createdDate;

    // constructors
    // getters and setters

}

With the @Document annotation, I indicate that my entity will be located in the app_messages collection. Otherwise, it will save them in a collection named MongoMessages, as the class name. I prefer to maintain the lower case in the Mongo collection’s name.

Spring Data Repositories

Let’s now create the Spring Data repository to query, save and update the entities into MongoDB.

public interface UserRepository extends PagingAndSortingRepository<MongoUser, String> {

    Optional<MongoUser> findByLogin(String login);

    @Query("{$or: [{firstName: /?0/}, {lastName: /?0/}, {login: /?0/}]}")
    List<MongoUser> searchUsers(String term);

}

I need the interface to extend PageAndSortingRepository, to have some queries and pagination available in the repository by default. I also want to search by user’s login with the method findByLogin. With the repositories, I can build a query just by naming the method with the fields i want to filter. And finally, I want to perform a custom or complicated query. I have the @Query annotation and write it inside my query.

public interface MessageRepository extends PagingAndSortingRepository<MongoMessage, String> {

    List<MongoMessage> findAllByUserIdIn(List<String> ids, Pageable pageable);
}

In this case, I filter it by a list of IDs with the in keyword in the method name findAllByUserIdIn. And I’ve done this filter in a nested field. Finally, I’ve added the pagination to the query just with the additional parameter Pageable. Spring Data will handle the pagination from me just with this additional parameter.

Querying the Repositories

Let’s see some examples using those repositories.

private MongoUser getUser(String id) {
        return userRepository.findById(id)
                .orElseThrow(() -> new RuntimeException("User not found"));
    }

As this method returns me an optional, i can chain the OrElseThrow method to throw an exception or return the result.

    public UserDto signUp(SignUpDto userDto) {
        Optional<MongoUser> optionalUser = userRepository.findByLogin(userDto.getLogin());

        if (optionalUser.isPresent()) {
            throw new RuntimeException("Login already exists");
        }

        MongoUser user = new MongoUser(
                userDto.getFirstName(),
                userDto.getLastName(),
                userDto.getLogin(),
                passwordEncoder.encode(CharBuffer.wrap(userDto.getPassword())),
                Collections.emptyList(),
                LocalDateTime.now()
                );

        MongoUser savedUser = userRepository.save(user);

        return new UserDto(savedUser.getId(),
                savedUser.getFirstName(),
                savedUser.getLastName(),
                savedUser.getLogin());
    }

To save a new user, i just call the save method of the repository.

    public void addFriend(UserDto userDto, String friendId) {
        MongoUser user = getUser(userDto.getId());

        MongoUser newFriend = getUser(friendId);

        if (user.getFriends() == null) {
            user.setFriends(new ArrayList<>());
        }

        user.getFriends().add(newFriend.buildShareableUser());

        userRepository.save(user);
    }

And to update an existing user, as before, i just need to call the save method.

    public List<MessageDto> getCommunityMessages(UserDto userDto, int page) {
        MongoUser user = getUser(userDto);

        List<String> ids = new ArrayList<>();
        ids.add(user.getId());
        user.getFriends().stream().forEach(friend -> ids.add(friend.getId()));

        Iterable<MongoMessage> messages = messageRepository.findAllByUserIdIn(ids, PageRequest.of(page, PAGE_SIZE));

        List<MessageDto> messageDtoList = new ArrayList<>();
        messages.forEach(message -> messageDtoList.add(new MessageDto(
                message.getId(),
                message.getContent(),
                new UserSummaryDto(message.getUser().getId(), message.getUser().getFirstName(), message.getUser().getLastName()),
                message.getCreatedDate())));

        return messageDtoList;
    }

To use the pagination, I have the builder PageRequest.of with the index of the page and the page size I want.

Conclusion

  • I’ve added two dependencies: the Mongo driver and Spring Data Mongo.
  • I’ve extended an abstracted configuration of the Mongo client to indicate the database name and the credentials to connect to the database.
  • I have to take into account that the IDs with Mongo are strings and no more longs.
  • I’ve created the entities to reflect the schema in Mongo.
  • And I’ve created the Spring Data Repositories with my needed queries. I’ve used some build-in queries and some custom queries.

References

Repository

My New ebook, How to Master Git With 20 Commands, is available now.

Leave a comment

A WordPress.com Website.