LoginSignup
21
8

More than 3 years have passed since last update.

graphql-rubyのmutationが勝手にInputObjectを生成する理由

Last updated at Posted at 2021-02-16

最近、新しくgraphql-ruby(v1.12.3)を使ったRailsアプリケーションを作ったら、下記のようなMutationのBaseクラスが生成されるようになっていました。

module Types
  class BaseMutation < GraphQL::Schema::RelayClassicMutation
    argument_class Types::BaseArgument
    field_class Types::BaseField
    input_object_class Types::BaseInputObject
    object_class Types::BaseObject
  end
end

私が初めてgraphql-rubyを使った時(v1.9系)はBaseMutationが生成されなかったため、GraphQL::Schema::Mutationを継承してMutationクラスを作っていたのですが、どうやら今はGraphQL::Schema::RelayClassicMutationを継承するようです。
(この記事を書くために過去のコードを辿ってみましたが、私が気づいていなかっただけでv1.9の頃からGraphQL::Schema::RelayClassicMutationはあったようです)

ドキュメントにも下記のように書かれれており、どうやらGraphQL::Schema::RelayClassicMutationを使った方が良さそうな感じです。

GraphQL::Schema::Mutation, a bare-bones base class
GraphQL::Schema::RelayClassicMutation, a base class with a set of nice conventions that also supports the Relay Classic mutation specification.

GraphQL::Schema::RelayClassicMutationを使ったらInputObjectが生成されるようになった

GraphQL::Schema::RelayClassicMutationを使ってみたところ、InputObjectが勝手に生成されるようになりました。
文章で書いてもわかりづらいと思うので、実際にGraphQL::Schema::RelayClassicMutationを継承したMutationと、GraphQL::Schema::Mutationを継承したMutationでパラメーターがどう変わるのか確認してみます。
両方ともinput / outputや処理内容は一緒です。

  • GraphQL::Schema::RelayClassicMutationを継承したMutation
# app/graphql/mutations/base_new_mutation.rb
module Mutations
  class BaseNewMutation < GraphQL::Schema::RelayClassicMutation
    argument_class Types::BaseArgument
    field_class Types::BaseField
    input_object_class Types::BaseInputObject
    object_class Types::BaseObject
  end
end

# app/graphql/mutations/create_new_user.rb
module Mutations
  class CreateNewUser < BaseNewMutation
    null false

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

    field :user, Types::UserType, null: false

    def resolve(name:, email:)
      { user: User.create!( name: name, email: email) }
    end
  end
end
  • GraphQL::Schema::Mutationを継承したMutation
# app/graphql/mutations/base_mutation.rb
module Mutations
  class BaseMutation < GraphQL::Schema::Mutation
  end
end

# app/graphql/mutations/create_old_user.rb
module Mutations
  class CreateOldUser < BaseMutation
    null false

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

    field :user, Types::UserType, null: false

    def resolve(name:, email:)
      { user: User.create!( name: name, email: email) }
    end
  end
end

それぞれのschemaを確認してみます。
createOldUserのパラメーターはCreateOldUserクラスにargumentで定義した通りemailとnameを受け取るようになっていますが、createNewUserのパラメーターはCreateNewUserInputという明示的には定義していないInputObjectになっています。
CreateNewUserInputの項目としてemailとnameを受け取れるようになっています。
(clientMutationIdという項目も追加されていますが、今回の本題とズレるため説明は省きます)

  type Mutation {
    createNewUser(
      """
      Parameters for CreateNewUser
      """
      input: CreateNewUserInput!
    ): CreateNewUserPayload!

    createOldUser(email: String!, name: String!): CreateOldUserPayload!
  }

  """
  Autogenerated input type of CreateNewUser
  """
  input CreateNewUserInput {
    """
    A unique identifier for the client performing the mutation.
    """
    clientMutationId: String
    email: String!
    name: String!
  }

  type CreateNewUserPayload {
    """
    A unique identifier for the client performing the mutation.
    """
    clientMutationId: String
    user: User!
  }

  type User {
    email: String!
    id: ID!
    name: String!
  }

考察

最初この挙動に気づいた時は、勝手にInputObjectを生成されてしまうことや1階層パラメーターが深くなってしまうことに拒否反応を起こしていたのですが、テンプレートの継承元がGraphQL::Schema::RelayClassicMutationになっており、こちらを使うのがデファクトスタンダードなのかと思い色々調べてみました。

色々調べた結果、一番参考になったのは下記のサイトでした。
https://www.apollographql.com/blog/designing-graphql-mutations-e09de826ed97/

詳細はURL先を参照していただくとして、要約すると「input objectを使った方がクライアントサイドが使いやすい。ネストは長所だ。」と記載されています(なかりざっくりした要約ですみません。とても良い記事なので気になる方は読んでみてください)

また、graphql.orgのドキュメントでもinputというパラメーターが使われており、input typesを使用することを勧めています。
(こちらは共通化の観点で書かれているので上記と論点は違いますが)
https://graphql.org/graphql-js/mutations-and-input-types/

A common example is that creating an object in a database and updating an object in a database often take the same parameters.
To make your schema simpler, you can use “input types” for this, by using the input keyword instead of the type keyword.

具体的に2つのcreateUserを使用するときのクエリーを比較してみます。
上記例ではパラメーターがnameとemailの2つしかありませんでしたが、違いがわかりやすいように追加で5項目(col1〜col5)あると仮定します。

  • createNewUserの場合
mutation CreateUser($CreateNewUserInput:CreateNewUserInput!) {
  createNewUser(input: $CreateNewUserInput) {
    user {
      id
      name
      email
    }
  }
}
variables
{
  "CreateNewUserInput": {
    "name": "name1",
    "email": "email1@example.com",
    "col1": "col1",
    "col2": "col2",
    "col3": "col3",
    "col4": "col4",
    "col5": "col5"
  }
}
  • createOldUserの場合
mutation CreateUser($name: String!, $email: String!, $col1: String!, $col2: String!, $col3: String!, $col4: String!, $col5: String!) {
  createOldUser(name: $name, email: $email, col1: $col1, col2: $col2, col3: $col3, col4: $col4, col5: $col5) {
    user {
      id
      name
      email
    }
  }
}
variables
{
  "name": "name2",
  "email": "email2@example.com"
  "col1": "col1", 
  "col2": "col2",
  "col3": "col3",
  "col4": "col4",
  "col5": "col5"
}

variablesはどちらでもほぼ同じですが、クエリーは前者の方がシンプルに書けることがわかると思います。
項目が増えれば増えるほどこの差は顕著になります。

この違いは実際にフロント側(GraphQLを呼び出す側)を実装しないと気づきづらい点だと思います。
今回の件、私もバックエンドの視点で見ていたときは記事に書いた通り予期せぬInputObjectが生成されて煩わしいと感じました。
実際にバックエンド側はどちらで書いてもメリットはほとんどありません。

ただ、フロントエンドの視点で実際に使ってみると、使い勝手に大きな違いがあると気づきました。

近年、フロントエンドとバックエンドの分業が進んでいますが、インターフェースのような両者の結節点は双方の観点で見る必要があると再認識するよい機会になりました。

21
8
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
21
8