【Rails6】利用GraphQL开发API(查询篇)

首先

我使用Rails 6进行了GraphQL开发,并总结了其使用和引入方法。这次我将介绍查询/关联和N+1问题。

开发环境

ruby2.7.1
rails6.0.3
GraphQL -> Ruby2.7.1、Rails6.0.3、GraphQL

GraphQL是什么?

GraphQL是一种用于API请求的查询语言,具有以下特点。

    • エンドポイントは/graphql1つのみ

 

    • Query: データの取得(Get)

 

    Mutation: データの作成、更新、削除(Create, Update, Delete)

与REST不同的一个重要区别是GraphQL只有一个端点。在REST中,存在多个端点,如/sign_up、/users、/users/1等,而在GraphQL中,只有一个端点,即/graphql。

在REST中,如果需要多个资源,就需要发起多个API请求,而GraphQL只有一个端点,可以一次获取所需的数据,从而使代码更简洁。

2. 这次使用的桌子

這將在兩個資料表中實現,父親資料表是User,子資料表是Post。我們將設定User可以發佈多個Post的一對多關係。

$ rails g model User name:string email:string
$ rails g model Post title:string description:string user:references
$ rails db:migrate

用户表

カラム型namestringemailstring
class User < ApplicationRecord
  has_many :posts, dependent: :destroy
end

帖子数据表

カラム型titlestringdescriptionstring
class Post < ApplicationRecord
  belongs_to :user
end

在Rails中引入GraphQL

安装gem。

gem 'graphql'    #追加

group :development, :test do
  gem 'byebug', platforms: [:mri, :mingw, :x64_mingw]
  gem 'graphiql-rails'   #開発環境に追加
end
require "sprockets/railtie"    #コメントアウトを外す
$ bundle install
$ rails generate graphql:install   #GraphQLに関するファイルが作成されます

请在routes.rb中添加以下内容:
在开发环境下,端点为/graphiql,在生产环境下,端点为/graphql。

Rails.application.routes.draw do
  if Rails.env.development?
    # add the url of your end-point to graphql_path.
    mount GraphiQL::Rails::Engine, at: "/graphiql", graphql_path: "/graphql" 
  end
  post '/graphql', to: 'graphql#execute'  #ここはrails generate graphql:installで自動生成される
end

请参考下面的详细总结,了解在Rails 6的API模式下如何使用GraphQL(包括错误处理方法)。

查询获取用户列表的命令

在GraphQL中,我们可以定义类型(针对每个模型),然后按照这些类型执行查询来获取数据。

创建用户的ObjectType

使用下面的命令来创建用户类型。
添加感叹号(!)将添加 null:false。

$ rails g graphql:object User id:ID! name:String! email:String!

会生成下述文件。

module Types
  class UserType < Types::BaseObject
    field :id, ID, null: false  # `!`をつけると`null:false`が追加されます。
    field :name, String, null: false
    field :email, String, null: false
  end
end

创建用户的查询

根据刚刚创建的用户类型来生成查询。

module Types
  class QueryType < Types::BaseObject
    field :users, [Types::UserType], null: false # userを配列で定義する
    def users
      User.all # user一覧を取得
    end
  end
end

使用控制台创建用户数据。

$ rails c
$ > User.create(name: "user1", email: "user-1@test.com")
$ > User.create(name: "user2", email: "user-2@test.com")

执行查询

