Photo by Rodolfo Cuadros on Unsplash

MongoDB is widely used to store schema-less data. One of the main reasons contributing to this is the availability of drivers for various programming languages, making adoption easier. The Java driver is used to connect MongoDB with applications written in Java. Today, we will see how to use a Spring Boot application to create, update and delete data in MongoDB for this first part.

The use case is about championship management. We have a team with many players. A team has a name, an acronym, an address formed by a city, a postal code, and a street address. A player is formed by a name, a date of birth, a position, and availability, meaning if he can play or not.
We will build an API to create a team, a player, and add a team player.

Initialize the project

Let's initialize a spring boot project through the website and select the dependencies we will need.

Create a new Spring Boot project on start.spring.io

Click on generate to launch the Download of the project zipped, extract and open it with your favorite IDE; IntelliJ is the one I recommend.

Connect to MongoDB

To connect to the database, we will configure credentials inside the application properties. Spring Data MongoDB provides keys to configure the database connection.

spring.data.mongodb.host=localhost
spring.data.mongodb.port=27017
spring.data.mongodb.username=teco
spring.data.mongodb.password=234myPasswod
spring.data.mongodb.database=footbal
spring.data.mongodb.auto-index-creation=true
Add MongoDB credentials to the application.properties file

Above, we provided credentials to connect to the database. To create a user with a password in MongoDB, you can check this tutorial. We also indicate we want indexes on properties of our entities Player and Team to be created automatically.

Note: If you want to provide credentials as a URL, you can use the property spring.data.mongodb.uri.

Run the application to make sure the connection to the database succeeds. The application will fail to start the connection to MongoDB fails.

Modeling schemas

Create a new package called models who host the model for a team and player.

Player's schema

package com.soccer.mongo.models;

// --------- import here ------------

@Document(collection = "players")
@Accessors(chain = true)
@NoArgsConstructor
@Data
public class Player {
  @MongoId(FieldType.OBJECT_ID)
  private String id;

  @Indexed
  private String name;

  private Date birthDate;

  @Indexed
  @Field(targetType = FieldType.STRING)
  private PlayerPosition position;

  private boolean isAvailable;

  private Date createdAt;

  private Date updatedAt;
}
The player entity located in models/Player.java

We use the annotation @Document to indicate the document's structure stored in the collections named "players".

@MongoId is used to indicate the property is the document's id and will be automatically generated following the Mongo ObjectId structure. The value FieldType.OBJECT_ID indicates we want to store the value as an ObjectId there are other types like String, Array, Integer, etc. But usually, ObjectId is the most used.

@Indexed indicate we want to create an index on this field, which is a good idea for filtering document by this property. You can pass an argument unique with the value "true" to set this property unique across the collection to this annotation. It is not needed here because two players of different teams can have the same name.

@Field is used to enhance the property by changing the type; like our case, MongoDB doesn't support Enum, so we need to tell Mongo to store this property's value as a string. When retrieving the data from the collection, the value will be returned back as an Enum. You can also provide a different name for the property in MongoDB.

@Field(name = "dateOfBirth")
private Date birthDate;

Player's position Enumeration

package com.soccer.mongo.models;

import lombok.Getter;

@Getter
public enum PlayerPosition {
  GOALKEEPER("goalkeeper"),
  LEFT_FULLBACK("left_fullback"),
  RIGHT_FULLBACK("right_fullback"),
  CENTER_FULLBACK("center_fullback"),
  MIDFIELDER("midfielder"),
  LEFT_MIDFIELDER("left_midfielder"),
  RIGHT_MIDFIELDER("right_midfielder"),
  DEFENSIVE_MIDFIELDER("defensive_midfielder"),
  WINGER("winger"),
  LEFT_WINGER("left_winger"),
  RIGHT_WINGER("right_winger"),
  STRIKER("striker"),
  SECOND_STRIKER("second_striker");

  private final String position;

  PlayerPosition(String position) {
    this.position = position;
  }
}
models/PlayerPosition.java

Nothing special here; we define an Enum that holds the possible positions a player can have.

Team's schema

package com.soccer.mongo.models;

// --------- import here ------------

@Document(collection = "teams")
@Accessors(chain = true)
@Data
public class Team {
  @MongoId(FieldType.OBJECT_ID)
  private String id;

  @Indexed(unique = true)
  @Field
  private String name;

  @Field
  @Indexed(unique = true)
  private String acronym;

  private Address address;

  private Date createdAt;

  private Date updatedAt;

  @DBRef
  private Set<Player> players;

  public Team() {
    this.players = new HashSet<>();
  }
}
models/Team.java

