Easy solution to run all mutations in DB transactions
This post reveals one of the features that’s so useful that worth being in the docs.
Consider a Rails application that defines a GraphQL server using the graphql-ruby gem. The app has a base mutation for all:
class BaseMutation < GraphQL::Schema::Mutation end
The rest mutations are inherited from it:
class SomeMutation < BaseMutation # fields definition def resolve(**params) SomeModel1.create!(**params[:name]) SomeModel2.create!(**params[:email]) end end
Also, as you may guess, the app has many mutations, not just this one.
Potentially, any of these mutations might have several DB writes (an
insert SQL statement) as in the
To guarantee the mutation atomicity (all DB inserts occur or none, if any of them is unsuccessful)
create! operations should be wrapped into a DB transaction:
class SomeMutation < BaseMutation # fields definition def resolve(**params) ApplicationRecord.transaction do SomeModel1.create!(**params[:name]) SomeModel2.create!(**params[:email]) end end end
Besides that, any new mutation might require this wrapper as well. But as it’s created by humans that thing might be easily missed.
That’s why we want the transaction open implicitly for all mutations.
Also, we don’t want to change all mutations that have defined the
#resolve method as above and have missed an open transaction.
Rewriting all mutations would be a monkey business, and it’s too risky.
In cases like this one we jump into the gem internals and see what’s defined in the base.
We need to figure out how these
#resolve methods are called and try to extend the functionality so that we achieve the desired behavior.
It’s not hard to find the searching code on GitHub.
Specifically, these lines we are interested in:
# Finally, all the hooks have passed, so resolve it if loaded_args.any? public_send(self.class.resolve_method, **loaded_args) else public_send(self.class.resolve_method) end
Aha! It turns out that in the end, it calls a method name that’s dynamic, it’s defined in the
By default, as expected it’s set to
:resolve, it’s easy to check in a Rails console:
> BaseMutation.resolve_method => :resolve
Somewhere close to this code we can find out that this value can be changed, see the related code:
# Default `:resolve` set below. # @return [Symbol] The method to call on instances of this object to resolve the field def resolve_method(new_method = nil) if new_method @resolve_method = new_method end @resolve_method || (superclass.respond_to?(:resolve_method) ? superclass.resolve_method : :resolve) end
That means we can define our own “resolve” method that will be called by the gem internals instead of the default
Using that knowledge, it’s easy to see that we’ve got a solution with all needs:
- define a custom resolver in the
- it will be a wrapper for the already defined
#resolvemethods in all mutations
- it will call these already defined
#resolvemethods inside an opened DB transaction.
That’s all what we need:
class BaseMutation < GraphQL::Schema::Mutation resolve_method :resolve_in_transaction def resolve_in_transaction(*args) ActiveRecord::Base.transaction do resolve(*args) end end end
Ta-da! That’s all that should be done. A few lines of code and we’ve solved a complex problem.
See how it’s important to have code organized, that has the same API (set of public methods) and behavior. Changing just a base code with just a few lines of code we easily change the whole family of classes!