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

tinyfeng · 2019年04月12日 · 最后由 tinyfeng 回复于 2019年04月21日 · 4133 次阅读
本帖已被设为精华帖!

背景

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的人,欢迎提出改进意见。

共收到 7 条回复

同样是 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 回复

多谢,改了一下名字~

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