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 ответ очень хорош, но я хочу прокомментировать их и показать третье решение:
Мне не нравится это решение, так как мне приходится много работать в контексте, когда оно меня не волнует. Это не то, о чем GraphQL. Разрешение должно происходить, если это необходимо. Мне кажется странным смотреть вперед в каждом запросе GraphQL и во всех направлениях.
Мне удалось реализовать это с помощью класса обслуживания. Но вы столкнетесь с проблемами с исключениями, поскольку исключения упакованы и не обрабатываются должным образом.
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(); }
}
Также есть возможность инструментировать, см. https://spectrum.chat/graphql/general/transactional-queries-with-spring%7E47749680-3bb7-4508-8935-1d20d04d0c6a
Что мне больше всего нравится сейчас, так это иметь еще один ручной резолвер.
@GraphQLQuery @Transactional(readOnly = true) public Team getHomeTeam(@GraphQLContext Match match) { return matchRepository.getOne(match.getId()).getHomeTeam(); }
Конечно, вы также можете установить
spring.jpa.open-in-view=false
(анти-шаблон)Или вы можете забрать с нетерпением.
Было бы неплохо, если бы вы могли определить границы транзакций с помощью GraphQL.