We are using graphql-spqr and graphql-spqr-spring-boot-starter for a new project (With Spring DataJPA, hibernate and so on).
We have a mutation like this:
@Transactional
@GraphQLMutation
public Match createMatch(@NotNull @Valid MatchForm matchForm) {
Match match = new Match(matchForm.getDate());
match.setHomeTeam(teamRepository.getOne(matchForm.getHomeId()));
match.setAwayTeam(teamRepository.getOne(matchForm.getAwayId()));
match.setResult(matchForm.getResult());
matchRepository.save(match);
return match;
}
This mutation works fine:
mutation createMatch ($matchForm: MatchFormInput!){
match: createMatch(matchForm: $matchForm) {
id
}
variables: {...}
I have ommitted the variables as they are not important. It does not work if I change it to:
mutation createMatch ($matchForm: MatchFormInput!){
match: createMatch(matchForm: $matchForm) {
id
homeTeam {
name
}
}
variables: {...}
I get a LazyInitalizationException and I know why:
The homeTeam is referenced by an ID and is loaded by teamRepository
. The returned team is only a hibernate proxy. Which is fine for saving the new Match, nothing more is needed. But for sending back the result GraphQL needs to access the proxy and calls match.team.getName(). But this happens obviously outside of the transaction marked with @Transactional
.
I can fix it with Team homeTeam teamRepository.findById(matchForm.getHomeId()).orElse(null); match.setHomeTeam(homeTeam);
Hibernate is no longer loading a proxy but the real object. But as I do not know what the GraphQL query exactly is asking for, it does not make sense to eagerly load all the data if it is not needed later on. It would be nice if GraphQL would be executed inside the @Transactional
, so I can define the transaction Boundaries for each query and mutation.
Any recommendation for this?
PS: I stripped the code and did some cleanup to make it more concise. so the code might not be runnable, but does illustrate the problem.
I can come with a couple of things you may want to consider.
1) Eagerly load as needed
You can always preemptively check what fields the query wants and eagerly load them. The details are well explained in the Building efficient data fetchers by looking ahead article at graphql-java blog.
In short, you can get call
DataFetchingEnvironment#getSelectionSet()
which will give youDataFetchingFieldSelectionSet
and that contains all the info you need to optimize loading.In SPQR, you can always get a hold of
DataFetchingEnvironment
(and a lot more) by injectingResolutionEnvironment
:If you just need the names of the 1st level sub-fields, you can inject
instead.
For testability, you can always wire in your own
ArgumentInjector
that cherry-picks exactly what you need injected, so it's easier to mock in tests.2) Run the entire GraphQL query resolution in a transaction
Instead of or in addition to having
@Transactional
on individual resolvers, you can substitute the default controller with one that runs the entire thing in a transaction. Just slap@Transactional
onto the controller method, and you're good to go.