[RUBY] [Rails 6] Développement d'API à l'aide de GraphQL (Query)

introduction

Depuis que j'ai développé en utilisant GraphQL avec rails6, j'ai essayé de résumer la méthode d'introduction et l'utilisation. Cette fois, je présenterai le problème Requête / Association / N + 1.

Environnement de développement

ruby2.7.1 rails6.0.3 GraphQL

1. Qu'est-ce que GraphQL?

GraphQL est un langage de requête pour les requêtes API et possède les fonctionnalités suivantes.

--Un seul point de terminaison / graphql --Query: obtenir des données --Mutation: créer, mettre à jour, supprimer

La grande différence avec REST est que le premier point de terminaison est un. Dans le cas de REST, il existe plusieurs points de terminaison tels que / sign_up, / users, / users / 1, Pour GraphQL, le seul point final est / graphql.

REST nécessite plusieurs demandes d'API lorsque plusieurs ressources l'exigent GraphQL a un point de terminaison, donc vous pouvez obtenir les données dont vous avez besoin en une seule fois </ font> et le code est simple.

2. La table utilisée cette fois

Il est implémenté par deux, la table User parent et la table Post enfant. Supposons une relation un-à-plusieurs dans laquelle un utilisateur peut publier plusieurs publications.

Terminal


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

Table des utilisateurs

colonne Moule
name string
email string

user.rb


class User < ApplicationRecord
  has_many :posts, dependent: :destroy
end

Table de poste

colonne Moule
title string
description string

post.rb


class Post < ApplicationRecord
  belongs_to :user
end

3. Présentez GraphQL aux rails

Installez gem.

Gemgile


gem 'graphql'    #ajouter à

group :development, :test do
  gem 'byebug', platforms: [:mri, :mingw, :x64_mingw]
  gem 'graphiql-rails'   #Ajouté à l'environnement de développement
end

config/application.rb


require "sprockets/railtie"    #Décommenter

Terminal


$ bundle install
$ rails generate graphql:install   #Un fichier lié à GraphQL est créé

Ajoutez ce qui suit à routes.rb Le point final est / graphiql dans l'environnement de développement et / graphql dans l'environnement de production.

routes.rb


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'  #Voici les rails générer graphql:Généré automatiquement par l'installation
end

Veuillez vous référer aux détails ci-dessous. Comment utiliser GraphQL en mode API Rails 6 (y compris les contre-mesures d'erreur)

4. Requête pour obtenir la liste des utilisateurs

GraphQL a un Type (défini pour chaque modèle) et les requêtes sont exécutées en fonction de ce Type pour acquérir des données.

Créer un ObjectType pour l'utilisateur

Créez un type d'utilisateur avec la commande suivante. Si vous ajoutez !, Null: false sera ajouté.

Terminal


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

Les fichiers suivants seront générés.

user_type.rb


module Types
  class UserType < Types::BaseObject
    field :id, ID, null: false  # `!`Avec`null:false`Sera ajouté.
    field :name, String, null: false
    field :email, String, null: false
  end
end

Créer une requête utilisateur

Créez une requête basée sur le ʻuser_type` créé précédemment.

query_type.rb


module Types
  class QueryType < Types::BaseObject
    field :users, [Types::UserType], null: false #Définir l'utilisateur comme un tableau
    def users
      User.all #Obtenir la liste des utilisateurs
    end
  end
end

Créez des données utilisateur dans la console.

$ rails c
$ > User.create(name: "user1", email: "[email protected]")
$ > User.create(name: "user2", email: "[email protected]")

Exécuter l'ordre

Maintenant que vous êtes prêt, démarrez le serveur et vérifiez avec Graphiql. (http: // localhost: 3000 / graphiql)

$ rails s

Exécutez la requête suivante.

query{
  users{
    id
    name
    email
  }
}

Ensuite, une réponse au format json sera renvoyée. Le type d'utilisateurs étant un tableau, il s'agit d'un tableau.

{
  "data": {
    "users": [
      {
        "id": "1",
        "name": "user1",
        "email": "[email protected]"
      },
      {
        "id": "2",
        "name": "user2",
        "email": "[email protected]"
      }
    ]
  }
}

C'est le résultat de la connexion à http: // localhost: 3000 / graphiql et de son exécution. スクリーンショット 2020-09-27 20.32.34.png

Lors de la demande et de la réception uniquement des données nécessaires

Modifiez la requête si vous n'avez pas besoin de toutes les données de la colonne. Par exemple, vous ne pouvez obtenir que l'identifiant de l'utilisateur.

query{
  users{
    id
  }
}

réponse

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

5. Association

Ensuite, essayez d'associer le message à l'utilisateur. Créez ObjectType et Query for User de la même manière.

Générer un ObjectType pour la publication

Terminal


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

Les fichiers suivants seront générés. Ajoutez field: user, Types :: UserType, null: false pour obtenir les données de l'utilisateur.

post_type.rb


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 #Ajoutez cette phrase. fait parti_to :Quelque chose comme utilisateur
  end
end

Ajoutez field: posts, [Types :: PostType], null: false à UserType. Puisque plusieurs données sont liées ici, définissez-les comme un tableau.

user_type.rb


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 #Ajoutez cette phrase. a_many :Quelque chose comme des messages
  end
end

Terminal


$ rails c
$ > Post.create(title: "title", description: "description", user_id: 1)

Générer une requête de publication

query_type.rb


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

    #Ajoutez ce qui suit
    field :posts, [Types::PostType], null: false
    def posts
      Post.all
    end
  end
end

Exécuter l'ordre

La requête à exécuter. Imbriquez le message aux utilisateurs et demandez-le.

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

Une fois exécutées, les données post imbriquées seront renvoyées. Vous pouvez obtenir les données de ʻuser.posts` dans le cas de REST.

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

Si vous avez besoin des données de post.user, vous pouvez les obtenir avec la requête suivante.

query{
  posts{
    id
    title
    description
    user{
      id
    }
  }
}

スクリーンショット 2020-09-28 9.54.16.png

6. Introduisez Bullet pour détecter N + 1

Les requêtes GraphQL sont structurées en arborescence, les associations sont donc sujettes à des problèmes N + 1. Par conséquent, nous vous recommandons d'installer Bullet, qui est une gemme qui détecte N + 1.

Installer Bullet

Gemfile


group :development do
  gem 'bullet'
end

Terminal


$ bundle install

Ajoutez les paramètres suivants à config / environnements / development.rb

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

Vérifier N + 1

Une fois l'installation de Bullet terminée, vérifions si N + 1 se produit.

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

Si vous exécutez la même requête qu'auparavant, le journal suivant s'affiche.

Terminal


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

ʻAjoutez à votre requête: N + 1 se produit car il dit .includes ([: posts]) `. SQL a également été publié trois fois.

Terminal


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]]

Comment éliminer N + 1

Pour éliminer N + 1, «inclut» comme d'habitude. Vous pouvez le résoudre avec un chargeur de données, mais cette fois, nous utiliserons ʻincludes`. Modifiez la partie qui obtient la liste des utilisateurs comme suit.

query_type.rb


def users
  # User.all  #Changer avant
  User.includes(:posts).all #Après le changement
end

Maintenant, vérifions le journal pour voir si N + 1 a été résolu. L'avertissement est parti.

Terminal


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)

