Home > Back-end >  OneToMany lazy initialization when needing collection data
OneToMany lazy initialization when needing collection data

Time:01-09

what's a workaround if I have a relation OneToMany and would like to access the collection that is lazy loaded? Currently I get LazyInitializationException having this:

Club entity:

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

Player entity:

Is it a good idea to have two methods, where one fetches data without players and the second one that fetches also players?

    @Override
    List<Club> findAll();

    @Query("Select clubs from Club clubs left join fetch clubs.players")
    List<Club> getAllClubsWithPlayers();

What I'm thinking of is that it is a bad idea, because if I have a situation where I have for example 4 properties that are lazy loaded and I'd need at once 3 of them, I'd have to have a query like: getAllClubsWithPlayersAndSponsorsAndCoaches, so I'd have to have a lot of combinations of such queries. I don't want to use EAGER, so could you tell me if it's a common way to do this if I need access to players from the Club sometimes which is undoable with the findAll() method that throws LazyInitializationException?

Don't understand me wrong - I know where the LazyInitializationException comes from, but I just don't know what's the best way to get access to players if I sometimes need them when fetching clubs. Is my way of doing it correct?

CodePudding user response:

There are 3 choices:

  1. Access all the lazy fields inside a @Transactional method. You don't show your code, but there's usually a Service Facade layer which is responsible for being @Transactional. It invokes Repositories.
  2. Write a query that fetches all the required data. Then you'd have to create a method specifically to fetch all the lazy fields required for that logic.
  3. Use OpenSessionInViewFilter or OpenSessionInViewInterceptor so that Session/EntityManager are started before the execution even reaches the Controller. The Session then would be closed by the same high-level layer at the end of the request processing.

CodePudding user response:

In addition to what Stanislav wrote, I'd like to elaborate on his 2nd point, because I think that this is often the best approach - that's simply because it saves unnecessary calls to the database which results in better performance.

Apart from writing separate JPQL query in your repository for each use-case, you could do one of the following .:

  1. Make your repository extend JpaSpecificationExecutor and programmatically describe what needs to be fetched as described in this answer

  2. Use Entity Graph either described using annotations, or programmatically, and fetch your entities using EntityManager as described in this tutorial

CodePudding user response:

To optionally load what you want you can use EntityGraph.

Declare @NamedEntityGraph at your entity

@Entity
@NamedEntityGraph(name = "Club.players",
    attributeNodes = @NamedAttributeNode("players")
)
public class Club {

Then you should annotate your findAll() method with this graph using it's name

@EntityGraph(value = "Club.players")
List<Club> findAll();

However that would override your basic findAll() method. To avoid this (to have both implementations) you can follow this way:

Add dependency from https://mvnrepository.com/artifact/com.cosium.spring.data/spring-data-jpa-entity-graph/<version>

Then replace your repository with

@Repository
public interface ClubRepository extends JpaSpecificationExecutor<Club>, JpaRepository<Club, Long>, EntityGraphJpaSpecificationExecutor<Club> {

}

And then you'll have basic method findAll() and also from your sevice you can call

List<Club> clubs = clubRepository.findAll(specification, new NamedEntityGraph(EntityGraphType.FETCH, "Club.players"))
  • Related