@@ -471,3 +471,182 @@ Spring for GraphQL defines a `SortStrategy` to create `Sort` from GraphQL argume
471
471
`AbstractSortStrategy` implements the contract with abstract methods to extract the sort
472
472
direction and properties. To enable support for `Sort` as a controller method argument,
473
473
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