Comme le nombre d'instructions SQL a été réduit à deux, N + 1 a été résolu avec succès.

Terminal


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. Découpez le résolveur

Requête normale

Si vous écrivez le code normalement, les champs et les méthodes seront ajoutés à query_type quel que soit le modèle, donc query_type grandira de plus en plus.

query_type.rb


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

Requête avec le résolveur

Le problème GitHub présente les meilleures pratiques pour éviter le gonflement de query_type.rb à l'aide de Resolver. https://github.com/rmosolgo/graphql-ruby/issues/1825#issuecomment-441306410

Seul le champ est défini dans query_type.

query_type.rb


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

Ensuite, la partie de méthode est découpée en Resolver pour chaque ObjectType. (Un nouveau répertoire de résolveurs a été créé.) Si vous oubliez d'écrire GraphQL :: Schema :: Resolver, une erreur se produira, alors faites attention à ne pas l'oublier.

resolvers/query_types/users_resolver.rb


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

resolvers/query_types/posts_resolver.rb


module Resolvers::QueryTypes
  class PostsResolver < GraphQL::Schema::Resolver
    type [Types::PostType], null: false
    def resolve
      Post.all
    end
  end
end

À la fin

GraphQL a été étonnamment long lorsque j'ai également résumé ma critique. Je voulais aussi écrire sur rspec et mutation, mais j'aimerais le faire la prochaine fois.

Recommended Posts

[Rails 6] Développement d'API à l'aide de GraphQL (Query)
Notes de développement MOD à l'aide de l'API Minecraft 14.4 Fabric # 1
Construction de l'environnement du serveur API Rails à l'aide de docker-compose
Comment créer une requête à l'aide de variables dans GraphQL [Utilisation de Ruby on Rails]
Comment créer une API avec GraphQL et Rails
Utilisation de l'API PAY.JP avec Rails ~ Préparation de l'implémentation ~ (payjp.js v2)
[Rails] Développement avec MySQL
Utilisation de l'API PAY.JP avec Rails ~ Enregistrement de la carte ~ (payjp.js v2)
Développement de DSL avec ANTLR 4.7.1
Essayez d'utiliser l'attribut de requête Ruby on Rails
[Rails API + Vue] Télécharger et afficher des images à l'aide du stockage actif
[Rails] Créez un bot d'écho à l'aide de l'API de messagerie LINE.
Afficher la définition d'API dans l'interface utilisateur Swagger à l'aide de Docker + Rails6 + apipie
Construction de l'environnement de développement Rails6 [Mac]
Fonction de recherche à l'aide de [rails] ransack
Authentification SNS à l'aide de Rails google
[Rails] Enregistrez des images à l'aide de carrierwave
Japaneseize en utilisant i18n avec Rails
[Rails] Localisation japonaise à l'aide de rails-i18n
[Rails] Code de test à l'aide de Rspec
Erreur lors de l'utilisation des rails capybara
[Rails] Essayez d'utiliser le middleware de Faraday
Conseils détaillés lors de l'utilisation de Rails
[Rails 6] Revue en forme d'étoile utilisant Raty.js