4

I've a spring boot application which uses Hibernate as an ORM and DGS framework as the graphql engine. I've been struggling with finding ways to initialize a lazy loaded collection, the proper way. I've the following scenario:

application.properties
# The below has been set to false to get rid of the anti-pattern stuff it introduces
spring.jpa.open-in-view=false
...
@Entity
public class User {
    @Id
    @GeneratedValue
    private UUID id;

    @OneToMany(mappedBy = "user", fetch = FetchType.LAZY)
    private List<Article> articles;

    ...

}
@Entity
public class Article {
    @Id
    @GeneratedValue
    private UUID id;

    @ManyToOne(optional = false, fetch = FetchType.LAZY)
    private User user;

    ...
}

My User data fetcher looks something like this:

@DgsComponent
public class UserDataFetcher {
    @Autowired
    private UserService userService;

    @DgsQuery
    public User getUserById(@InputArgument UUID id) {
        return userService.findById(id);
    }
    ...
}

My UserService looks something like this:

@Service
public class UserServiceImpl implements UserService {

    @Autowired
    private UserRepository userRepository;

    @Override
    public User findById(UUID id) {
        return userRepository.findById(id).orElseThrow(DgsEntityNotFoundException::new);
    }
    ...
}

Now, I only want to initialize/load my articles collections from the DB when the user asks for it in the graphql query. For that purpose I created a child resolver for my articles which only executes when a user asks for the article in the query. My UserDataFetcher started looking like this:

@DgsComponent
public class UserDataFetcher {
    @Autowired
    private UserService userService;

    @DgsQuery
    public User getUserById(@InputArgument UUID id) {
        return userService.findById(id);
    }

    @DgsData(parentType = "User", field = "articles")
    public List<Article> getArticle(DgsDataFetchingEnvironment dfe) {
        User user = dfe.getSource();
        Hibernate.initialize(user.getArticles());
        return user.getArticles();
    }
    ...
}

But, the above started throwing exceptions telling me that Hibernate couldn't find an open session for the above request. Which made sense because there wasn't any so I put a @Transactional on top of my child resolver and it started looking like this:

@DgsComponent
public class UserDataFetcher {
    @Autowired
    private UserService userService;

    @DgsQuery
    public User getUserById(@InputArgument UUID id) {
        return userService.findById(id);
    }

    @DgsData(parentType = "User", field = "articles")
    @Transactional
    public List<Article> getArticle(DgsDataFetchingEnvironment dfe) {
        User user = dfe.getSource();
        Hibernate.initialize(user.getArticles());
        return user.getArticles();
    }
    ...
}

However, the above didn't work either. I tried moving this @Transactional into my service layer as well but even then it didn't work and it throwed the same exception. After much deliberation, I founded out that (maybe) Hibernate.initialize(...) only works if I call it in the initial transaction, the one which fetched me my user in the first place. Meaning, it's of no use to me since my use-case is very user-driven. I ONLY want to get this when my user asks for it, and this is always going to be in some other part of my application outside of the parent transaction.

I am looking for solutions other than the following:

  1. Changing the child resolver to something like this:
    @DgsData(parentType = "User", field = "articles")
    @Transactional
    public List<Article> getArticle(DgsDataFetchingEnvironment dfe) {
        User user = dfe.getSource();
        List<Article> articles = articlesRepository.getArticlesByUserId(user.getUserId);
        return articles;
    }

I am not in the favor of the above solution since I feel this is under-utilizing the ORM itself by trying to resolve the relation yourself rather than letting hibernate itself do it. (Correct me if I wrong thinking this way)

  1. Changing my User entity to use FetchMode.JOIN.
@Entity
public class User {
    @Id
    @GeneratedValue
    private UUID id;

    @OneToMany(mappedBy = "user", fetch = FetchType.LAZY)
    @Fetch(FetchMode.JOIN)
    private List<Article> articles;
    ...

}

This is the same as telling hibernate to eagerly load the below collection no matter what. I don't want this either.

  1. Setting spring.jpa.open-in-view=false to spring.jpa.open-in-view=true. Not in the favor of this either since this is just a band aid for LazyInitializationExceptions.

  2. Any other solutions that just makes your forget about LazyInitializationException by keeping the session open throughout the lifecycle of the request.

Adil Waqar
  • 41
  • 2
  • You need to open a new session and reattach the detached entity. For example by calling `update`. – Boris the Spider Sep 11 '21 at 14:18
  • Doing this just instantly resolves all the relations in the entity without me specifying which one I want to resolve, even before calling `Hibernate.initialize(...)`. I don't want this. I want to explicitly specify what I want to resolve. – Adil Waqar Sep 11 '21 at 16:01

2 Answers2

2

Please note this answers assumes that Spring Data JPA can be used.

Helpful can be full dynamic usage of EntityGraphs

Entity Graphs give us a possibility to define fetch plans and declare which relations (attributes) have to be queried from the database.

According to the documentation

You can do something similar to this

productRepository.findById(1L, EntityGraphUtils.fromAttributePaths(“article, “comments”));

And pass all necessary params (relations) based on user selection to the EntityGraphUtils.fromAttributePaths method. This give us possibility to fetch only necessary data.

Additional resources:

  1. Sample project
  2. Spring Blog mentioned this extension
  3. JPA EntityGraph
  4. EntityGraph
Dharman
  • 30,962
  • 25
  • 85
  • 135
user12176589
  • 107
  • 1
1

Another workaround I've used is to skip any child resolver and just load additional entities conditionally in the base resolver.

@DgsQuery
public User getUserById(@InputArgument UUID id) {
    var user = userService.findById(id);
    if (dfe.getSelectionSet().contains("articles") {
        Hibernate.initialize(user.getArticles());
    }
    return user;
}
Johan Nordlinder
  • 1,672
  • 1
  • 13
  • 17