graphql-spqr with Spring Boot and Transactional Boundary

1.2k views Asked by At

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.

2

There are 2 answers

1
kaqqao On BEST ANSWER

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 you DataFetchingFieldSelectionSet 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 injecting ResolutionEnvironment:

@GraphQLMutation
public Match createMatch(
           @NotNull @Valid MatchForm matchForm,
           @GraphQLEnvironment ResolutionEnvironment env) { ... }

If you just need the names of the 1st level sub-fields, you can inject

@GraphQLEnvironment Set<String> selection

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.

0
Janning Vygen On

@kaqqao answer is very good but I want to comment on these and show a third solution:

  1. I don't like this solution as I have to do a lot of work in a context where I really don't care about it. This is not what GraphQL is about. The resolving should take place if it is needed. Looking ahead in every GraphQL Query and in every direction of the path sounds strange to me.

  2. I managed to implement this with a service class. But you will get problems with exceptions as the exceptions are wrapped and not processed properly.

    public class TransactionalGraphQLExecutor implements GraphQLServletExecutor {
     private final ServletContextFactory contextFactory;
     @Autowired(required = false)
     @SuppressWarnings("SpringJavaAutowiredFieldsWarningInspection")
     private DataLoaderRegistryFactory dataLoaderRegistryFactory;
    
     private final TxGraphQLExecutor txGraphQLExecutor;
    
     public TransactionalGraphQLExecutor(ServletContextFactory contextFactory, TxGraphQLExecutor txGraphQLExecutor) {
         this.contextFactory = contextFactory;
         this.txGraphQLExecutor = txGraphQLExecutor;
     }
    
     @Override
     public Map<String, Object> execute(GraphQL graphQL, GraphQLRequest graphQLRequest, NativeWebRequest nativeRequest) {
         ExecutionInput executionInput = buildInput(graphQLRequest, nativeRequest, contextFactory, dataLoaderRegistryFactory);
         if (graphQLRequest.getQuery().startsWith("mutation")) {
             return txGraphQLExecutor.executeReadWrite(graphQL, executionInput);
         } else {
             return txGraphQLExecutor.executeReadOnly(graphQL, executionInput);
         }
     }
    

    }

    public class TxGraphQLExecutor  { 
     @Transactional
     public Map<String, Object> executeReadWrite(GraphQL graphQL, ExecutionInput executionInput) {
         return graphQL.execute(executionInput).toSpecification();
     }
    
     @Transactional(readOnly = true)
     public Map<String, Object> executeReadOnly(GraphQL graphQL, ExecutionInput executionInput) {
         return graphQL.execute(executionInput).toSpecification();
     }
    

    }

  1. There is also a possibility to instrument, see https://spectrum.chat/graphql/general/transactional-queries-with-spring~47749680-3bb7-4508-8935-1d20d04d0c6a

  2. What I like best at the moment is to have another manual Resolver

     @GraphQLQuery
     @Transactional(readOnly = true)
     public Team getHomeTeam(@GraphQLContext Match match) {
         return matchRepository.getOne(match.getId()).getHomeTeam();
     }
    
  3. Of course you can also set spring.jpa.open-in-view=false (anti pattern)

  4. Or you can fetch eagerly.

It would be nice if you could define the Transactional boundaries with GraphQL.