深度潜水n+1个查询
#database #graphql #ruby #rails

如果您不知道roadmap.sh,则应该完全看一看。如果您想提高自己的技能和知识,它提供了您应该学到什么的高级视野。

作为后端工程师,您应该拥有的知识之一是n+1个查询。这是本文的主题,没有任何惊喜。

在您的Rails应用程序中的GraphQL API中,它们如何发生?如何防止它们,尤其是在GraphQl和Ruby中?我们将尝试在以下文章中查看答案。 :)

哦,是的,如果您想学习或阅读有关Rails,Ruby,数据库和许多与技术相关的内容的了解:

保持联系

在Twitter上:@yet_anotherDev

on LinkedIn:Lucas Barret

n+1查询

进入how之前,让我们了解n+1个查询的what

这并不像听起来那么复杂:

假设您正在开发SaaS产品;您的客户是公司;这些公司有用户。

您必须编写查询才能获取所有公司及其用户。

使用graphql-ruby,您的代码将大致如下所示:

app/graphql/types/user_type.rb
module Types
  class UserType < Types::BaseObject
    field :name, String, null: false
  end
end
##app/graphql/types/company_type.rb
module Types
  class CompanyType < Types::BaseObject

    field :id, ID, null: false
    field :name, String, null: false
    field :user, [Types::UserType], null: true

    def user
      User.where(company_id: object.id)
    end

  end
end
##app/graphql/queries/company_query.rb
module Queries
  class CompaniesQuery < BaseQuery
    type [Types::CompanyType], null: true

    def resolve(id)
      Company.all
    end

  end 
end

我们定义了一个获取所有公司的查询。此查询返回一系列公司,由[type :: CompanyType]实现。在此公司类型中,我们检索了所有用户实现的[类型:: usertype]。

要测试它,您可以按照这样的RSPEC测试进行测试:

Company Load (0.6ms)  SELECT "companies.*" FROM "companies"
  ↳ app/controllers/graphql_controller.rb:15:in `execute'
User Load (0.4ms)  SELECT "users".* FROM "users" WHERE "users"."company_id" = $1  [["company_id", 1]]
  ↳ app/controllers/graphql_controller.rb:15:in `execute'
User Load (0.3ms)  SELECT "users".* FROM "users" WHERE "users"."company_id" = $1  [["company_id", 2]]
  ↳ app/controllers/graphql_controller.rb:15:in `execute'
Completed 200 OK in 49ms (Views: 0.4ms | ActiveRecord: 19.1ms | Allocations: 8978)

应该引起您注意的是公司被查询一次,然后对用户进行查询两次。您正在进行2个SQL查询,以加载两家公司的用户。

如果您有三个用户,则为公司用户触发3个SQL查询。依此类推,您公司的每家公司中有10家拥有10个用户的公司进行100个查询!
我们称这些n+1个查询可以在graphql和Rest API中发生。

我们可以很容易地理解此查询需要更具扩展性,因为它可能在我们的数据库上施加太大的压力。

GraphQL批次GEM批处理加载

graphql-batch Gem将使您能够更加解决此问题。首先,通过批处理和将所有相同实体的查询分组为一个SQL查询来避免N+1。两个允许您在字段上进行懒惰加载,因此如果不在特定查询中使用,则不会查询它们。

在本文的下一部分中,我们将看到如何为我们的数据创建加载程序以避免此问题。

它将专门用于使其更容易理解,但是您可以创建一个通用,以与所有模型一起使用。

因此,正如我们之前所说,我们想收集公司的用户列表。

让我们使用与GraphQl-Ruby存储库中示例相似的内容。如果我复制并粘贴了自由的示例,那么在调整一点后,我将得到类似的东西。

class RecordLoader < GraphQL::Batch::Loader
  def initialize(model)
    @model = model
  end


  def perform(ids)
    @model.where(company_id: ids).each { |record| fulfill(record.company_id, record) }
    ids.each { |id| fulfill(id, nil) unless fulfilled?(id) }
  end
end

这将行不通;将会发生的事情与您的期望不同。您最终会出现错误!

这是由于我们实现诺言的方式。它将ID与本地缓存中的记录相关联。

 @model.where(company_id: ids).each { |record| fulfill(record.company_id, record) }

换句话说:我们已将第一个用户与Company_ID 1,第二个用户与Company_ID 2等相关联,依此类推。为了解决这个问题,您必须group_by并写下这样的东西。

class ArrayRecordLoader < GraphQL::Batch::Loader
  def initialize(model)
    @model = model
  end

  def perform(ids)
    @model.where(company_id: ids).group_by(&:company_id).each { |key,record| fulfill(key, record) }
    ids.each { |id| fulfill(id, nil) unless fulfilled?(id) }
  end
end

这很好;执行它将为您提供您的期望。

您提供的每个ID键有承诺。在这种情况下,这是Company_ID,您无法履行已实现的诺言。因此,在这种情况下,具有以下代码:

 @model.where(company_id: ids).each { |record| fulfill(record.company_id, record) }

您将履行对第一个使用第一个用户检索的第一个Company_ID的承诺。这不是数组,因此它将不起作用,因此预期的类型不会受到尊重,并且GraphQl会告诉您它不是合适的类型。

结论

通过本文,我们看到了n+1个查询,并且在调用数据库时可以通过任何API,GraphQl或Rest SOAP发生。

我们已经看到了如何使用GraphQL批次并实现基本加载程序。为了避免n+1并使用懒惰的加载以减轻数据库的压力是您想知道的。