如果您不知道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并使用懒惰的加载以减轻数据库的压力是您想知道的。