Skip to content

Database Relationships in JPA - One to Many

Learning Objectives

  • Understand the One-to-many relationship
  • Be able to implement a One-to-many relationship using Hibernate/JPA
  • Be able to annotate models to set up a database schema describing a one-to-many relationship
  • Appreciate why @JsonIgnoreProperties is used to partially suppress output
  • Be able to correctly format a POST request when another table is being referenced

Introduction

So far, we've created some models, some controllers, added some endpoints using multiple HTTP methods such as GET and POST, and even learned a little bit about the importance of separation of concerns, including how to apply this best practice with the use of a service layer in our application. We've even learned the essentials of persisting data to a Postgres DB via JPA Repositories. So far, however, our models are unrelated to one another. In practice we often need to track relationships between records in our DB. We will be learning about the types of relationships we can create, and how to best track them, in our next couple of lessons.

One-to-many Relationships

The first type of relationship we will learn about is the one-to-many. You actually worked with one-to-many examples in the SQL lessons, but let's do a little refresher; the best way to understand the different types of relationships is often by considering several theoretical, as well as more concrete, real-world examples. Firstly, think of a bowl of fruit:

---
title: Fruit Example
---

classDiagram
    note "one to many"
    FruitBowl "1" -- "!..*" Fruit
    class FruitBowl{
        - ArrayList< Fruit > fruit
    }
    class Fruit{
        - FruitBowl fruitBowl
    }

If we were to represent this relationship in code, it would be one-to-many - a bowl can have many fruit (in a collection, such as an ArrayList), whereas a fruit can be in only one bowl, not several. Another example of one-to-many would be the relationsip between a car dealership and the cars in it's inventory:

---
title: Car Dealership Example
---

classDiagram
    note "one to many"
    CarDealership "1" -- "!..*" Car
    class CarDealership{
        - ArrayList< Car > cars
    }
    class Car{
        - CarDealership dealership
    }

Hopefully those examples help to make the concept of a one-to-many relationship a bit clearer for you. But, you may wonder what would be a common example you'd come accross in real-world programming scenarios? In eCommerce, a great example would be the relationship between a customer and their purchases:

---
title: eCommerce Example
---

classDiagram
    note "one to many"
    Customer "1" -- "!..*" Purchase
    class Customer{
        - ArrayList< Purchase > purchases
        // other properties
    }
    class Purchase{
        - Customer customer
        // other properties
    }

Creating One-to-Many Relationships in Hibernate/JPA

Thanksfully, a lot of the grunt work of creating relationships between database tables/records is handled for us by Hibernate/JPA. We simply need to mark certain elements of our models and their properties with the appropriate annotations, so Hibernate knows what to do. In our guessing game project, we are going to create a one-to-many relationship between games and players - A player can play many different games over time, but since our example is a single-player game, a game can have only one player. The basic steps we will need to follow are as follows:

  • Create a Player model, and give it a collection of all the games the player has played
  • Annotate this collection as @OneToMany
  • Add a player property to the Game model
  • Annotate this property as @ManyToOne

Some other tasks we will complete, which don't relate directly to the one-to-many concept, but are still essential, are to create a PlayerService, a PlayerController, and a PlayerRepository.

The Player Model

Let's start by adding a Player model to our models package, alongside the other models, such as Game, LetterList, Reply etc. The first thing you'll want to do is open this new class up and tell Hibernate that it is an entity, using @Entity:

models/Player.java
// models/Player.java

@Entity
@Table(name = "players")
public class Player {

}

By setting the name attribute to players, we're telling Hibernate that we want the table in the DB to be called players (plural). Otherwise, it would default to the name of the class (Player), which might not cause any errors, but would break with the accepted convention for naming DB tables.

Next, we'll add our private properties:

models/Player.java
// models/Player.java

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private long id;

    @Column(name = "name")
    private String name;

    private List<Game> games;

Which of the above properties do you think relates to the one-to-many relationship? That's right, games! So we need to add an annotation to mark it as such. First we'll add the @OneToMany annotation (one player to many games).

models/Player.java
// models/Player.java

    // ...

    @OneToMany(mappedBy = "player")
    private List<Game> games;

Now that our property is annotated like this Hibernate will attempt to set up a relationship between players and another table. In SQL terms we need to define a foreign key somewhere, but Hibernate needs us to tell it where to make the link. This information is derived from the datatype of the annotated property. In this case we have a list of Game objects, so Hibernate will set up the relationship to the table defined by the Game model.

Wealso need to add the mappedBy argument to tell Hibernate which property of Game will represent the foreign key. We haven't refactored Game yet so this property currently doesn't exist, but ultimately it will have a player property which

