graphql-spqr с загрузкой Spring и транзакционной границей

Мы используем graphql-spqr и graphql-spqr-spring-boot-starter для нового проекта (с Spring DataJPA, hibernate и так далее).

У нас есть такая мутация:

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

Эта мутация отлично работает:

mutation createMatch ($matchForm: MatchFormInput!){   
  match: createMatch(matchForm: $matchForm) {       
    id
}
variables: {...}

Я пропустил переменные, поскольку они не важны. Это не сработает, если я поменяю его на:

mutation createMatch ($matchForm: MatchFormInput!){   
  match: createMatch(matchForm: $matchForm) {       
    id
    homeTeam {
       name
    }
 }
variables: {...}

Я получаю исключение LazyInitalizationException и знаю почему:

На домашнюю команду ссылается идентификатор, и она загружается teamRepository. Возвращенная команда - это только прокси-сервер гибернации. Что нормально для сохранения нового матча, больше ничего не нужно. Но для отправки результата GraphQL должен получить доступ к прокси и вызвать match.team.getName(). Но это явно происходит вне транзакции, отмеченной@Transactional.

Я могу исправить это с помощью Team homeTeam teamRepository.findById(matchForm.getHomeId()). OrElse(null); match.setHomeTeam(домашняя команда);

Hibernate больше не загружает прокси, а реальный объект. Но поскольку я не знаю, что именно запрашивает GraphQL-запрос, нет смысла загружать все данные, если они впоследствии не понадобятся. Было бы неплохо, если бы GraphQL выполнялся внутри@Transactional, поэтому я могу определить границы транзакции для каждого запроса и изменения.

Какие-нибудь рекомендации по этому поводу?

PS: Я удалил код и немного подчистил, чтобы сделать его более лаконичным. поэтому код может быть неработоспособным, но иллюстрирует проблему.

1 ответ

Решение

Я могу предложить несколько вещей, которые вы, возможно, захотите рассмотреть.

1) С нетерпением загружайте по мере необходимости

Вы всегда можете заранее проверить, какие поля нужны для запроса, и быстро их загрузить. Подробности подробно описаны в статье Создание эффективных сборщиков данных в статье в блоге graphql-java.

Короче можно позвонить DataFetchingEnvironment#getSelectionSet() что даст вам DataFetchingFieldSelectionSet и содержит всю информацию, необходимую для оптимизации загрузки.

В SPQR вы всегда можете получить DataFetchingEnvironment (и многое другое) путем инъекции ResolutionEnvironment:

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

Если вам просто нужны имена подполей 1-го уровня, вы можете ввести

@GraphQLEnvironment Set<String> selection

вместо.

Для проверки вы всегда можете подключить свой собственный ArgumentInjector эта вишня выбирает именно то, что вам нужно ввести, поэтому в тестах легче имитировать.

2) Запустить полное разрешение запроса GraphQL в транзакции

Вместо или в дополнение к @Transactionalна отдельных преобразователях вы можете заменить контроллер по умолчанию на тот, который выполняет все операции в транзакции. Просто шлепни@Transactional на метод контроллера, и все готово.

@kaqqao ответ очень хорош, но я хочу прокомментировать их и показать третье решение:

  1. Мне не нравится это решение, так как мне приходится много работать в контексте, когда оно меня не волнует. Это не то, о чем GraphQL. Разрешение должно происходить, если это необходимо. Мне кажется странным смотреть вперед в каждом запросе GraphQL и во всех направлениях.

  2. Мне удалось реализовать это с помощью класса обслуживания. Но вы столкнетесь с проблемами с исключениями, поскольку исключения упакованы и не обрабатываются должным образом.

    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. Также есть возможность инструментировать, см. https://spectrum.chat/graphql/general/transactional-queries-with-spring%7E47749680-3bb7-4508-8935-1d20d04d0c6a

  2. Что мне больше всего нравится сейчас, так это иметь еще один ручной резолвер.

     @GraphQLQuery
     @Transactional(readOnly = true)
     public Team getHomeTeam(@GraphQLContext Match match) {
         return matchRepository.getOne(match.getId()).getHomeTeam();
     }
    
  3. Конечно, вы также можете установить spring.jpa.open-in-view=false (анти-шаблон)

  4. Или вы можете забрать с нетерпением.

Было бы неплохо, если бы вы могли определить границы транзакций с помощью GraphQL.

Другие вопросы по тегам