在JSON响应中返回一个延迟加载的实体

huangapple go评论67阅读模式
英文:

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 的截图:

在JSON响应中返回一个延迟加载的实体

当我调试我的控制器的操作时:

@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);
}

看起来像是 foundPlayerplayerToDto 都有像这样的 Club

在JSON响应中返回一个延迟加载的实体

但当我执行 foundPlayer.getClub().getName() 时,我得到了一个正确的名称。我知道这可能是预期的行为,但我希望在我的响应中像这样返回 Club(如果设置了 EAGER,则来自响应的截图):

在JSON响应中返回一个延迟加载的实体

而不必将拉取类型设置为 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:

在JSON响应中返回一个延迟加载的实体

When I debug my controller's action:

@GetMapping(&quot;/api/players/{id}&quot;)
    ResponseEntity&lt;PlayerDto&gt; 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:

在JSON响应中返回一个延迟加载的实体

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):
在JSON响应中返回一个延迟加载的实体

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 = &quot;club&quot;, cascade = CascadeType.PERSIST, fetch = FetchType.LAZY)
    @JsonBackReference
    private List&lt;Player&gt; players;

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

 @Override
    public Player getPlayer(Long id) {
        Optional&lt;Player&gt; 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(&quot;/api/players/{id}&quot;)
    ResponseEntity&lt;PlayerDto&gt; 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(&quot;/api/players&quot;)
    ResponseEntity&lt;List&lt;PlayerDto&gt;&gt; getAllPlayers() {
        List&lt;PlayerDto&gt; playersList = playerInterface.getAllPlayers().stream().map(this::convertToDto).collect(Collectors.toList());
        playersList.forEach(playerInterface::fetchClubToPlayer);
        return ResponseEntity.ok().body(playersList);
    }

    @GetMapping(&quot;/api/players/{id}&quot;)
    ResponseEntity&lt;PlayerDto&gt; 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&lt;Player, Long&gt;{
   ...
   @EntityGraph(attributePaths = {&quot;club&quot;})
   Optional&lt;Player&gt; 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.

huangapple
  • 本文由 发表于 2023年1月8日 23:37:45
  • 转载请务必保留本文链接:https://go.coder-hub.com/75049080.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定