Find an article

Start typing to search the archive.

← All articles

Easy solution to run all mutations in DB transactions

Learn an easy solution to wrap all mutations in DB transactions using the graphql-ruby gem.

Updated

graphql-ruby is a cool gem that lets you define a GraphQL server in Ruby. It provides a ton of useful functionality out of the box. Some of it isn’t described in the official docs.

This post reveals one of the features that’s so useful that it’s 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 remaining mutations inherit 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’s atomicity (all DB inserts occur or none, if any of them is unsuccessful) 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 detail may easily be missed. That’s why we want the transaction to 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 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 code we’re looking for on GitHub.

Specifically, we are interested in these lines:

# 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 dynamic method name defined in 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 that meets all our 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 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 important it is to organize code with the same API (set of public methods) and behavior. By changing just a few lines of base code, we easily change the whole family of classes!