Gem 解决 GraphQL 与 ActiveRecord 嵌套 N + 1 SQL 问题

tinyfeng · 2019年04月12日 · 最后由 tinyfeng 回复于 2019年04月21日 · 10070 次阅读
本帖已被管理员设置为精华贴

背景

GraphQL 多级嵌套查询,如果不手动处理,会导致很严重的 N+1 问题,甚至 (N+1)^n

技术背景是 GraphQL + ActiveRecord + 关系型数据库

举个例子

您原来的 Resolver 代码可能如下所示:

class Resolvers::ProfileResolver < GraphQL::Function
  description 'User profile'

  type Types::ProfileType

  def _call(_, args, ctx)
    ctx[:current_user]
  end
end

当查询如下的时候:

query{
  profile{
    id
    works{
      comments{
        replyUser{
          name
        }
        content
      }
      name
      id
      likes{
        owner{
          name
          works{
            name
            likes{
              owner{
                name
              }
            }
          }
        }
      }
    }
    name
  }
}

它将导致多级嵌套 N + 1 问题。

如果你要解决它,你可以像这样写:

class Resolvers::ProfileResolver < OptimizedFunction
  description 'User profile'

  type Types::ProfileType

  def _call(_, args, ctx)
    User.includes([works: [comments: :reply_user, likes: [owner: [works: [likes: :owner]]]]]).find(ctx[:current_user].id)
  end
end

您将在每个 Resolver 中手动解决 N + 1 问题。更糟糕的是,即使只请求了一个字段,也不得不向数据库查询 includes 的所有表。

解决办法

用法

  • Resolver从继承 GraphQL::Function 改为继承于OptimizedFunction
  • 定义_call方法而不是call
  • 使用includes_klass(your_model_name)替换 includes 语句。
class Resolvers::ProfileResolver < OptimizedFunction
  description 'User profile'

  type Types::ProfileType

  def _call(_, args, ctx)
    includes_kclass(User).find(ctx[:current_user].id)
  end
end

原理

原理是解析最开始请求的 json 多叉树,与 ActiveRecord 的模型关联关系对比,从请求 json 多叉树中过滤出嵌套的关联关系树。

希望能帮助到和我一样喜欢 rails 和 graphql 的人,欢迎提出改进意见。

同样是 GraphQL + Rails 用户。考虑到有的人用的不是 ActiveRecord, 比如 Sequel,并且 N+1 的场景并不只是关系型数据库,任何 IO 操作都有 N+1 场景。所以这个graphql-batch的定位目前已经做到通用了(和 Node 中的 data-loader)一样。

GQL 的一些实践可以参考 Gitlab 的,比如 https://gitlab.com/gitlab-org/gitlab-ce/blob/master/lib/gitlab/graphql/loaders/batch_model_loader.rb

不过 GraphQL-Ruby 最近每一个版本都在做内部重构,所以要注意版本

huacnlee 将本帖设为了精华贴。 04月17日 10:02

如果你想你的 Gem 流行起来,应该正式点起个好的名字,现在这个像搞着玩似的。

huacnlee 回复

这名字挺表意的把,,,感觉没啥问题,

razertory 回复

是我的标题写的比较大,已经修改了。

graphql-batch需要在 type 里每个关联的地方手动加入以下类似代码来使用,但是我感觉如果根据请求,就能自动完成需要的 association 的预加载会更方便。

field :product, Types::Product, null: true do
  argument :id, ID, required: true
end

def product(id:)
  RecordLoader.for(Product).load(id)
end
jasl 回复

谢谢指导,感觉代码写的是很漂亮😅

huacnlee 回复

多谢,改了一下名字~

需要 登录 后方可回复, 如果你还没有账号请 注册新账号