graphql-spring的LazyInitializationException

1bqhqjot  于 2023-08-02  发布在  Spring
关注(0)|答案(7)|浏览(93)

我目前正在将我的REST服务器迁移到GraphQL(至少部分)。大部分的工作已经完成,但我偶然发现了这个我似乎无法解决的问题:Graphql查询中的OneToMany关系,使用FetchType. LAZY。
我使用:https://github.com/graphql-java/graphql-spring-boothttps://github.com/graphql-java/graphql-java-tools进行集成。
下面是一个示例:

实体:

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

   @OneToMany(mappedBy = "show")
   private List<Competition> competition;
}

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

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

字符串

架构:

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

type Competition {
    id: ID!
    name: String
}

extend type Query {
    shows : [Show]
}

解析器:

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

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


如果我现在用这个(简写)查询端点:

{
  shows {
    id
    name
    competitions {
      id
    }
  }
}


我得到:
org.hibernate.LazyInitializationException:无法延迟初始化角色的集合:Show.competitions,cannot initialize proxy - no Session
现在我知道为什么会发生这个错误,这意味着什么,但我真的不知道我们要应用这个修复程序。我不想让我的实体急切地获取所有关系,因为这会否定GraphQL的一些优点。有什么想法我可能需要寻找一个解决方案?谢谢你,谢谢

ni65a41a

ni65a41a1#

我的首选解决方案是打开事务,直到Servlet发送其响应。通过这个小的代码更改,您的LazyLoad将正常工作:

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();
  }

}

字符串

wgx48brx

wgx48brx2#

我解决了这个问题,我想我应该更仔细地阅读graphql-java-tools库的文档。除了解决基本查询的GraphQLQueryResolver之外,我还需要一个GraphQLResolver<T>用于我的Show类,它看起来像这样:

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

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

字符串
这告诉库如何解析Show类中的复杂对象,并且仅在最初的查询请求包含Competition对象时使用。新年快乐!

编辑31.07.2019:从那以后,我就离开了下面的解决方案。长时间运行的事务很少是一个好主意,在这种情况下,一旦扩展应用程序,它可能会导致问题。我们开始实现DataLoaders,以异步方式批处理查询。长时间运行的事务与DataLoaders的异步特性相结合可能导致死锁:https://github.com/graphql-java-kickstart/graphql-java-tools/issues/58#issuecomment-398761715(以上和以下获取更多信息)。我不会删除下面的解决方案,因为它可能仍然是小型应用程序和/或不需要任何批量查询的应用程序的良好起点,但请在这样做时记住这一点。
**编辑:**此处请求的是使用自定义执行策略的另一种解决方案。我正在使用graphql-spring-boot-startergraphql-java-tools

创建一个ExecutionStrategy类型的Bean来处理事务,如下所示:

@Service(GraphQLWebAutoConfiguration.QUERY_EXECUTION_STRATEGY)
public class AsyncTransactionalExecutionStrategy extends AsyncExecutionStrategy {

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


这将查询的整个执行放在同一个事务中。我不知道这是否是最佳的解决方案,而且它在错误处理方面也有一些缺点,但您不需要以这种方式定义类型解析器。
请注意,如果这是唯一存在的ExecutionStrategy Bean,则它也将用于突变,这与Bean名称可能暗示的相反。参见https://github.com/graphql-java-kickstart/graphql-spring-boot/blob/v11.1.0/graphql-spring-boot-autoconfigure/src/main/java/graphql/kickstart/spring/web/boot/GraphQLWebAutoConfiguration.java#L161-L166以获取参考。为了避免这种情况,定义另一个ExecutionStrategy用于突变:

@Bean(GraphQLWebAutoConfiguration.MUTATION_EXECUTION_STRATEGY)
public ExecutionStrategy queryExecutionStrategy() {
    return new AsyncSerialExecutionStrategy();
}

rnmwe5a2

rnmwe5a23#

对于任何对接受的答案感到困惑的人,那么你需要改变java实体以包含双向关系,并确保你使用helper方法添加一个Competition,否则很容易忘记正确设置关系。

@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;
}

字符串
公认答案背后的一般直觉是:
graphql解析器ShowResolver将打开一个事务以获取节目列表,但一旦完成,它将关闭该事务。
然后competitions的嵌套graphql查询将尝试调用getCompetition()上的每个Show示例,因为事务已经关闭,这将抛出LazyInitializationException

{
  shows {
    id
    name
    competitions {
      id
    }
  }
}


接受的答案基本上是绕过通过OneToMany关系检索竞争列表,而是在新事务中创建新查询,从而消除了问题。
不确定这是否是一个黑客,但@Transactional解析器不为我工作,虽然这样做的逻辑确实有一些意义,但我显然不明白根本原因。

kx1ctssn

kx1ctssn4#

