Spring Data JPA - 具有无限递归的双向关系

时间:2018-04-05 09:03:46

标签: java spring-data-jpa infinite-recursion

首先,这是我的实体。

播放器

@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} =&gt; Team = {id ...,members = [Player = {id ..., team = null }]}
  • 端点 / members / {id} =&gt; Player = {id ...,team = {id ..., members = null }}

谢谢!

编辑:也许问题不是很清楚,我得到了答案,所以我会尝试更精确。

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

我想知道是否有办法告诉JPA或存储库(或任何东西)不要反复绑定实体中的实体?

3 个答案:

答案 0 :(得分:3)

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

SELECT t.*
  FROM notifications t
 INNER JOIN (SELECT DISTINCT s.post_id, s.user_id
               FROM notifications s
              WHERE belongs_to_user_id = 101
              ORDER BY post_id DESC, user_id DESC
              LIMIT 3) u
    ON u.post_id = t.post_id
   AND u.user_id = t.user_id
 WHERE t.belongs_to_user_id = 101
 ORDER BY t.id

或者像这样:

@JsonIgnoreProperties("members")
private Team team;

或两者兼而有之。

答案 1 :(得分:3)

以下是我在项目中处理此问题的方法。

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

我将包含引用实体的对象定义为List Dto(仅包含可序列化值的数据传输对象),并定义一个没有引用实体为Info的对象。

Info对象只保存有关实体本身的信息,而不是关于关系的信息。

现在,当我通过REST API传递Dto对象时,我只需要为引用添加Info个对象。

我们假设我PlayerDto超过GET /players/1

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

答案 2 :(得分:0)

在我的案例中,我通过意识到我不需要OneToMany-ManyToOne(双向)关系来解决了这个问题。

这样的问题解决了我的问题

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

// Player Class:
// - I removed the 3 lines

这里有更多示例的链接: https://github.com/thombergs/code-examples/tree/master/spring-data/spring-data-rest-associations/src/main/java/com/example/demo

项目龙目岛也产生了同样的问题。我尝试了 @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