Using MongoDB with Spring Boot Part 2

Photo by Mauro Sbicego on Unsplash
Photo by Mauro Sbicego on Unsplash

Photo by Mauro Sbicego on Unsplash

In the previous tutorial, we have seen how to connect a Spring Boot web application with MongoDB and perform write-related actions like insert, update and delete.

Storing data is good, but it is better to have the ability to retrieve this data, apply filters, sort, and others. MongoRepository provides useful methods to retrieve data. We will see how to use it in this tutorial.

Setup the project

Before we begin, we will install the project. If you followed the first part, you are all set to continue. Otherwise, we will clone the repository from GitHub and set up the project:


git clone https://github.com/tericcabrel/blog-tutorials.git

cd blog-tutorials/springboot-mongodb

cp application-example.properties application.properties

mvn install

mvn spring-boot:run

You can use your favorite Java IDE to perform the Maven dependencies installation and the project launch. Don't forget to check the previous part if you are stuck on how to consume the API.

Populate the database

Before starting, we need the data we will interact with. We could use a REST client to create 10 teams with 10 players, but it will take too much time.

We will use a library that generates fake data called JavaFaker, and we will write a method that will populate the database. This method will be executed once when the application starts.

Open pom.xml, then add the dependencies for JavaFaker.

<dependency>
    <groupId>com.github.javafaker</groupId>
    <artifactId>javafaker</artifactId>
    <version>1.0.2</version>
</dependency>

Create a package called bootstrap and create a file DatabaseSeeder.java, then add the code below.

package com.soccer.mongo.bootstrap;

// import here

@Component
public class DatabaseSeeder implements ApplicationListener<ContextRefreshedEvent> {
  private final TeamRepository teamRepository;

  private final PlayerRepository playerRepository;

  private final Faker faker;

  public DatabaseSeeder(TeamRepository teamRepository, PlayerRepository playerRepository) {
    this.teamRepository = teamRepository;
    this.playerRepository = playerRepository;
    this.faker = new Faker(Locale.FRANCE);
  }

  @Override
  public void onApplicationEvent(ContextRefreshedEvent contextRefreshedEvent) {
    teamRepository.deleteAll();
    playerRepository.deleteAll();

    Set<String> teamNames = new HashSet<>();
    int counter = 0;

    while (counter < 10) {
      String teamName = faker.team().name();

      if (!teamNames.contains(teamName)) {
        teamNames.add(teamName);
        counter++;
      }
    }

    for(String teamName: teamNames) {
      createAndPersistTeam(teamName);
    }
  }

  private void createAndPersistTeam(String teamName) {
    List<Player> players = new ArrayList<>() {{
      add(createPlayer());
      add(createPlayer());
      add(createPlayer());
      add(createPlayer());
      add(createPlayer());
      add(createPlayer());
      add(createPlayer());
      add(createPlayer());
      add(createPlayer());
      add(createPlayer());
      add(createPlayer());
    }};

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

    Address address = new Address(
      faker.address().city(),
      faker.address().zipCode(),
      faker.address().streetAddress()
    );

    Team team = new Team()
        .setName(teamName)
        .setAcronym(teamName.replaceAll(" ", "").toUpperCase().substring(0, 4))
        .setAddress(address)
        .setPlayers(new HashSet<>(createdPlayers));

    teamRepository.save(team);
  }

  private Player createPlayer() {
    PlayerPosition[] positions = PlayerPosition.toArray();

    return new Player()
        .setName(faker.name().firstName() + " " + faker.name().lastName())
        .setBirthDate(faker.date().birthday(18, 38))
        .setPosition(positions[faker.random().nextInt(0, positions.length - 1)])
        .setAvailable(faker.random().nextBoolean());
  }
}

Update the file PlayerPosition.java by adding the code below:

public static PlayerPosition[] toArray() {
    return new PlayerPosition[] {
        GOALKEEPER,
        LEFT_FULLBACK,
        RIGHT_FULLBACK,
        CENTER_FULLBACK,
        MIDFIELDER,
        LEFT_MIDFIELDER,
        RIGHT_MIDFIELDER,
        DEFENSIVE_MIDFIELDER,
        WINGER,
        LEFT_WINGER,
        RIGHT_WINGER,
        STRIKER,
        SECOND_STRIKER,
    };
}

We create 10 teams, and for each team, we add 11 players. Run the application and when launched, check the collections of your database.

We have already done something similar with Node.js in this tutorial. Check it out if you are curious.

Retrieve many items

MongoRepository provides a method findAll() to retrieve all the documents of a collection. Add the code below in SoccerController.java:

@GetMapping("/teams")
public ResponseEntity<List<Team>> allTeams() {
  List<Team> teams = teamRepository.findAll();

  return new ResponseEntity<>(teams, HttpStatus.OK);
}

@GetMapping("/players")
public ResponseEntity<List<Player>> allPlayers() {
  List<Player> players = playerRepository.findAll();

  return new ResponseEntity<>(players, HttpStatus.OK);
}

Run the application and call the routes /teams and /players

Sorting

We can sort our documents by property in ascending or descending. The findAll() method accepts an argument of type Order:


