Easy solution to run all mutations in DB transactions
The graphql-ruby is a cool gem that allows a server definition for GraphQL on Ruby. It provides a ton of useful functionalities out of the box. So, you won’t find them in the official docs.
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 update
/delete
/insert
SQL statement) as in the SomeMutation
above.
To guarantee the mutation atomicity (all DB inserts occur or none, if any of them is unsuccessful)
the both 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 self.class.resolve_method
.
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 #resolve
one.
Using that knowledge, it’s easy to see that we’ve got a solution with all needs:
- define a custom resolver in the
BaseMutation
- it will be a wrapper for the already defined
#resolve
methods in all mutations - it will call these already defined
#resolve
methods 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.
Conclusion
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!