LazyInitializationException with graphql-spring

▼魔方 西西 提交于 2019-12-21 03:45:50

问题


I am currently in the middle of migrating my REST-Server to GraphQL (at least partly). Most of the work is done, but i stumbled upon this problem which i seem to be unable to solve: OneToMany relationships in a graphql query, with FetchType.LAZY.

I am using: https://github.com/graphql-java/graphql-spring-boot and https://github.com/graphql-java/graphql-java-tools for the integration.

Here is an example:

Entities:

@Entity
class Show {
   private Long id;
   private String name;

   @OneToMany
   private List<Competition> competition;
}

@Entity
class Competition {
   private Long id;
   private String name;
}

Schema:

type Show {
    id: ID!
    name: String!
    competitions: [Competition]
}

type Competition {
    id: ID!
    name: String
}

extend type Query {
    shows : [Show]
}

Resolver:

@Component
public class ShowResolver implements GraphQLQueryResolver {
    @Autowired    
    private ShowRepository showRepository;

    public List<Show> getShows() {
        return ((List<Show>)showRepository.findAll());
    }
}

If i now query the endpoint with this (shorthand) query:

{
  shows {
    id
    name
    competitions {
      id
    }
  }
}

i get:

org.hibernate.LazyInitializationException: failed to lazily initialize a collection of role: Show.competitions, could not initialize proxy - no Session

Now i know why this error happens and what it means, but i don't really know were to apply a fix for this. I don't want to make my entites to eagerly fetch all relations, because that would negate some of the advantages of GraphQL. Any ideas where i might need to look for a solution? Thanks!


回答1:


I solved it and should have read the documentation of the graphql-java-tools library more carefully i suppose. Beside the GraphQLQueryResolver which resolves the basic queries i also needed a GraphQLResolver<T> for my Showclass, which looks like this:

@Component
public class ShowResolver implements GraphQLResolver<Show> {
    @Autowired
    private CompetitionRepository competitionRepository;

    public List<Competition> competitions(Show show) {
        return ((List<Competition>)competitionRepository.findByShowId(show.getId()));
    }
}

This tells the library how to resolve complex objects inside my Showclass and is only used if the initially query requests to include the Competitionobjects. Happy new Year!

EDIT 31.07.2019: I since stepped away from the solution below. Long running transactions are seldom a good idea and in this case it can cause problems once you scale your application. We started to implemented DataLoaders to batch queries in an async matter. The long running transactions in combination with the async nature of the DataLoaders can lead to deadlocks: https://github.com/graphql-java-kickstart/graphql-java-tools/issues/58#issuecomment-398761715 (above and below for more information). I will not remove the solution below, because it might still be good starting point for smaller applications and/or applications which will not need any batched queries, but please keep this comment in mind when doing so.

EDIT: As requested here is another solution using a custom execution strategy. I am using graphql-spring-boot-starter and graphql-java-tools:

I first define a GraphQL Config like this:

@Configuration
public class GraphQLConfig {
    @Bean
    public Map<String, ExecutionStrategy> executionStrategies() {
        Map<String, ExecutionStrategy> executionStrategyMap = new HashMap<>();
        executionStrategyMap.put("queryExecutionStrategy", new AsyncTransactionalExecutionStrategy());
        return executionStrategyMap;
    }
}

Where AsyncTransactionalExecutionStrategy is defined like this:

@Service
public class AsyncTransactionalExecutionStrategy extends AsyncExecutionStrategy {

    @Override
    @Transactional
    public CompletableFuture<ExecutionResult> execute(ExecutionContext executionContext, ExecutionStrategyParameters parameters) throws NonNullableFieldWasNullException {
        return super.execute(executionContext, parameters);
    }
}

This puts the whole execution of the query inside the same transaction. I don't know if this is the most optimal solution, and it also already has some drawbacks in regards to error handling, but you don't need to define a type resolver that way.




回答2:


My prefered solution is to have the transaction open until the Servlet sends its response. With this small code change your LazyLoad will work right:

import javax.servlet.Filter;
import org.springframework.orm.jpa.support.OpenEntityManagerInViewFilter;

@SpringBootApplication
public class Application {

  public static void main(String[] args) {
    SpringApplication.run(Application.class, args);
  }

  /**
   * Register the {@link OpenEntityManagerInViewFilter} so that the
   * GraphQL-Servlet can handle lazy loads during execution.
   *
   * @return
   */
  @Bean
  public Filter OpenFilter() {
    return new OpenEntityManagerInViewFilter();
  }

}



回答3:


For anyone confused about the accepted answer then you need to change the java entities to include a bidirectional relationship and ensure you use the helper methods to add a Competition otherwise its easy to forget to set the relationship up correctly.

@Entity
class Show {
   private Long id;
   private String name;

   @OneToMany(cascade = CascadeType.ALL, mappedBy = "show")
   private List<Competition> competition;

   public void addCompetition(Competition c) {
      c.setShow(this);
      competition.add(c);
   }
}

@Entity
class Competition {
   private Long id;
   private String name;

   @ManyToOne(fetch = FetchType.LAZY)
   private Show show;
}

The general intuition behind the accepted answer is:

The graphql resolver ShowResolver will open a transaction to get the list of shows but then it will close the transaction once its done doing that.

Then the nested graphql query for competitions will attempt to call getCompetition() on each Show instance retrieved from the previous query which will throw a LazyInitializationException because the transaction has been closed.

{
  shows {
    id
    name
    competitions {
      id
    }
  }
}

The accepted answer is essentially bypassing retrieving the list of competitions through the OneToMany relationship and instead creates a new query in a new transaction which eliminates the problem.

Not sure if this is a hack but @Transactional on resolvers doesn't work for me although the logic of doing that does make some sense but I am clearly not understanding the root cause.




回答4:


You just need to annotate your resolver classes with @Transactional. Then, entities returned from repositories will be able to lazily fetch data.




回答5:


I am assuming that whenever you fetch an object of Show, you want all the associated Competition of the Show object.

By default the fetch type for all collections type in an entity is LAZY. You can specify the EAGER type to make sure hibernate fetches the collection.

In your Show class you can change the fetchType to EAGER.

@OneToMany(cascade=CascadeType.ALL,fetch=FetchType.EAGER)
private List<Competition> competition;


来源:https://stackoverflow.com/questions/48037601/lazyinitializationexception-with-graphql-spring

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!