@DBRef indicates a reference to another document which is Player in our case. A set is important to note because it tells us that we can have the same player twice in the team. For instance, a Set store a list of unique elements.

The property address is an instance of the Address but is not annotated. In this case, Mongo will store this information as a nested object of the team document. If the property address was annotated with @DBRef, only the entity's id would be saved instead of the whole object.

The other annotation has already been covered before so let move on.

Address's property

package com.soccer.mongo.models;

// --------- import here ------------

@Accessors(chain = true)
@AllArgsConstructor
@Setter
@Getter
public class Address {
  private String city;

  private String postalCode;

  private String street;
}
models/Address.java

All the models are defined. Run the app and verify that those two collections, "players" and "teams" have been created in your database. There will be no document inside, which totally normal.

Repositories

A repository is an interface between our entity and the database. It provides methods to perform CRUD operations on the database. We will create one for the Player's entity and another for the Team's entity.

Create a new package called "repositories" then, create the file PlayerRepository.java and add the code below:

package com.soccer.mongo.repositories;

// --------- import here ------------

@Repository
public interface PlayerRepository extends MongoRepository<Player, String> {

}
PlayerRepository.java

Create another file called TeamRepository.java, then add the code below:

package com.soccer.mongo.repositories;

// --------- import here ------------

@Repository
public interface TeamRepository extends MongoRepository<Team, String> {

}
TeamRepository.java

The interface is empty because by extending MongoRepository, TeamRepository inherits methods to create, read, update and delete a document in teams collections. If you have a custom query to do, you can define your method for that. We will see how to create one on part 2 of the tutorial.

Controllers

To test all of this, let's create two routes to create a team and create a player.

Create a new package called controllers and create a file SoccerController.java, then add the code below:

package com.soccer.mongo.controllers;

// --------- classes import here -------

@RestController
public class SoccerController {
  TeamRepository teamRepository;

  PlayerRepository playerRepository;

  public SoccerController(TeamRepository teamRepository, PlayerRepository playerRepository) {
    this.teamRepository = teamRepository;
    this.playerRepository = playerRepository;
  }

  @PostMapping("/teams")
  public ResponseEntity<Team> createTeam(@RequestBody CreateTeamDto createTeamDto) {
    Team teamCreated = teamRepository.save(createTeamDto.toTeam());

    return new ResponseEntity<>(teamCreated, HttpStatus.CREATED);
  }

  @PostMapping("/players")
  public ResponseEntity<Player> createPlayer(@RequestBody CreatePlayerDto createPlayerDto) {
    Player playerCreated = playerRepository.save(createPlayerDto.toPlayer());

    return new ResponseEntity<>(playerCreated, HttpStatus.CREATED);
  }
}

Note: As you can see, we created a DTO object that maps the request's body content. You can find the code of these classes in the final repository on Github.

Testing routes

Let's use Postman to test our routes.

Create a team with Postman

The response contains our data as expected, but..... the property createdAt and updatedAt has the value null. Yet, the value should be set automatically at the creation of the document in the collection. What happened 🤔.

Spring Data MongoDB doesn't provide annotation to automatically set the value of those fields. However, there is a suite of events triggered at each step of the entity manipulation by the Reposority. We will use this feature to achieve our goal.

Mongo Repository Event Lifecycle

When performing change on an entity, there is a suite of events happening from the start to the end. They can be separated into two groups.

The Write Events

They are triggered when adding or updating a document in the collection like save, insert, insertList. They are listed following their trigger order:

  • onBeforeConvert: It's called before the object is converted to a document by a MongoConverter.
  • onBeforeSave: It's called before inserting or saving the document in the database.
  • onAfterSave: It's called after inserting or saving the document in the database.

The Read Events

They are triggered for the methods that read documents in the collection like find, findAndRemove, findOne, and getCollection. They are listed following their trigger order:

  • onAfterLoad: It's called after the document has been retrieved from the database.
  • onAfterConvert: after the document retrieved from the database was converted to an entity.

Following this, it is clear that the best place to set value for createdAt and updatedAt is when the event onBeforeConvert is triggered because we will not edit the object after being converted to a Document.

Create the listener

Create a new package called listeners, then create a class called MongoListener.java and add the code below:

package com.soccer.mongo.listeners;

import com.soccer.mongo.models.Team;
import org.springframework.data.mongodb.core.mapping.event.AbstractMongoEventListener;
import org.springframework.data.mongodb.core.mapping.event.BeforeConvertEvent;
import org.springframework.stereotype.Component;

import java.util.Date;

