Home > Enterprise >  Returning a lazy loaded entity in the JSON response
Returning a lazy loaded entity in the JSON response

Time:01-09

I have a problem with my Club entity - I'm using LAZY fetch type and ModelMapper to return my JSON. The problem is that if I use LAZY instead of EAGER what I get as a response of GET /api/players/{id} is:

Resolved [org.springframework.http.converter.HttpMessageNotWritableException: Could not write JSON: could not initialize proxy

and a screenshot from Postman:

enter image description here

When I debug my controller's action:

@GetMapping("/api/players/{id}")
    ResponseEntity<PlayerDto> getPlayer(@PathVariable String id) {
        Player foundPlayer = playerInterface.getPlayer(Long.valueOf(id));
        PlayerDto playerToDto = convertToDto(foundPlayer);

        return ResponseEntity.ok().body(playerToDto);
    }

...

 private PlayerDto convertToDto(Player player) {
        return modelMapper.map(player, PlayerDto.class);
    }

it seems like both foundPlayer and playerToDto have the Club like this:

enter image description here

but when I do foundPlayer.getClub().getName() I get a proper name. I know it's probably expected behavior, but I would love to have the Club returned in my response like this (screenshot from the response if EAGER is set): enter image description here

without having to set the fetch type to EAGER.

My Player entity:

@Entity
public class Player {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;


    private String firstName;


    private String lastName;;

    @ManyToOne(cascade = { CascadeType.PERSIST, CascadeType.REMOVE }, fetch = FetchType.EAGER)
    @JsonManagedReference
    private Club club;

My Club entity:

@Entity
public class Club {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;

    @OneToMany(mappedBy = "club", cascade = CascadeType.PERSIST, fetch = FetchType.LAZY)
    @JsonBackReference
    private List<Player> players;

getPlayer method from the PlayerService (the one, that the controller calls):

 @Override
    public Player getPlayer(Long id) {
        Optional<Player> foundPlayer = playerRepository.findById(id);
        return foundPlayer.orElseThrow(PlayerNotFoundException::new);
    }

PlayerToDto:

package pl.ug.kchelstowski.ap.lab06.dto;

import pl.ug.kchelstowski.ap.lab06.domain.Club;

public class PlayerDto {
    private Long id;
    private String firstName;
    private String lastName;

    private Club club;

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getFirstName() {
        return firstName;
    }

    public void setFirstName(String firstName) {
        this.firstName = firstName;
    }

    public String getLastName() {
        return lastName;
    }

    public void setLastName(String lastName) {
        this.lastName = lastName;
    }

    public Club getClub() {
        return club;
    }

    public void setClub(Club club) {
        this.club = club;
    }



}

CodePudding user response:

You're right, this is the expected behavior of lazy loading. It's a good thing, don't set it to eager! Instead of returning a Club @Entity class directly on your response body, you should create a ClubDto and initialize it with another convertToDto method. It's kinda tedious (I like using Mapstruct and Lombok to alleviate that), but it'll induce Hibernate to make all the queries you need.

@Data
public class ClubDto {
    private String id;
    private String name;
}
@Mapper
public interface ClubMapper {
    public ClubDTO mapToDto(Club club);
}

Oops, didn't realize you were already using ModelMapper. I'm not too familiar with that, but it sounds like it will just work if you swap Club for ClubDto.

CodePudding user response:

I have a solution guys, but I'd like to hear from you if it can be done this way, or it is some kind of anti-pattern. I just simply set the playerToDto's Club to the brandly new fetched Club with the ID of the foundPlayer

@GetMapping("/api/players/{id}")
    ResponseEntity<PlayerDto> getPlayer(@PathVariable String id) {
        Player foundPlayer = playerInterface.getPlayer(Long.valueOf(id));
        PlayerDto playerToDto = convertToDto(foundPlayer);
        playerToDto.setClub(clubInterface.getClub(foundPlayer.getClub().getId()));

        return ResponseEntity.ok().body(playerToDto);
    }

In the end I came up with this:

@GetMapping("/api/players")
    ResponseEntity<List<PlayerDto>> getAllPlayers() {
        List<PlayerDto> playersList = playerInterface.getAllPlayers().stream().map(this::convertToDto).collect(Collectors.toList());
        playersList.forEach(playerInterface::fetchClubToPlayer);
        return ResponseEntity.ok().body(playersList);
    }

    @GetMapping("/api/players/{id}")
    ResponseEntity<PlayerDto> getPlayer(@PathVariable String id) {
        Player foundPlayer = playerInterface.getPlayer(Long.valueOf(id));
        PlayerDto playerToDto = convertToDto(foundPlayer);
        playerInterface.fetchClubToPlayer(playerToDto);
        return ResponseEntity.ok().body(playerToDto);
    }
public PlayerDto fetchClubToPlayer(PlayerDto player) {
        if (player.getClub() != null) {
            Club club = clubInterface.getClub(player.getClub().getId());
            player.setClub(club);
        }
        return player;
    }

is it fine?

  • Related