Reputation: 107
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:
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:
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):
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;
}
}
Upvotes: 2
Views: 1050
Reputation: 5115
I suggest you use @EntityGraph
to configure the fetch plan of the resulting method's query. For example, you can declare a method in PlayerRepository
to find a Player
entity by id, apart from the default findById
method, where its Club
entity would be fetched eagerly.
public interface PlayerRepository extends JpaRepository<Player, Long>{
...
@EntityGraph(attributePaths = {"club"})
Optional<Player> findWithClubFetchedEagerlyById(LongId);
}
By providing attributePaths
, fields that should be fetched eagerly are defined.
If the Club
entity should be always fetched eagerly when you call the findById
method then there's no need for a separate method, therefore you can annotate the default one with @EntityGraph
.
With this solution, the network traversal is minimized because all needed data is fetched at once from a database.
Upvotes: 0
Reputation: 107
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?
Upvotes: 0
Reputation: 139
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.
Upvotes: 1