Spring Data JPA - 双向关系导致无限递归问题

16

首先,这是我的实体。

玩家 :

@Entity
@JsonIdentityInfo(generator=ObjectIdGenerators.UUIDGenerator.class, 
property="id")
public class Player {

    // other fields

    @ManyToOne
    @JoinColumn(name = "pla_fk_n_teamId")
    private Team team;

    // methods

}

团队 :

@Entity
@JsonIdentityInfo(generator=ObjectIdGenerators.UUIDGenerator.class, 
property="id")
public class Team {

    // other fields

    @OneToMany(mappedBy = "team")
    private List<Player> members;

    // methods

}

如许多主题所述,您可以使用Jackson以许多方式避免WebService中的StackOverflowExeption。

但是JPA仍然在序列化之前将实体与另一个实体递归构建。这只是丑陋的,并且请求需要更长时间。检查此屏幕截图:IntelliJ debugger

有没有办法解决它?我想根据端点获得不同的结果。例如:

  • 端点/teams/{id} => Team={id…, members=[Player={id…, team=null}]}
  • 端点/members/{id} => Player={id…, team={id…, members=null}}

谢谢!

编辑:也许问题并不是很清楚,因此我将尝试更加精确。

我知道可以通过Jackson(@JSONIgnore、@JsonManagedReference/@JSONBackReference等)或通过将某些映射到DTO来防止无限递归。我仍然看到的问题是这样的:以上两种都是查询后处理。 Spring JPA返回的对象仍将是(例如)Team,其中包含玩家列表,其中包含团队,其中包含玩家列表等等。

我想知道是否有一种方法可以告诉JPA或存储库(或任何其他东西)不要一遍又一遍地绑定实体内的实体?



我在那里看到了重复的信息。如果你已经在团队实体中有成员信息,为什么还需要在球员实体中保存团队呢?REST API 不需要反映你的数据库模式。 - Herr Derb
为什么需要在玩家实体中保存团队?因为我想要一个完整的实体参考。当我检索一个玩家时,我希望提供关于他的所有信息。 - Mickaël Bénès
@JsonManagedReference/@JsonBackReference 对我来说很有效,可以防止在序列化GET时出现递归,即使使用了FetchType.EAGER。我没有收到任何指示未绑定递归的堆栈跟踪。也许你的调试器加剧了问题,因为它要求的比Jackson更多。 - undefined
3个回答

12

以下是我在项目中如何处理这个问题的方法。

我使用数据传输对象的概念,实现了两个版本:一个完整的对象和一个轻量级对象。

我定义一个对象,将引用实体作为列表包含在内,称之为Dto(只包含可序列化值的数据传输对象),并且我定义一个不包含引用实体的对象,称之为Info

Info对象仅包含有关实体本身而非关系的信息。

现在当我通过REST API传递Dto对象时,我只需为引用使用Info对象即可。

假设我通过GET /players/1获取PlayerDto

public class PlayerDto{
   private String playerName;
   private String playercountry;
   private TeamInfo;
}

TeamInfo 对象看起来像:

public class TeamInfo {
    private String teamName;
    private String teamColor;
}

TeamDto相比

public class TeamDto{
    private String teamName;
    private String teamColor;
    private List<PlayerInfo> players;
}

这样可以避免无尽的序列化,并且为您的其余资源提供一个逻辑终点,否则您应该能够 GET /player/1/team/player/1/team

此外,该概念清晰地将数据层与客户端层(在本例中为REST API)分离,因为您不会将实际实体对象传递给接口。为此,您需要在服务层内部将实际实体转换为DtoInfo。我使用http://modelmapper.org/完成这项任务,因为它非常容易(只需一个短方法调用)。

此外,我惰性地获取所有引用实体。我的服务方法获取实体并将其转换为Dto,因此在事务范围内运行,这是很好的做法。

惰性获取

要告诉JPA惰性获取实体,只需通过定义获取类型来修改关系注释。默认值为fetch = FetchType.EAGER,在您的情况下会出现问题。这就是为什么您应该将其更改为fetch = FetchType.LAZY

public class TeamEntity {

