DEV Community

Matteo Kovačić for Bornfight

Posted on

How to execute multiple GraphQL mutations in a transaction using Overblog GraphQL Bundle

When we started working with GraphQL, we experienced common issues like N+1 problem, error handling, etc. As our API started growing, new challenges occurred. They were mostly related to the schema design and how to have a schema that is easy to read and also functional.

By specification, GraphQL allows us to send multiple mutations in a single request and these mutations will be executed in the order in which they were sent. GraphQL specification doesn't guarantee us that if some of these mutations fail, all others will fail as well. In other words, GraphQL doesn't specify that all mutations should be executed in the transactions. Luckily, this can be easily enabled in the Overblog GraphQL Bundle using built-in events.

First, in services.yaml, you need to register the event listener for two events: graphql.pre_executor and graphql.post_executor.

services:
    ...

    App\EventListener\GraphQLExecutorListener:
        tags:
            - { name: kernel.event_listener, event: graphql.pre_executor, method: onPreExecutor }
            - { name: kernel.event_listener, event: graphql.post_executor, method: onPostExecutor }
Enter fullscreen mode Exit fullscreen mode

Then, in the event listener, you have a method for each event. In onPreExecutor we start the transaction. After mutations are resolved, in onPostExecutor we commit the changes if there are no errors or rollback if an error occurred.

class GraphQLExecutorListener
{
    private EntityManagerInterface $entityManager;

    public function __construct(EntityManagerInterface $entityManager)
    {
        $this->entityManager = $entityManager;
    }

    public function onPreExecutor(): void
    {
        $this->entityManager->getConnection()->beginTransaction();
    }

    public function onPostExecutor(ExecutorResultEvent $event): void
    {
        $connection = $this->entityManager->getConnection();

        if ($connection->isTransactionActive() === false) {
            throw new LogicException('Transaction not active');
        }

        if (count($event->getResult()->errors) > 0) {
            while ($connection->getTransactionNestingLevel() > 0) {
                $connection->rollback();
            }

            return;
        }

        $connection->commit();
    }
}
Enter fullscreen mode Exit fullscreen mode

That's it, all mutations should now be executed in the transaction.
If necessary, the event listener can be extended to conditionally enable the transaction, for example using a header or some other flag.

Top comments (0)