对我来说,使用AsyncTransactionalExecutionStrategy工作不正确,有异常。例如,延迟初始化或应用程序级异常触发事务至仅回滚状态。Spring事务机制随后在策略execute的边界抛出了仅回滚事务,导致HttpRequestHandlerImpl返回400个空响应。有关详细信息,请参阅https://github.com/graphql-java-kickstart/graphql-java-servlet/issues/250https://github.com/graphql-java/graphql-java/issues/1652
对我来说有效的是使用Instrumentation将整个操作 Package 在一个事务中:https://spectrum.chat/graphql/general/transactional-queries-with-spring~47749680-3bb7-4508-8935-1d20d04d0c6a

ruarlubt

ruarlubt5#

正如Oleg所指出的,使用AsyncTransactionalExecutionStrategy的错误处理在嵌套事务中被破坏了。
由于他的答案中的URL不再起作用,以下是我如何解决的。
首先,让我们来看看我想通过GraphQL响应正确处理的一些异常

public class UserFriendlyException extends RuntimeException {
    public UserFriendlyException(String message) {
        super(message);
    }
}

字符串
然后我定义了错误响应

public class UserFriendlyGraphQLError implements GraphQLError {
    /** Message shown to user */
    private final String message;

    private final List<SourceLocation> locations;

    private final ExecutionPath path;

    public UserFriendlyGraphQLError(String message, List<SourceLocation> locations, ExecutionPath path) {
        this.message = message;
        this.locations = locations;
        this.path = path;
    }

    @Override
    public String getMessage() {
        return message;
    }

    @Override
    public List<SourceLocation> getLocations() {
        return locations;
    }

    @Override
    public ErrorClassification getErrorType() {
        return CustomErrorClassification.USER_FRIENDLY_ERROR;
    }

    @Override
    public List<Object> getPath() {
        return path.toList();
    }
}
public enum CustomErrorClassification implements ErrorClassification {
    USER_FRIENDLY_ERROR
}

的数据
然后我创建了DataFetcherExceptionHandler来将其转换为适当的GraphQL响应

/**
 * Converts exceptions into error response
 */
public class GraphQLExceptionHandler implements DataFetcherExceptionHandler {

    private final DataFetcherExceptionHandler delegate = new SimpleDataFetcherExceptionHandler();

    @Override
    public DataFetcherExceptionHandlerResult onException(DataFetcherExceptionHandlerParameters handlerParameters) {
        // handle user friendly errors
        if (handlerParameters.getException() instanceof UserFriendlyException) {
            GraphQLError error = new UserFriendlyGraphQLError(
                    handlerParameters.getException().getMessage(),
                    List.of(handlerParameters.getSourceLocation()),
                    handlerParameters.getPath());

            return DataFetcherExceptionHandlerResult.newResult().error(error).build();
        }

        // delegate to default handler otherwise
        return delegate.onException(handlerParameters);
    }
}


最后在AsyncTransactionalExecutionStrategy中使用了它,还使用了@Transactional注解来允许惰性解析器。

@Component
public class AsyncTransactionalExecutionStrategy extends AsyncExecutionStrategy {

    public AsyncTransactionalExecutionStrategy() {
        super(new GraphQLExceptionHandler());
    }

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


现在,如果你在某个地方throw new UserFriendlyException("Email already exists");,你最终会得到这样的好响应

{
    "errors": [
        {
            "message": "Email already exists",
            "locations": [
                {
                    "line": 2,
                    "column": 3
                }
            ],
            "path": [
                "createUser"
            ],
            "extensions": {
                "classification": "USER_FRIENDLY_ERROR"
            }
        }
    ],
    "data": null
}


给定分类USER_FRIENDLY_ERROR,如果您使UserFriendlyException消息用户友好,则可以直接将其显示给用户:)
但是,如果你在某个方法中使用throw new UserFriendlyException("Email already exists");注解@Transactional,你最终会得到空响应和HTTP 400状态。
@Transactional(propagation = Propagation.REQUIRES_NEW)添加到Mutation可解决此问题

@Transactional(propagation = Propagation.REQUIRES_NEW)
public class Mutation implements GraphQLMutationResolver {

    public User createUser(...) {
        ...
    }
}

  • 请注意,这可能不是那么高性能的解决方案。不过,它可以满足一些较小的项目。*
tzdcorbm

tzdcorbm6#

我假设,每当你获取一个 Show 对象时,你想要所有与 Show 对象相关的 Competition
默认情况下,实体中所有集合类型的获取类型为LAZY。您可以指定EAGER类型以确保hibernate获取集合。
Show 类中,可以将fetchType更改为EAGER

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

字符串

hpcdzsge

hpcdzsge7#

您只需要用@Transactional注解解析器类。然后,从存储库返回的实体将能够延迟地获取数据。

相关问题