使用Rails和GraphQL创建API

版本差异不同

ruby: 2.7.1
rails: 6.0.3.4
graphql-ruby: 1.11.6

GraphQL Ruby (GraphQL-的Ruby实现)

公式网页

如果在Rails中使用GraphQL,我们可以使用上述的gem来实现API。

图形界面查询(GraphiQL)是一个用于Ruby on Rails框架的插件。

如果安装上 graphiql-rails gem,就可以在浏览器上使用一个可以检查已实施的GraphQL的IDE。在安装 graphql-ruby 时,会自动将 graphiql-rails gem 添加到 Gemfile 中。

graphiql-rails

环境搭建


gem 'graphql'
gem 'graphiql-rails' # 今回は先に入れました

当Gem安装完成后,执行rails generate graphql:install命令来生成各个文件。
生成的文件如下所示↓

$ rails generate graphql:install
      create  app/graphql/types
      create  app/graphql/types/.keep
      create  app/graphql/app_schema.rb
      create  app/graphql/types/base_object.rb
      create  app/graphql/types/base_argument.rb
      create  app/graphql/types/base_field.rb
      create  app/graphql/types/base_enum.rb
      create  app/graphql/types/base_input_object.rb
      create  app/graphql/types/base_interface.rb
      create  app/graphql/types/base_scalar.rb
      create  app/graphql/types/base_union.rb
      create  app/graphql/types/query_type.rb
add_root_type  query
      create  app/graphql/mutations
      create  app/graphql/mutations/.keep
      create  app/graphql/mutations/base_mutation.rb
      create  app/graphql/types/mutation_type.rb
add_root_type  mutation
      create  app/controllers/graphql_controller.rb
       route  post "/graphql", to: "graphql#execute"
     gemfile  graphiql-rails
       route  graphiql-rails

在此时点的 routes.rb 文件如下所示。

Rails.application.routes.draw do

  # GraphQL
  if Rails.env.development?
    mount GraphiQL::Rails::Engine, at: '/graphiql', graphql_path: '/graphql'
  end

  post '/graphql', to: 'graphql#execute'
end

做实


请创建查询

首先,需要定义与每个表对应的类型,所以我想要创建一个与以下示例用户表(users)对应的用户类型(user_type)。

create_table :users do |t|
  t.string :name, null: false
  t.string :email
  t.timestamps
end

执行以下命令将创建user_type。
(指定类型是为GraphQL定义的用于ID的类型(实际上是字符串)
同时,以!结尾的类型不允许为空,而没有!的类型允许为空。)

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

如果数据库中已经存在该表,那么它似乎会帮我们补充完成。

$ bundle exec rails g graphql:object User

↑这样也没问题

生成的文件 graphql/type/user_type.rb 的内容如下所示。

module Types
  class UserType < Types::BaseObject
    field :id, ID, null: false
    field :name, String, null: false
    field :email, String, null: true
    field :created_at, GraphQL::Types::ISO8601DateTime, null: false
    field :updated_at, GraphQL::Types::ISO8601DateTime, null: false
  end
end

我将以下内容添加到已经生成的 graphql/type/query_type.rb 文件中。

    field :users, [Types::UserType], null: false
    def users
      User.all
    end

我认为当您在 http://localhost:3000/graphiql 上提出以下查询时,将返回响应。

{
  users {
    id
    name
    email
  }
}

创建变异

接下来,我想创建一个名为 “CreateUser” 的Mutations以便创建用户。

$ bundle exec rails g graphql:mutation CreateUser

由于创建了graphql/mutations/create_user.rb,因此将根据以下方式进行修改。

module Mutations
  class CreateUser < BaseMutation
    field :user, Types::UserType, null: true

    argument :name, String, required: true
    argument :email, String, required: false

    def resolve(**args)
      user = User.create!(args)
      {
        user: user
      }
    end
  end
end

我会向已经生成的graphql/types/mutation_type.rb文件中添加以下内容。

module Types
  class MutationType < Types::BaseObject
    field :createUser, mutation: Mutations::CreateUser # 追記
  end
end

当在 http://localhost:3000/graphiql 上执行以下操作时,将会创建一个名为User的对象。

mutation {
  createUser(
    input:{
      name: "user"
      email: "user@email.com"
    }
  ){
    user {
      id
      name 
      email
    }
  }
}

协会

    1:1の関連テーブルの場合

如果将帖子与标签进行一对一的关联的话,可以举例如下。

module Types
  class LabelType < Types::BaseObject
    field :id, ID, null: false
    field :name, String, null: false
    ...
  end
end
module Types
  class PostType < Types::BaseObject
    field :label, LabelType, null: true
  end
end

可以将label定义为LabelType,就像上面的例子那样。
在这种情况下,可以将查询图像想象成…

{
  posts {
    id
    label {
      id
      name
    }
  }
}

您可以使用查询将标签(label)作为LabelType获取所需的值。

    1:Nの関連テーブルの場合

假设用户与帖子是一对多的关系

module Types
  class PostType < Types::BaseObject
    field :id, ID, null: false
    field :label, LabelType, null: true
  end
end
module Types
  class UserType < Types::BaseObject
    field :posts, [PostType], null: false
  end
end

可以将上述的帖子定义为[帖子类型],并且可以作为查询

{
  user(id: 1234) {
    id
    posts {
      id
      label {
        id
        name
      }
    }
  }
}

可以如上所述进行调用。

GraphQL批处理

根据上述说明,可以提取1:1或1:N关联表的数据,但当前情况下可能会导致大量向数据库发出查询请求。以User和Post的1:N关系为例,如果有100条Post,那么每条都会发起100次查询请求。

因此,我們將試著引入一種解決方法,即使用 graphql-batch 來將多個請求整合在一起。

