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:
- 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)
- Changing my
User
entity to useFetchMode.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.
Setting
spring.jpa.open-in-view=false
tospring.jpa.open-in-view=true
. Not in the favor of this either since this is just a band aid forLazyInitializationExceptions
.Any other solutions that just makes your forget about
LazyInitializationException
by keeping the session open throughout the lifecycle of the request.