英文:
Returning a lazy loaded entity in the JSON response
问题
我遇到了关于我的 Club
实体的问题 - 我正在使用 LAZY
拉取类型和 ModelMapper
来返回我的 JSON。问题是,如果我使用 LAZY
而不是 EAGER
,那么当我使用 GET
/api/players/{id}
时,我得到的响应是:
Resolved [org.springframework.http.converter.HttpMessageNotWritableException: Could not write JSON: could not initialize proxy
以及 Postman 的截图:
当我调试我的控制器的操作时:
@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);
}
看起来像是 foundPlayer
和 playerToDto
都有像这样的 Club
:
但当我执行 foundPlayer.getClub().getName()
时,我得到了一个正确的名称。我知道这可能是预期的行为,但我希望在我的响应中像这样返回 Club
(如果设置了 EAGER
,则来自响应的截图):
而不必将拉取类型设置为 EAGER
。
我的 Player
实体:
@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;
我的 Club
实体:
@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;
PlayerService
中的 getPlayer
方法(控制器调用的方法):
@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;
}
}
英文:
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;
}
}
答案1
得分: 1
你说得对,这是懒加载的预期行为。这是一件好事,不要设置为急加载!而是在响应体中创建一个ClubDto,并使用另一个convertToDto方法初始化它。这有点繁琐(我喜欢使用Mapstruct和Lombok来简化这个过程),但它会让Hibernate执行你需要的所有查询。
@Data
public class ClubDto {
private String id;
private String name;
}
@Mapper
public interface ClubMapper {
public ClubDTO mapToDto(Club club);
}
哎呀,我没意识到你已经在使用ModelMapper了。我对它不太熟悉,但听起来如果你将Club替换为ClubDto,它应该可以正常工作。
英文:
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.
答案2
得分: 0
我有一个解决方案,但我想听听你们的意见,看看是否可以这样做,或者这是某种反模式。
我只是简单地将playerToDto
的Club设置为全新获取的Club
,其ID为foundPlayer
的ID。
@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);
}
最后我得出了这个结论:
@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;
}
这样可以吗?
英文:
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?
答案3
得分: 0
建议您使用 @EntityGraph
来配置生成方法的查询获取计划。例如,您可以在 PlayerRepository
中声明一个方法,通过该方法可以按id查找 Player
实体,除了默认的 findById
方法之外,该方法会急切地获取其 Club
实体。
public interface PlayerRepository extends JpaRepository<Player, Long>{
...
@EntityGraph(attributePaths = {"club"})
Optional<Player> findWithClubFetchedEagerlyById(LongId);
}
通过提供 attributePaths
,可以定义应该急切获取的字段。
如果在调用 findById
方法时 Club
实体应该始终急切获取,那么就不需要单独的方法,因此可以使用 @EntityGraph
注释默认方法。
通过这种解决方案,可以最小化网络遍历,因为可以一次从数据库中获取所有需要的数据。
英文:
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.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论