// Sort teams by name in descending order
List<Team> teams = teamRepository.findAll(Sort.by(Direction.DESC, "name"));

// Sort player by position in ascending order and name in descending order
List<Order> orders = new ArrayList<>(){{
	add(Order.by("position").with(Direction.ASC));
	add(Order.by("name").with(Direction.DESC));
}};

List<Player> players = playerRepository.findAll(Sort.by(orders));

Retrieve one item

The method findById() is used to find a document by its ID.

@GetMapping("/teams/{id}")
public ResponseEntity<Team> oneTeam(@PathVariable String id) {
    Optional<Team> teamOptional = teamRepository.findById(id);

    return teamOptional
        .map(team -> new ResponseEntity<>(team, HttpStatus.OK))
        .orElseGet(() -> ResponseEntity.notFound().build());
}

@GetMapping("/players/{id}")
public ResponseEntity<Player> onePlayer(@PathVariable String id) {
    Optional<Player> playerOptional = playerRepository.findById(id);

    return playerOptional
        .map(player -> new ResponseEntity<>(player, HttpStatus.OK))
        .orElseGet(() -> ResponseEntity.notFound().build());

}

Retrieve with conditions

MongoRepository interface is built on top CrudRepository, which provides a powerful way to query data from various databases.

We can define a method in the Repository, and it will be parsed at runtime thanks to Java Reflection, then generates the corresponding query. Learn more about CrudRepository here.

Example: Assume we want to query players who are strikers and are not available. Here is the method that returns the data we want:

// In PlayerRepository.java
List<Player> findByPositionAndIsAvailable(PlayerPosition playerPosition, boolean isAvailable);

// In SoccerController.java
List<Player> players = playerRepository.findByPositionAndIsAvailable(PlayerPosition.STRIKER, false);

Example 2: Retrieve teams that name contains "real", ignore case, and order by name descending.

// In TeamRepository.java
List<Team> findByNameContainingIgnoreCaseOrderByNameDesc(String nameKeyword);

// In SoccerController.java
List<Team> teams = teamRepository.findByNameContainingIgnoreCaseOrderByNameDesc("real");

Example 3: Retrieve distinct players by name who are defenders or goalkeepers

// In PlayerRepository.java
List<Player> findDistinctNameByPositionIn(List<PlayerPosition> playerPositions);

// In SoccerController.java
List<PlayerPosition> playerPositions = new ArrayList<>() {{
    add(PlayerPosition.DEFENSIVE_MIDFIELDER);
    add(PlayerPosition.GOALKEEPER);
}};

List<Player> players = playerRepository.findDistinctNameByPositionIn(playerPositions);

Example 4: Retrieve players aged between 25 and 30 years old.

// In PlayerRepository.java
List<Player> findByBirthDateIsBetweenOrderByBirthDate(Date fromDate, Date toDate);

// In SoccerController.java
Calendar calendar = Calendar.getInstance();

calendar.set(1991, Calendar.JANUARY, 1);
Date fromDate = calendar.getTime();

calendar.set(1996, Calendar.JANUARY, 1);
Date toDate = calendar.getTime();

List<Player> players = playerRepository.findByBirthDateIsBetweenOrderByBirthDate(fromDate, toDate);

Example 5: Retrieve the youngest player

// In PlayerRepository.java
Player findFirstByOrderByBirthDateDesc();

// In SoccerController.java
Player player = playerRepository.findFirstByOrderByBirthDateDesc();

Example 6: Retrieve the 10 oldest players

// In PlayerRepository.java
List<Player> findFirst10ByOrderByBirthDate();

// In SoccerController.java
List<Player> players = playerRepository.findFirst10ByOrderByBirthDate();

Example 7: Retrieve teams who are based in Paris
This case shows how to query the nested field in a document. We are looking inside. team.address.city

// In TeamRepository.java
List<Team> findByAddressCityIgnoreCase(String city);

// In SoccerController.java
List<Team> teams = teamRepository.findByAddressCityIgnoreCase("Paris");

If you want to learn more about how it works, check out here.

Obviously, I can enumerate all the position methods. You can browse the documentation for more at this link.

Pagination

Spring Data makes it easy to implement index-based pagination.  Let's see how to use it on players:

// In PlayerRepository.java
Page<Player> findByIdIsNotNull(Pageable pageable);

// In SoccerController.java
@GetMapping("/players-page")
public ResponseEntity<Page<Player>> listPlayersPage(@RequestParam int page) {
    Page<Player> players = playerRepository.findByIdIsNotNull(PageRequest.of(page - 1, 10));

    return new ResponseEntity<>(players, HttpStatus.OK);
}

Run the application and browse the URL: /players-page?page=1. It returns the 10 players starting to the index (page - 1) * 10.

Conclusion

With this post and the previous one, we covered the basic concepts for building applications with SprinBoot and MongoDB. Feel free to check the documentation to see other concepts I didn't cover.

You can find the code source on the GitHub repository.

Follow me on Twitter or subscribe to my newsletter to avoid missing the upcoming posts and the tips and tricks I occasionally share.