@Component
public class MongoListener extends AbstractMongoEventListener<Team> {
  @Override
  public void onBeforeConvert(BeforeConvertEvent<Team> event) {
    super.onBeforeConvert(event);

    Date dateNow = new Date();

    event.getSource().setCreatedAt(dateNow);
    event.getSource().setUpdatedAt(dateNow);
  }
}

We create a listener on the Team entity, and before converting the entity to a document, we set a value for createdAt and updatedAt to the current time.

Run the application and use Postman to create a new team and see the result.

Creation and update's date are set correctly. 🎉

The Problem with MongoListener

Now we need to the same for entity Player.  If we have 20 entities, we will do the same for each entity, which is not sustainable. We need to find a generic way to do that for all our entities with only one listener.

To achieve our goal, we will use one of the main concepts of OOP called inheritance. Since all our entity has the properties id, createdAt, and updatedAt, we will create an entity BaseModel that will become a base entity with these three properties. All our entities will inherit the latter. Here is a picture of the change to make:

Change on entities structure

With this, We only need to tell MongoListener to listen for change on the BaseModel entity, and we are all set.

package com.soccer.mongo.listeners;

import com.soccer.mongo.models.BaseModel;
// ------- other import here ---------

@Component
public class MongoListener extends AbstractMongoEventListener<BaseModel> {
  @Override
  public void onBeforeConvert(BeforeConvertEvent<BaseModel> event) {
    super.onBeforeConvert(event);

    Date dateNow = new Date();

    event.getSource().setCreatedAt(dateNow);
    event.getSource().setUpdatedAt(dateNow);
  }
}

Create a player

Create a player with Postman

As we can see, the values of createdAt and updatedAt are well set.

Update a team

Update an entity is very straightforward, as we can see below:

@PutMapping("/teams/{id}")
public ResponseEntity<Team> updateTeam(@PathVariable String id, @RequestBody CreateTeamDto createTeamDto) {
    Optional<Team> optionalTeam = teamRepository.findById(id);

    if (optionalTeam.isEmpty()) {
      return new ResponseEntity<>(null, HttpStatus.OK);
    }
    
    Team teamToUpdate = optionalTeam.get()
          .setAddress(createTeamDto.getAddress())
          .setName(createTeamDto.getName())
          .setAcronym(createTeamDto.getAcronym());

    Team teamUpdated = teamRepository.save(teamToUpdate);

    return new ResponseEntity<>(teamUpdated, HttpStatus.OK);
}
Method to update a team in SoccerController.java

Delete a team

@DeleteMapping("/teams/{id}")
public ResponseEntity<Void> deleteTeam(@PathVariable String id, @RequestBody CreateTeamDto createTeamDto) {
    teamRepository.deleteById(id);

    return new ResponseEntity<>(HttpStatus.NO_CONTENT);
}
Method to delete a team in SoccerController.java

Create many players

@PostMapping("/players/bulk")
public ResponseEntity<List<Player>> createPlayers(@RequestBody List<CreatePlayerDto> createPlayerDtoList) {
    List<Player> players = createPlayerDtoList
        .stream()
        .map(CreatePlayerDto::toPlayer)
        .collect(Collectors.toList());

    List<Player> playersCreated = playerRepository.saveAll(players);

    return new ResponseEntity<>(playersCreated, HttpStatus.CREATED);
}
Method to create many players in SoccerController.java

Here is the result in Postman

Create many players with Postman

Add players to a team.

A team can have many players, and we cannot add players to existing teams until now. The code below shows how to do that:

@PostMapping("/teams/{id}/players")
public ResponseEntity<Team> addPlayersToTeam(@PathVariable String id, @RequestBody List<String> playerIds) {
    Optional<Team> optionalTeam = teamRepository.findById(id);

    if (optionalTeam.isEmpty()) {
      return new ResponseEntity<>(null, HttpStatus.OK);
    }

    Team teamToUpdate = optionalTeam.get();

    Set<Player> playersToAdd = playerIds.stream()
        .map(playerId -> playerRepository.findById(playerId))
        .filter(Optional::isPresent)
        .map(Optional::get)
        .collect(Collectors.toSet());

    teamToUpdate.setPlayers(playersToAdd);

    Team teamUpdated =  teamRepository.save(teamToUpdate);

    return new ResponseEntity<>(teamUpdated, HttpStatus.OK);
}
Method to add players in a team in SoccerController.java

On Postman, we got the following result:

Response data of the request to add players to a team.

Conclusion

In this first part, we focused on setup a connection to MongoDB with a Spring Boot project; after that, we saw how to create the schema for our collections and finally performed action related to Write's action (insert, update, delete) in the database. In the second part of the tutorial, we will see how to perform simple queries first, then advanced queries.

You can find the full code of the project here.

I hope you find it interesting and see you at the next tutorial 😉.