Next, let's write our constructor. It only needs to accept a name parameter to set the property, plus initialise an empty list of games. We'll also add a default constructor and getters and setters for each property.

models/Player.java
// models/PLayer.java

public Player(String name) {
    this.name = name;
    this.games = new ArrayList<>();
}

// default constructor

// getters & setters

And that's everything for the Player model - next, we'll create our repository, service and controller.

The Player Service & Repository

We need a service class to, essentially, act as the manager for our player entity, and handle all dealings with the player repository, which we will also create in this step. Let's start by adding an interface called PlayerRepository to the repositories package - functionally, this is much the same as the GameRepository interface we created previously:

repositories/PlayerRepository.java
// repositories/PlayerRepository.java

public interface PlayerRepository extends JpaRepository<Player, Long> {

}

Since this is almost identical to GameRepository, there's nothing particularly new here, so let's move on. Please create a class called PlayerService in the services package. First, at the top of our new PlayerService class, we need to add the PlayerRepository as a dependency, so we can use it's methods.

services/PlayerService.java
// services/PlayerService.java

@Service
public class PlayerService{

    @Autowired
    PlayerRepository playerRepository;

}

The @Autowired annotation makes sure that a PlayerRepository object gets injected into our PlayerService object upon creation.

Next, we will add 3 methods which are going to be very useful to us in the PlayerController - getAllPlayers, getPlayerById, and savePlayer, all of which are pretty self-explanatory:

services/PlayerService.java
// services/PlayerService.java

public List<Player> getAllPlayers(){
    return playerRepository.findAll();
}

public Optional<Player> getPlayerById(Long id){
    return playerRepository.findById(id);
}

public Player savePlayer(Player player){
    playerRepository.save(player);
    return player;
}

The Player Controller

Now that we have our model and service classes, we'll need a controller to handle the HTTP requests from the end user. Please create a new class called PlayerController in the controllers package. Firstly, add the @RestController and @RequestMapping annotations to it, as we did with GameController:

controllers/PLayerController.java
// controllers/PlayerController.java

@RestController
@RequestMapping(value = "/players")
public class PlayerController {

}

As a brief reminder, @RestController is basically @Controller and @ResponseBody smushed together in one tidy little annotation. It marks the class as a controller for Spring, and indicates that all methods within should return serialized JSON, wihtout having to explicitly state this by adding the @ResponseBody annotation to all of them individually. @RequestMapping is used to make /players the base of all endpoints on this controller.

Next, we'll add our PlayerService as a dependency with @Autowired:

controllers/PLayerController.java
// controllers/PlayerController.java

@RestController
@RequestMapping(value = "/players")
public class PlayerController {

    @Autowired
    PLayerService playerService;

}

Doing so gives us access to the methods of PlayerService. To finish off our PlayerController, we'll need to add 3 mappings: getAllPlayers, getPlayerById and addNewPlayer, the function of which is self-explanatory:

controllers/PlayerController.java
// controllers/PlayerController.java

    @GetMapping
    public ResponseEntity<List<Player>> getAllPlayers(){
        List<Player> players = playerService.getAllPlayers();
        return new ResponseEntity<>(players, HttpStatus.OK);
    }

    @GetMapping(value = "/{id}")
    public ResponseEntity<Player> getPlayerById(@PathVariable Long id){
        Optional<Player> player = playerService.getPlayerById(id);
        if (player.isPresent()){
        return new ResponseEntity<>(player.get(), HttpStatus.OK);
        } else {
            return new ResponseEntity<>(null, HttpStatus.NOT_FOUND);
        }
    }

    @PostMapping
    public ResponseEntity<Player> addNewPlayer(@RequestBody Player player){
        Player savedPlayer = playerService.savePlayer(player);
        return new ResponseEntity<>(savedPlayer, HttpStatus.CREATED);
    }

And that's all of our player classes implemented. Well done!

Updating the Game Model

Next, we need to update the Game model so that it has a property called player, which references the the Player object which is associated with the game - this is the many side of the one-to-many relationship. Please add the following private property after your existing private properties in the Game class:

models/Game.java
// models/Game.java

@ManyToOne
@JoinColumn(name = "player_id")
private Player player;

Notice that, on the many side of the one-to-many relationship, we use the @ManyToOne annotation, instead of @OneToMany, which should only be used on the one side. @JoinColumn specifies the name of the column to use for the foreign key of the player in the SQL table - in this case, player_id. @JoinColumn must be used on the many side of the relationship in a one-to-many relationship. It wouldn't work if we put it in the Player class instead of Game.

Please take a look at the existing constructor in Game - you'll notice that, presently, it only accepts arguments for the word and curentState but not for the player. Let's update it to accept a player argument, and assign it to the player property we just created:

