@@ -471,3 +471,182 @@ Spring for GraphQL defines a `SortStrategy` to create `Sort` from GraphQL argume
471471`AbstractSortStrategy` implements the contract with abstract methods to extract the sort
472472direction and properties. To enable support for `Sort` as a controller method argument,
473473you need to declare a `SortStrategy` bean.
474+
475+
476+
477+ [[data.transaction-management]]
478+ == Transaction Management
479+
480+ At some point when working with data atomicity and isolation of operations start to
481+ matter. These are both properties of transactions. GraphQL itself does not define any
482+ transaction semantics, so it is up to the server and your application to decide how to
483+ handle transactions.
484+
485+ GraphQL and specifically GraphQL Java are designed to be non-opinionated about how data
486+ is fetched. A core property of GraphQL is that clients drive the request; Fields
487+ can be resolved independently of their original source to allow for composition.
488+ A reduced fieldset can require less data to be fetched resulting in better performance.
489+
490+ Applying the concept of distributed field resolution within transactions is not a good fit:
491+
492+ * Transactions keep a unit of work together resulting typically in fetching the entire
493+ object graph (like a typical object-relational mapper would behave) within a single
494+ transaction. This is at odds with GraphQL's core design to let the client drive queries.
495+
496+ * Keeping a transaction open across multiple data fetchers of which each one would
497+ fetch only its flat object mitigates the performance aspect and aligns with decoupled
498+ field resolution, but it can lead to long-running transactions that hold on to resources
499+ for longer than necessary.
500+
501+ Generally speaking, transactions are best applied to mutations that change state and not
502+ necessarily to queries that just read data. However, there are use cases where
503+ transactional reads are required.
504+
505+ GraphQL is designed to support multiple mutations within a single request. Depending on
506+ the use case, you might want to:
507+
508+ * Run each mutation within its own transaction.
509+ * Keep some mutations within a single transaction to ensure a consistent state.
510+ * Span a single transaction over all involved mutations.
511+
512+ Each approach requires a slightly different transaction management strategy.
513+
514+ When using Spring Framework (e.g. JDBC) or Spring Data, the Template API and repositories
515+ default (without any further instrumentation) to use implicit transactions for individual
516+ operations resulting in starting and commiting a transaction for each repository method
517+ call. This is the normal mode of operation for most databases.
518+
519+ The following sections are outlining two different strategies to manage transactions in a
520+ GraphQL server:
521+
522+ 1. <<data.transaction-management.transactional-service-methods,Transaction per Controller Method>>
523+ 2. <<data.transaction-management.transactional-instrumentation,Spanning a Transaction programmatically over the entire request>>
524+
525+
526+ [[data.transaction-management.transactional-service-methods]]
527+ === Transactional Controller Methods
528+
529+ The simplest approach to manage transactions is to use Spring's Transaction Management
530+ together with `@MutationMapping` controller methods (or any other `@SchemaMapping` method)
531+ for example:
532+
533+ [tabs]
534+ ======
535+ Declarative::
536+ +
537+ [source,java,indent=0,subs="verbatim,quotes,attributes",role="primary"]
538+ ----
539+ @Controller
540+ public class AccountController {
541+
542+ @MutationMapping
543+ @Transactional
544+ public Account addAccount(@Argument AccountInput input) {
545+ // ...
546+ }
547+ }
548+ ----
549+
550+ Programmatic::
551+ +
552+ [source,java,indent=0,subs="verbatim,quotes,attributes",role="secondary"]
553+ ----
554+ @Controller
555+ public class AccountController {
556+
557+ private final TransactionOperations transactionOperations;
558+
559+ @MutationMapping
560+ public Account addAccount(@Argument AccountInput input) {
561+ return transactionOperations.execute(status -> {
562+ // ...
563+ });
564+ }
565+ }
566+ ----
567+ ======
568+
569+ A transaction spans from entering the `addAccount` method until its return.
570+ All invocations to transactional resources are part of the same transaction resulting in
571+ atomicity and isolation of the mutation.
572+
573+ This is the recommended approach. It gives you full control over transaction boundaries
574+ with a clearly defined entrypoint without the need to instrument GraphQL server
575+ infrastructure.
576+
577+ Cleaning up a transaction after the method call results that subsequent data fetching
578+ (e.g. for nested fields) is not part of the transactional method `addAccount` as
579+ outlined below:
580+
581+ [source,java,indent=0,subs="verbatim,quotes"]
582+ ----
583+ @Controller
584+ public class AccountController {
585+
586+ @MutationMapping
587+ @Transactional
588+ public Account addAccount(@Argument AccountInput input) { <1>
589+ // ...
590+ }
591+
592+ @SchemaMapping
593+ @Transactional
594+ public Person person(Account account) { <2>
595+ ... // fetching the person within a separate transaction
596+ }
597+ }
598+ ----
599+ <1> The `addAccount` method invocation runs within its own transaction.
600+ <2> The `person` method invocation creates its own, separate transaction that is not
601+ tied to the `addAccount` method in case both methods were invoked as part of the same
602+ GraphQL request. A separate transaction comes with all possible drawbacks of not
603+ being part of the same transaction, such as non-repeatable reads or inconsistencies
604+ in case the data has been modified between the `addAcount` and `person` method invocations.
605+
606+ To run multiple mutations in a single transaction maintaining a simple setup we recommend
607+ designing a mutation method that accepts all required inputs. This method can then call
608+ multiple service methods, ensuring they all participate in the same transaction.
609+
610+
611+ [[data.transaction-management.transactional-instrumentation]]
612+ === Transactional Instrumentation
613+
614+ Applying a Transactional Instrumentation is a more advanced approach to span a
615+ transaction over the entire execution of a GraphQL request. By stating a transaction
616+ before the first data fetcher is invoked your application can ensure that all data
617+ fetchers can participate in the same transaction.
618+
619+ When instrumenting the server, you need to ensure an `ExecutionStrategy` runs
620+ `DataFetcher` invocations serially so that all invocations are executed on the same
621+ `Thread`. This is mandatory: Synchronous transaction management uses `ThreadLocal` state
622+ to allow participation in transactions. Considering `AsyncSerialExecutionStrategy` as
623+ starting point is a good choice as it executes data fetchers serially.
624+
625+ You have two general options to implement transactional instrumentation:
626+
627+ 1. GraphQL Java's `Instrumentation` contract allows to hook into the execution lifecycle
628+ at various stages. The Instrumentation SPI was designed with observability in mind, yet it
629+ serves as execution-agnostic extension points regardless of whether you're using
630+ synchronous reactive, or any other asynchronous form to invoke data fetchers and is less
631+ opinionated in that regard.
632+
633+ 2. An `ExecutionStrategy` provides full control over the execution and opens a variety
634+ of possibilities how to communicate failed transactions or errors during transaction
635+ cleanup back to the client. It can also serve as good entry point to implement custom
636+ directives that allow clients specifying transactional attributes through directives or
637+ using directives in your schema to demarcate transactional boundaries for certain queries
638+ or mutations.
639+
640+ When manually managing transactions, ensure to clean up the transaction, that is either
641+ commiting or rolling back, after completing the unit of work.
642+ `ExceptionWhileDataFetching` can be a useful `GraphQLError` to obtain an underlying
643+ `Exception`. This error is constructed when using `SimpleDataFetcherExceptionHandler`.
644+ By default, Spring GraphQL falls back to an internal `GraphQLError` that doesn't expose
645+ the original exception.
646+
647+ Applying transactional instrumentation creates opportunities to rethink transaction
648+ participation: All `@SchemaMapping` controller methods participate in the transaction
649+ regardless whether they are invoked for the root, nested fields, or as part of a mutation.
650+ Transactional controller methods (or service methods within the invocation chain) can
651+ declare transactional attributes such as propagation behavior `REQUIRES_NEW` to start
652+ a new transaction if required.
0 commit comments