    @OneToMany(mappedBy = "team",fetch = FetchType.LAZY)
    private List<PlayerEntity> members;
}

同样地,Player(播放器)

public class PlayerEntity {

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "pla_fk_n_teamId")
    private TeamEntity team;
}

在从服务层调用您的存储库方法时,重要的是要确保这是在 @Transactional 范围内发生的,否则,您将无法获取懒惰引用的实体。具体而言,可能会出现以下情况:

 @Transactional(readOnly = true)
public TeamDto getTeamByName(String teamName){
    TeamEntity entity= teamRepository.getTeamByName(teamName);
    return modelMapper.map(entity,TeamDto.class);
}

1
是的,经过一些研究,我遇到了DTO来解决我遇到的问题。再次感觉很酷,但是映射之前查询的结果仍然是相同的!难道没有办法告诉JPA(或者仓库,不知道)不要一遍又一遍地绑定实体吗? - Mickaël Bénès
这正是懒加载的用途。默认的获取类型是FetchType.EAGER。你不想要这个。只需修改你的关系注释(@ManyToOne)为@ManyToOne(fetch = FetchType.LAZY)。这需要一个活动事务范围来获取引用实体,但仅在需要时才会加载它。 - Herr Derb
好的,我按照你的要求做了(LAZY加载和事务范围),只是还没有进行任何DTO映射来查看结果,但我再次收到了StackOverflowError错误。我不明白,因为在调试器中我清楚地看到我的球员列表包含一个Team对象,但所有字段都为null。 - Mickaël Bénès
哦,我甚至在Player和Team实体上都删除了@JsonIdentityInfo。这可能就是我遇到错误的原因,这意味着我需要LAZY获取、事务范围和JSON序列化参数化才能得到我需要的内容。本以为LAZY获取就足够了。:P - Mickaël Bénès
如果序列化发生在事务范围内并且您删除了@JsonIdentityInfo,它将继续递归操作 ;) Dto会防止这种情况发生,因为它具有定义的深度。 - Herr Derb
看来我无法避免使用DTO,它似乎比让Jackson完成所有工作更加清晰。非常感谢您的帮助! - Mickaël Bénès

8

在我的情况下,我意识到我不需要双向(一对多-多对一)的关系。

这解决了我的问题:

// Team Class:
@OneToMany(fetch = FetchType.LAZY, cascade = CascadeType.ALL)
private Set<Player> members = new HashSet<Player>();

// Player Class - These three lines removed:
// @ManyToOne
// @JoinColumn(name = "pla_fk_n_teamId")
// private Team team;

如果您正在使用 Lombok,可能会出现此问题。请尝试添加 @ToString@EqualsAndHashCode

@Data
@Entity

@EqualsAndHashCode(exclude = { "members"}) // This,
@ToString(exclude = { "members"}) // and this

public class Team implements Serializable {

// ...


这是一份关于无限递归注释的良好指南:https://www.baeldung.com/jackson-bidirectional-relationships-and-infinite-recursion


哦,天啊。我希望我能给你100个赞。我尝试了所有的方法,却没有意识到是Lombok引起了问题。这解决了我的问题。 - cbmeeks
1
此外,如果您的实体具有像我的一样许多属性,您可以使用: @EqualsAndHashCode(onlyExplicitlyIncluded = true) @ToString(onlyExplicitlyIncluded = true) 然后,在您想要包含的每个成员上,只需注释@EqualsAndHashCode.Include即可。 - cbmeeks

6
您可以使用@JsonIgnoreProperties注解来避免无限循环,示例如下:

@JsonIgnoreProperties

@JsonIgnoreProperties("members")
private Team team;

或者像这样:
@JsonIgnoreProperties("team")
private List<Player> members;

或者两者都可以。


1
它避免了序列化时的无限循环,但JPA仍然构建了一个包含无限实体的实体。 - Mickaël Bénès
它们上面没有任何覆盖。这与我的问题有什么关系? - Mickaël Bénès
@MickaëlBénès 只需懒惰地获取引用即可。通常情况下,没有必要急切地获取引用实体。 - Herr Derb

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接