【问题标题】:Spring Data JPA - bidirectional relation with infinite recursionSpring Data JPA - 无限递归的双向关系
【发布时间】:2018-04-05 09:03:46
【问题描述】:

首先,这是我的实体。

播放器

@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 不需要反映您的数据库方案。
  • “为什么你需要在玩家实体中保存团队”因为我想要一个实体的完整参考。当我找回一个玩家时,我想提供关于他的所有信息。

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


【解决方案1】:

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

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

我将包含引用实体的对象定义为 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);
}

【讨论】:

  • 是的,我遇到了 DTO 来解决我在研究后遇到的问题。再一次很酷但是查询的结果在映射之前仍然是一样的!有没有办法告诉 JPA(或存储库,不知道)不要一遍又一遍地绑定实体?
  • 这正是延迟抓取的用途。默认获取类型是 FetchType.EAGER 。你不想要这个。只需将您的关系注释 (@ManyToOne) 修改为 @ManyToOne(fetch = FetchType.LAZY)。这需要一个活动的事务范围来获取引用的实体,但只有在这样的情况下才会加载它。
  • 好吧,我按照你说的做了(LAZY fetching 和 Transactional 范围),只是没有做任何 DTO 映射来查看结果,我再次得到 StackOverflowError。我不明白,因为我在调试器中清楚地看到我的玩家列表包含一个 Team 对象,但它的所有字段都为空。
  • 哦,我什至删除了 Player 和 Team 实体上的 @JsonIdentityInfo。这可能就是我收到错误的原因,这意味着我需要 LAZY 获取、事务范围和参数化的 JSON 序列化来获得我需要的东西。认为 LAZY 获取就足够了。 :P
  • 如果序列化发生在事务范围内并且您删除了@JsonIdentityInfo,它将继续进行递归;)Dto 将阻止这种情况,因为它具有定义的深度。
【解决方案2】:

就我而言,我意识到我不需要双向(一对多-多对一)关系。

这解决了我的问题:

// 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;

Project Lombok 也可能产生此问题。如果您使用 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

【讨论】:

    【解决方案3】:

    你可以使用@JsonIgnoreProperties注解来避免死循环,像这样:

    @JsonIgnoreProperties("members")
    private Team team;
    

    或者像这样:

    @JsonIgnoreProperties("team")
    private List<Player> members;
    

    或两者兼而有之。

    【讨论】:

    • 它避免了序列化时的无限循环,但JPA仍然构建了具有无限个实体的实体。
    • 不要对它们进行任何覆盖。这跟我的问题有什么关系?
    • @MickaëlBénès 只是懒惰地获取参考。通常没有理由急切地获取引用的实体。
    猜你喜欢
    • 1970-01-01
    • 2019-07-19
    • 2018-09-23
    • 1970-01-01
    • 2018-11-06
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多