Spring Data JPA - bidirectional relation with infinite recursion

Mickaël Bénès picture Mickaël Bénès · Apr 5, 2018 · Viewed 14.1k times · Source

First, here are my entities.

Player :

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

    // other fields

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

    // methods

}

Team :

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

    // other fields

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

    // methods

}

As many topics already stated, you can avoid the StackOverflowExeption in your WebService in many ways with Jackson.

That's cool and all but JPA still constructs an entity with infinite recursion to another entity before the serialization. This is just ugly ans the request takes much longer. Check this screenshot : IntelliJ debugger

Is there a way to fix it ? Knowing that I want different results depending on the endpoint. Examples :

  • endpoint /teams/{id} => Team={id..., members=[Player={id..., team=null}]}
  • endpoint /members/{id} => Player={id..., team={id..., members=null}}

Thank you!

EDIT : maybe the question isn't very clear giving the answers I get so I'll try to be more precise.

I know that it is possible to prevent the infinite recursion either with Jackson (@JSONIgnore, @JsonManagedReference/@JSONBackReference etc.) or by doing some mapping into DTO. The problem I still see is this : both of the above are post-query processing. The object that Spring JPA returns will still be (for example) a Team, containing a list of players, containing a team, containing a list of players, etc. etc.

I would like to know if there is a way to tell JPA or the repository (or anything) to not bind entities within entities over and over again?

Answer

Herr Derb picture Herr Derb · Apr 5, 2018

Here is how I handle this problem in my projects.

I used the concept of data transfer objects, implemented in two version: a full object and a light object.

I define a object containing the referenced entities as List as Dto (data transfer object that only holds serializable values) and I define a object without the referenced entities as Info.

A Info object only hold information about the very entity itself and not about relations.

Now when I deliver a Dto object over a REST API, I simply put Info objects for the references.

Let's assume I deliever a PlayerDto over GET /players/1:

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

Whereas the TeamInfo object looks like

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

compared to a TeamDto

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

This avoids an endless serialization and also makes a logical end for your rest resources as other wise you should be able to GET /player/1/team/player/1/team

Additionally, the concept clearly separates the data layer from the client layer (in this case the REST API), as you don't pass the actually entity object to the interface. For this, you convert the actual entity inside your service layer to a Dto or Info. I use http://modelmapper.org/ for this, as it's super easy (one short method call).

Also I fetch all referenced entities lazily. My service method which gets the entity and converts it to the Dto there for runs inside of a transaction scope, which is good practice anyway.

Lazy fetching

To tell JPA to fetch a entity lazily, simply modify your relationship annotation by defining the fetch type. The default value for this is fetch = FetchType.EAGER which in your situation is problematic. That is why you should change it to fetch = FetchType.LAZY

public class TeamEntity {

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

Likewise the Player

public class PlayerEntity {

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

When calling your repository method from your service layer, it is important, that this is happening within a @Transactional scope, otherwise, you won't be able to get the lazily referenced entity. Which would look like this:

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