gem 'graphql-batch'

当安装了Gem之后,我们将开始创建loader。
loader将用于实现“将多个查询整合在一起”的部分。

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

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

如果这个贴子与之前的帖子是一对一的关联的话,那么就适用于它。

module Types
  class PostType < Types::BaseObject
    field :label, LabelType, null: true
    def label
      Loaders::RecordLoader.for(Label).load(object.label_id)
    end
  end
end

在用户与帖子之间是一对多的关系时,需要单独创建一个loader。

module Loaders
  class AssociationLoader < GraphQL::Batch::Loader
    def self.validate(model, association_name)
      new(model, association_name)
      nil
    end

    def initialize(model, association_name)
      @model = model
      @association_name = association_name
      validate
    end

    def load(record)
      raise TypeError, "#{@model} loader can't load association for #{record.class}" unless record.is_a?(@model)
      return Promise.resolve(read_association(record)) if association_loaded?(record)
      super
    end

    # We want to load the associations on all records, even if they have the same id
    def cache_key(record)
      record.object_id
    end

    def perform(records)
      preload_association(records)
      records.each { |record| fulfill(record, read_association(record)) }
    end

    private

    def validate
      unless @model.reflect_on_association(@association_name)
        raise ArgumentError, "No association #{@association_name} on #{@model}"
      end
    end

    def preload_association(records)
      ::ActiveRecord::Associations::Preloader.new.preload(records, @association_name)
    end

    def read_association(record)
      record.public_send(@association_name)
    end

    def association_loaded?(record)
      record.association(@association_name).loaded?
    end
  end
end

由于graphql-batch的存储库中有示例,请参考该示例进行实现,这样做可能更好。

按照以下的方法写,可以一次性进行查询。

module Types
  class UserType < Types::BaseObject
    field :posts, [PostType], null: false
    def posts
      Loaders::AssociationLoader.for(User, :posts).load(object)
    end
  end
end

从架构文件生成文件。

我想尝试从最后定义的架构文件中自动生成漂亮的文档。

找到一个方便的gem叫做graphdoc-ruby,我会试着安装它,在routes.rb中挂载它,并实现自动更新graphdoc每次部署。

请在Gemfile中添加以下内容

gem 'graphdoc-ruby'

另外,由于我们还需要 npm 包 @2fd/graphdoc,请提前在 Docker 镜像中进行安装。(如果没有使用 Docker,请在本地环境中进行安装即可)

这个房子非常漂亮。

RUN set -ex \
    && wget -qO- https://deb.nodesource.com/setup_10.x | bash - \
    && apt-get update \
    && apt-get install -y \
                 ...
                 --no-install-recommends \
    && rm -rf /var/lib/apt/lists/* \
    && npm install -g yarn \
    && npm install -g @2fd/graphdoc # インストールしとく

在config/routes.rb文件中添加以下内容

Rails.application.routes.draw do
  mount GraphdocRuby::Application, at: 'graphdoc'
end

如果更改了端点,请修改config/initializers/graphdoc.rb。

这间餐厅的食物非常美味。

GraphdocRuby.configure do |config|
  config.endpoint = 'http://0.0.0.0:3000/api/v1/graphql'
end

只需重启Rails,然后在http://localhost:3000/graphdoc处生成文档即可。

graphdoc

坏的技巧


http://localhost:3000/graphiql アクセス時に以下エラーが発生する場合

Sprockets::Rails::Helper::AssetNotPrecompiled in GraphiQL::Rails::Editors#show

解決方法1

app/assets/config/manifest.js に以下を追加する

//= link graphiql/rails/application.css
//= link graphiql/rails/application.js

AssetNotPrecompiled error with Sprockets 4.0 · Issue #75 · rmosolgo/graphiql-rails
-> ただこれだとProduction時に Sprockets::FileNotFound: couldn’t find file ‘graphiql/rails/application.css’ エラーが出て使えない…

解決方法2 (うまくいった方法)

gem ‘sprocket’のバージョン3.7.2に下げる

gem ‘sprockets’, ‘~> 3.7.2’ [#1098: slowdev/knowledge/ios/FirebaseをCarthageで追加する](/posts/1098)

↑を追加し、bundle update
Rails6のAPIモードでGraphQLを使う方法(エラー対策も含む) – Qiita

graphiqlの画面にTypeError: Cannot read property ‘types’ of undefined が表示される
-> 手元の環境だとRails再起動で治りました
graphiqlの画面にSyntaxError: Unexpected token < in JSON at position 0 が表示される -> エラーが発生してる可能性がるのでログを見て修正する

有用的网站链接


    • 【Rails】graphql-rubyでAPIを作成 – Qiita

 

    • REST APIが主流のプロジェクトの中でGraphQLを導入してみた話(サーバーサイド編) – Sansan Builders Blog

 

    • 「GraphQL」徹底入門 ─ RESTとの比較、API・フロント双方の実装から学ぶ – エンジニアHub|若手Webエンジニアのキャリアを考える!

 

    • GraphQLを使ったAPI仕様中心開発の導入とその効果の紹介 – Kaizen Platform 開発者ブログ

 

    • 雑に始める GraphQL Ruby【class-based API】 – Qiita

 

    • hawksnowlog: Ruby (Sinatra) で GraphQL 入門

 

    • 既存のRailsプロジェクトにGraphQL APIを追加してみた – Qiita

 

    • Ruby on Rails で sprockets から Webpacker へ移行し、移行できないものは共存させる方法 – Qiita

 

    • Reading: 初めてGraphQL – 型の基礎|tkhm|note

 

    • https://github.com/loopstudio/rails-graphql-api-boilerplate

 

    https://github.com/rmosolgo/graphql-ruby-demo
广告
将在 10 秒后关闭
bannerAds