models/Game.java
// models/Game.java

public Game(String word, String currentState Player player) {       // MODIFIED
    this.word = word;
    this.currentState = currentState;
    this.guesses = 0;
    this.complete = false;
    this.player = player;                   // ADDED
}

Excellent. We're almost finished updating the Game model - but, remember that Game, being a model, should be a POJO, and that POJOs should have getters and setters for all private properties. This means that we also need to add in a getter and a setter for the player property, like so:

models/Game.java
// models/Game.java

public Player getPlayer() {
    return player;
}

public void setPlayer(Player player) {
    this.player = player;
}

Updating the Game Service

And that's everything for the model. The GameService class will also need to be updated however - specifically, the startNewGame method, since it calls the Game constructor, which now requires a player object as a third argument. We need to update the existing startNewGame method, but also use @Autowired to connect the PlayerService and give us the functionality to get the player details.

services/GameService.java
// services/GameService.java

@Service
public class GameService{

    @Autowired
    PlayerService playerService;                // ADDED

    public Reply startNewGame(long playerId){
        String targetWord = wordService.getRandomWord();
        String currentWordStatus = Strings.repeat("*", targetWord.length());
        Player player = playerService.getPlayerById(playerId).get();            // ADDED
        Game game = new Game(targetWord, currentWordStatus, player);            // MODIFIED
        gameRepository.save(game);
        return new Reply(
            game.getCurrentState(),
            String.format("Started new game with id %d", game.getId()),         // MODIFIED
            false
        );
    }

}

Updating the Game Controller

Now let's take a look at the GameController class, specifically the newGame mapping. In order to work correctly with GameService following our changes we need to accept a @RequestParam called playerId, and pass it to the GameService object, like so:

controllers/GameController.java
// controllers/GameService.java

@PostMapping
public ResponseEntity<Reply> newGame(@RequestParam long playerId){      // MODIFIED
    Reply reply = gameService.startNewGame(playerId);       // MODIFIED
    return new ResponseEntity<>(reply, HttpStatus.CREATED);

}

Updating the Data Loader

Now that we have changed the requirements for saving a game we need to relfect those changes in our seed data. It's not as simple as just passing id arguments to our startNewGame() calls, we need to save some players in order to generate those ids. We need to inject a PlayerService instance before saving some players, then using the id values returned to create some games.

components/DataLoader.java
// components/DataLoader.java

@Component
public class DataLoader implements ApplicationRunner {

    @Autowired
    GameService gameService;

    @Autowired
    PlayerService playerService;

    @Override
    public void run(ApplicationArguments args) throws Exception {

        Player player1 = new Player("Colin");
        Player player2 = new Player("Thibyaa");
        Player player3 = new Player("Anna");

        playerService.savePlayer(player1);
        playerService.savePlayer(player2);
        playerService.savePlayer(player3);

        gameService.startNewGame(player1.getId());
        gameService.startNewGame(player2.getId());
        gameService.startNewGame(player3.getId());
        gameService.startNewGame(player2.getId());
        gameService.startNewGame(player1.getId());
    }
}

Handling Infinite Recursion

If we test our GET requests for the /players routes we'll run into errors: Postman will abort the request while making the request from a browser shows a never-ending loop of raw JSON. What's gone wrong?

The way our data is structure is contributing to the problem. A Player has a list of Games. Each of those Games includes details of its Player, which will be the same Player as we started with! If we try to serialise all of this we end up with a never-ending chain of nested objects which Postman can't handle and a browser can only show as raw JSON.

The solution is to add another annotation which will partially suppress JSON serialisation, depending on how we request the data. We will start with the Player model and suppress the details which will be included when we serialise its list of Games.

models/Player.java
// models/Player.java

    // ...

    @OneToMany(mappedBy = "player")
    @JsonIgnoreProperties({"player"})           // ADDED
    private List<Game> games;

    // ...

@JsonIgnoreProperties does exactly what the name suggests - when a Game object is serialised as part of a Player object Spring will not include the player property. This breaks the recursive nesting and fixes our issue. We can ignore more than one property by comma-separating them in teh argument passed to the annotation.

It's important to annotate both ends of the relationship, meaning we need to update Game too.

models/Game.java
// models/Game.java

    // ...

    @ManyToOne
    @JoinColumn(name = "player_id")
    @JsonIgnoreProperties({"games"})            // ADDED
    private Player player;

    // ...

Test the Routes

Those are all the code changes we need to make - if the steps were followed successfully, then the routes should all work, but we should test them to confirm. Any GET routes can be tested in the browser, but other route will need to be tested in Postman. If you run into issues, follow the usual debugging and troubleshooting procedures to work through them.