3

I have two entities, which are in a many to many relationship.

@Entity
public class Room {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private long id;

    @ManyToMany(mappedBy = "rooms")
    private Set<Team> teams;
}
@Entity
public class Team {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private long id;

    @ManyToMany
    @JoinTable(name = "teams_rooms",
        joinColumns = @JoinColumn(name= "team_id"),
        inverseJoinColumns = @JoinColumn(name = "room_id"))
    private Set<Room> rooms;

}

To yield data, i have a repository for "Room" and "Team":

public interface RoomRepository extends CrudRepository<Room, Long> {
}
public interface TeamRepository extends CrudRepository<Team, Long> {
}

My goal is to request all rooms of a team, but prevent JPA from looping infinitely.

@RestController
@RequestMapping("....")
public class RoomController {
    @Autowired
    private RoomRepository roomRepository;
    
    @GetMapping
    public Iterable<Room> getAllRoomsOfTeam() {
        final long exampleId = 1; //This is just a placeholder. The id will be passed as a parameter.
        
        final var team = teamRepository.findById(exampleId);

        return ResponseEntity.ok(team);
    }
}

This is the result:

{
    "id": 1,
    "name": "Team1",
    "rooms": [
        {
            "id": 1,
            "name": "Room 1",
            "teams": [
                {
                    "id": 1,
                    "name": "Team 1",
                    "rooms": [
                        {
                            "id": 1,
                            "name": "Room 1",
                            "teams": [

Jackson will loop forever, until an exception occurs (Since the back reference also references the parent element, which will create a loop). I already tried @JsonManagedReference and @JsonBackReference, but they are used for many to one relationships.

How do i stop Jackson from looping infinitely? I want to affect other repositories and queries as little as possible.

Manuel
  • 188
  • 1
  • 12
  • If you don't have a use case where you need to get all the teams that use the rooms (fetch the teams via the rooms variable) you don't need the many-to-many but one-to-many relationship. Before proceeding with a comment regarding your problem, have you considered that approach? – Aethernite Jun 08 '21 at 12:01
  • 1
    You could check [this](https://www.baeldung.com/jackson-bidirectional-relationships-and-infinite-recursion) baeldung article as well. – Aethernite Jun 08 '21 at 12:04
  • @Aethernite i've considered this approach. However, i need to fetch both ways, which means i need a bidirectional many to many relationship. I've already read this article, but it is about **one to many** relationships with a back reference, which i have too (but they are working fine after i followed these steps). – Manuel Jun 08 '21 at 13:02
  • You can find the solution at this link https://stackoverflow.com/a/47118424/8986786 – Muhyidean Tarawneh Nov 30 '21 at 03:01

2 Answers2

3

Currently, there is cyclic dependency in your classes which is causing issues while converting objects to JSON. Please add @JsonIgnore annotation on rooms variable in your Team class as shown in below example:

import com.fasterxml.jackson.annotation.JsonIgnore;

@Entity
public class Team {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private long id;

    @ManyToMany
    @JoinTable(name = "teams_rooms",
        joinColumns = @JoinColumn(name= "team_id"),
        inverseJoinColumns = @JoinColumn(name = "room_id"))
    @JsonIgnore
    private Set<Room> rooms;

}

If you need a solution for bidirectional conversion then you can use JsonView annotation.

First of all you need to create JSON view profiles for Team and Room as shown in below example:

public class JsonViewProfiles
{
    /**
     * This profile will be used while converting Team object to JSON
     */
    public static class Team {}

    /**
     * This profile will be used while converting Room object to JSON
     */
    public static class Room {}
}

Use above created JSON view profiles in your entities as shown in below example:

public class Room {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    @JsonView({ JsonViewProfiles.Team.class, JsonViewProfiles.Room.class })
    private long id;

    @JsonView(JsonViewProfiles.Room.class)
    @ManyToMany(mappedBy = "rooms")
    private Set<Team> teams;
}
public class Team {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    @JsonView({JsonViewProfiles.Team.class, JsonViewProfiles.Room.class})
    private long id;

    @ManyToMany
    @JoinTable(name = "teams_rooms",
        joinColumns = @JoinColumn(name= "team_id"),
        inverseJoinColumns = @JoinColumn(name = "room_id"))
    @JsonView(JsonViewProfiles.Team.class)
    private Set<Room> rooms;
}

While converting your object to JSON please use these profiles as shown in below example:

@GetMapping
public String getAllRoomsOfTeam() {
    final long exampleId = 1; //This is just a placeholder. The id will be passed as a parameter.

    final Team team = teamRepository.findById(exampleId);

    String result = new ObjectMapper().writerWithView(JsonViewProfiles.Team.class)
                .writeValueAsString(team);

    return result;
}
pcsutar
  • 1,715
  • 2
  • 9
  • 14
  • This will work. But what if i want to fetch all rooms of a Team? Jackson will ignore it, since the variable is annotated with ``@JsonIgnore``. – Manuel Jun 08 '21 at 13:41
  • Do you want to perform bidirectional conversion like "room to teams" & "team to rooms"? Please confirm! – pcsutar Jun 08 '21 at 14:17
  • Updated answer for bidirectional conversion – pcsutar Jun 09 '21 at 06:28
  • Great example, thank you! The accepted answer works for me, since i also need to append custom variables in the controllers, which should not be in the database (which i can obviously do in the DTOs). But your answer is a great example of ``@JsonView``, thank you! – Manuel Jun 09 '21 at 07:32
3

Your controller shoud not return entities ( classes with the annotation @Entity). As a best practice is to create another separate class with same attributes. This code has a little dupplication but it keeps all the layers clean. I also suggest to use @Service.

   public class RoomDTO { 
       private String name;
       private List<TeamDTO> teams = new ArrayList<>();   

       public RoomDTO() { 
       }
        
       public RoomDTO(Room room) {
            this.name = room.name;
            for(Team team : room.getTeams()) {
                 TeamDTO teamDTO = new TeamDTO();
                 teamDTO.setName(team.getName);
                 teams.add(teamDTO);
            }
        }
   }



   public class TeamDTO { 
       List<RoomDTO> rooms = new ArrayList();

       public TeamDTO() {
       }

       public TeamDTO(Team team) {
            this.name = team.name;
            for(Room room : team.getRooms()) {
                 RoomDTO roomDTO = new RoomDTO();
                 roomDTO.setName(team.getName);
                 rooms.add(roomDTO);
            }
        }
        
   }

The controller should return this

@GetMapping
public Iterable<TeamDTO> getAllRoomsOfTeam() {
final long exampleId = 1;
final var team = teamRepository.findById(exampleId);

TeamDTO teamDTO = new TeamDTO(team);

return ResponseEntity.ok(teamDTO);

}

How to use DTOs in the Controller, Service and Repository pattern

Sorin Penteleiciuc
  • 653
  • 1
  • 10
  • 26