Skip to content

Commit 78a9812

Browse files
mp911debclozel
authored andcommitted
Document transaction management variants.
Closes gh-1305
1 parent 525e833 commit 78a9812

File tree

1 file changed

+179
-0
lines changed
  • spring-graphql-docs/modules/ROOT/pages

1 file changed

+179
-0
lines changed

spring-graphql-docs/modules/ROOT/pages/data.adoc

Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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
472472
direction and properties. To enable support for `Sort` as a controller method argument,
473473
you 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

Comments
 (0)