准备工作已经完成,现在可以启动服务器并在Graphiql上进行确认。(http://localhost:3000/graphiql)

$ rails s

执行以下查询。

query{
  users{
    id
    name
    email
  }
}

然后会返回JSON格式的响应。
由于users的Type是数组,所以它是一个数组。

{
  "data": {
    "users": [
      {
        "id": "1",
        "name": "user1",
        "email": "user-1@test.com"
      },
      {
        "id": "2",
        "name": "user2",
        "email": "user-2@test.com"
      }
    ]
  }
}
スクリーンショット 2020-09-27 20.32.34.png

如果只请求并接收必要的数据

如果不需要所有列的数据,则可以修改查询。
例如,您也可以仅获取用户的ID。

query{
  users{
    id
  }
}

“回应”

{
  "data": {
    "users": [
      {
        "id": "1",
      },
      {
        "id": "2",
      }
    ]
  }
}

5. 协会 (xié huì)

接下来,我们尝试获取与用户相关联的帖子。
我们将以同样的方式创建用户的ObjectType和Query。

生成Post的ObjectType。

$ rails g graphql:object Post id:ID! title:String! description:String!

将生成以下文件。
为了获取用户数据,我们将添加字段:user,类型为Types::UserType,不允许为空。

module Types
  class PostType < Types::BaseObject
    field :id, ID, null: false
    field :title, String, null: false
    field :description, String, null: false
    field :user, Types::UserType, null: false # この一文を追加。belongs_to :userのようなもの
  end
end

在UserType中添加字段: posts, [Types::PostType], null: false。
由于这是多个数据关联,因此使用数组进行定义。

module Types
  class UserType < Types::BaseObject
    field :id, ID, null: false
    field :name, String, null: false
    field :email, String, null: false
    field :posts, [Types::PostType], null: false # この一文を追加。has_many :postsのようなもの
  end
end
$ rails c
$ > Post.create(title: "title", description: "description", user_id: 1)

生成Post的查询

module Types
  class QueryType < Types::BaseObject
    field :users, [Types::UserType], null: false
    def users
      User.all
    end

    # 下記を追加
    field :posts, [Types::PostType], null: false
    def posts
      Post.all
    end
  end
end

执行查询。

这是一个要执行的查询。

将帖子嵌套在用户中并进行请求。

query{
  users{
    id
    name
    email
    post{
      id
      title
      description
    }
  }
}

执行后将返回嵌套的帖子数据。
在REST环境中,可以获取用户的帖子数据。

スクリーンショット 2020-09-27 21.00.34.png

如果需要post.user的数据,可以使用如下的查询来获取。

query{
  posts{
    id
    title
    description
    user{
      id
    }
  }
}
スクリーンショット 2020-09-28 9.54.16.png

引入能够检测到N+1的子弹

由于GraphQL查询是树状结构,所以当存在关联时,容易出现N+1问题。因此,我们建议引入用于检测N+1问题的Bullet gem。

安装Bullet

group :development do
  gem 'bullet'
end
$ bundle install

在`config/environments/development.rb`文件中添加以下配置。

config.after_initialize do
  Bullet.enable = true
  Bullet.alert = true
  Bullet.bullet_logger = true
  Bullet.console = true
  Bullet.rails_logger = true
end

我试着确认N+1。

当Bullet的安装完成后,让我们来确认一下是否出现了N+1。

query{
  users{
    id
    name
    email
    posts{
      id
      title
      description
    }
  }
}

如果执行与之前相同的查询,将会出现以下日志。

Processing by GraphqlController#execute as */*
  Parameters: {"query"=>"query{\n  users{\n    id\n    name\n    email\n    posts{\n      id\n      title\n      description\n    }\n  }\n}", "variables"=>nil, "graphql"=>{"query"=>"query{\n  users{\n    id\n    name\n    email\n    posts{\n      id\n      title\n      description\n    }\n  }\n}", "variables"=>nil}}
  User Load (0.2ms)  SELECT "users".* FROM "users"
  ↳ app/controllers/graphql_controller.rb:15:in `execute'
  Post Load (0.2ms)  SELECT "posts".* FROM "posts" WHERE "posts"."user_id" = ?  [["user_id", 1]]
  ↳ app/controllers/graphql_controller.rb:15:in `execute'
  Post Load (0.1ms)  SELECT "posts".* FROM "posts" WHERE "posts"."user_id" = ?  [["user_id", 2]]
  ↳ app/controllers/graphql_controller.rb:15:in `execute'
Completed 200 OK in 36ms (Views: 0.3ms | ActiveRecord: 1.7ms | Allocations: 18427)


POST /graphql
USE eager loading detected
  User => [:posts]
  Add to your query: .includes([:posts])
Call stack

在您的查询中添加.includes([:posts])的原因是出现了N+1问题。
SQL查询也执行了三次。

User Load (0.2ms)  SELECT "users".* FROM "users"
Post Load (0.2ms)  SELECT "posts".* FROM "posts" WHERE "posts"."user_id" = ?  [["user_id", 1]]
Post Load (0.1ms)  SELECT "posts".* FROM "posts" WHERE "posts"."user_id" = ?  [["user_id", 2]]

解决N+1的方法

要消除N+1,一般来说,只需要使用includes就可以了。
也可以通过dataloader解决,但这次我们将使用includes。
我们将更改获取用户列表的部分如下:

def users
  # User.all  # 変更前
  User.includes(:posts).all # 変更後
end

让我们通过日志来确认N+1是否已经解决。
警告已经消失了。

Processing by GraphqlController#execute as */*
  Parameters: {"query"=>"query{\n  users{\n    id\n    name\n    email\n    posts{\n      id\n      title\n      description\n    }\n  }\n}", "variables"=>nil, "graphql"=>{"query"=>"query{\n  users{\n    id\n    name\n    email\n    posts{\n      id\n      title\n      description\n    }\n  }\n}", "variables"=>nil}}
  User Load (0.7ms)  SELECT "users".* FROM "users"
  ↳ app/controllers/graphql_controller.rb:15:in `execute'
  Post Load (1.2ms)  SELECT "posts".* FROM "posts" WHERE "posts"."user_id" IN (?, ?)  [["user_id", 1], ["user_id", 2]]
  ↳ app/controllers/graphql_controller.rb:15:in `execute'
Completed 200 OK in 57ms (Views: 0.2ms | ActiveRecord: 1.9ms | Allocations: 15965)

由于SQL文减少到两个,N+1问题已经成功解决。

User Load (0.7ms)  SELECT "users".* FROM "users"
Post Load (1.2ms)  SELECT "posts".* FROM "posts" WHERE "posts"."user_id" IN (?, ?)  [["user_id", 1], ["user_id", 2]]

7. 提取resolver

常见的查询

如果按照常规方法编写代码,不管模型如何,fields和方法会被添加到query_type中,使得query_type越来越庞大。

module Types
  class QueryType < Types::BaseObject
    field :users, [Types::UserType], null: false
    def users
      User.includes(:posts).all
    end

    field :posts, [Types::PostType], null: false
    def posts
      Post.all
    end
  end
end

使用Resolver来查询

在GitHub的问题中,通过使用Resolver,介绍了一种避免query_type.rb扩大化的最佳实践。
https://github.com/rmosolgo/graphql-ruby/issues/1825#issuecomment-441306410

请只定义field在query_type中。

module Types
  class QueryType < BaseObject
    field :users, resolver: Resolvers::QueryTypes::UsersResolver
    field :posts, resolver: Resolvers::QueryTypes::PostsResolver
  end
end

然后,将方法部分按照每个ObjectType切分到Resolver中(我们创建了一个新的 resolvers 目录)。
请务必不要忘记写上 GraphQL::Schema::Resolver,否则会报错,请注意不要忘记。

module Resolvers::QueryTypes
  class UsersResolver < GraphQL::Schema::Resolver
    type [Types::UserType], null: false
    def resolve
      User.includes(:posts).all
    end
  end
end
module Resolvers::QueryTypes
  class PostsResolver < GraphQL::Schema::Resolver
    type [Types::PostType], null: false
    def resolve
      Post.all
    end
  end
end

最后

关于GraphQL,当我在复习时,总结起来比想象中更长。虽然我想写关于rspec和mutation的内容,但我打算留到下次。

bannerAds