在DB事务中运行所有突变的简单解决方案

53 阅读1分钟

graphql-ruby是一个很酷的宝石,它允许在Ruby上对GraphQL进行服务器定义。 它提供了大量有用的功能,开箱即用。因此,你不会在官方文档中找到它们。

这篇文章揭示了其中一个非常有用的功能,值得放在文档中。

考虑一个使用graphql-rubygem定义GraphQL服务器的Rails应用。 该应用有一个用于所有的基础突变:

class BaseMutation < GraphQL::Schema::Mutation
end

其余的突变都是从它那里继承的:

class SomeMutation < BaseMutation
  # fields definition

  def resolve(**params)
    SomeModel1.create!(**params[:name])
    SomeModel2.create!(**params[:email])
  end
end

此外,正如你可能猜到的,该应用程序有许多突变,而不仅仅是这一个。可能,这些突变中的任何一个都可能有几个DB写入(一个update/delete/insert SQL语句),如上面的SomeMutation 。为了保证突变的原子性(所有DB插入发生或没有,如果其中任何一个不成功),两个create! 操作应该被包装成一个DB事务:

class SomeMutation < BaseMutation
  # fields definition

  def resolve(**params)
    ApplicationRecord.transaction do
      SomeModel1.create!(**params[:name])
      SomeModel2.create!(**params[:email])
    end
  end
end

除此之外,任何新的突变可能也需要这个包装器。但是,由于它是由人类创建的,这一点可能很容易被忽略。 这就是为什么我们希望所有的突变都隐式地打开事务。 此外,我们不想改变所有的突变,这些突变已经定义了上述的#resolve 方法,并且已经错过了一个开放的事务。 重写所有的突变将是一个猴子的事情,而且风险太大。

在这样的情况下,我们跳到gem内部,看看基础中定义了什么。 我们需要弄清楚这些#resolve 方法是如何被调用的,并尝试扩展功能,从而实现我们想要的行为。 在GitHub上找到搜索代码并不难。

具体来说,我们感兴趣的是这几行:

# 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

啊哈!事实证明,在最后,它调用了一个动态的方法名,它被定义在self.class.resolve_method 。默认情况下,正如预期的那样,它被设置为:resolve ,这很容易在Rails控制台中检查:

> BaseMutation.resolve_method
=> :resolve

在接近这段代码的某个地方,我们可以发现这个值是可以改变的,见相关代码:

# 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

这意味着我们可以定义我们自己的 "resolve "方法,它将被 gem 内部调用,而不是默认的#resolve 方法。

利用这些知识,我们很容易看到,我们已经有了一个满足所有需求的解决方案:

  • 中定义一个自定义的解析器。BaseMutation
  • 它将是所有突变中已经定义的#resolve 方法的一个封装器
  • 它将在一个打开的 DB 事务中调用这些已经定义的#resolve 方法。

这就是我们所需要的一切:

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!这就是所有应该做的事情。仅仅几行代码,我们就解决了一个复杂的问题。

结论

看到了有组织的代码是多么的重要,它具有相同的API(公共方法集)和行为。 只需改变一个基础代码,只需几行代码,我们就能轻松地改变整个类的家族