2021/08/20

GraphQLを学ぶ②

@ 酒井悠宇

GraphQL入門② GraphQL のクエリを理解しよう

引き続き西川さんの記事でGraphQLを勉強していきます!

初めに

今回はGraphQLのクエリについて理解する。

GraphQLに必要なもの

GraphQLに必要なものおは大きく分けて3つ。それは、クエリ、スキーマ、リゾルバ。

スキーマとリゾルバはGraphQLの設計側で定義しておき、それらに対してクエリを発行する。クエリはまずスキーマによってバリデーションされ、バリデーションを突場したクエリに対して対応するリゾルバが実行される。
今回は、その中のクエリに対して解説していく。

GraphQLのクエリ

ざっくり解説


まずはGraphQLのクエリをざっくりと解説する。
今回も前回と同様に、ポケモンのクエリを使っていく。
以下のエンドポイントに対してクエリを発行する。クエリはGraphQL Playgroundを用いて発行する。

https://graphql-pokemon2.vercel.app

GraphQLは単一のエンドポイントに対して発行する。
それでは実際にクエリを発行してみる。

query {
  pokemon(name: "Pikachu"){
    name
    types
  }
}


上記のクエリをエンドポイントに対して発行すると、以下のデータが返却される。

{
  "data": {
    "pokemon": {
      "name": "Pikachu",
      "types": [
        "Electric"
      ]
    }
  }
}


なんとなくわかるような気がするが、何が起きたのかについてざっくりと解説する。

queryの部分で「query・mutation・subscription」の3つの内、queryを指定している。これはデータの取得を行うと言う意味だった。
mutationは、データの作成、更新、削除。
subscriptionは、API側の変更を検知してフロントに通知する。

今回はクエリに名前をつけていないが、クエリには名前をつけることができる。また、クエリには変数を渡すこともでき、その変数のことをクエリ変数という。

上記のクエリを以下のように書き直しても問題なく動く。

query getPikachu($name: String) {
  pokemon(name: $name) {
    name
    types
  }
}


また、上記のようにクエリ変数を使用した場合は、GraphQL Playgroundの左下のQUERYVARIABLESからオブジェクト形式でクエリ変数を定義する必要がある。今回は、nameという名前でクエリ変数を定義したので、以下のようにクエリ変数を定義する。

{
  "name": "Pikachu"
}


このように、クエリには名前をつけることができるし、変数を渡すこともできる。クエリに変数を渡す場合は、型を定義する必要がある。
queryのように、クエリの一番上の層に定義するGraphQLに元々備わっているクエリの型のことを、ルート型と呼ぶ。入れ子になっているクエリドキュメントの一番上位には必ずルート型が指定されている。

queryの中には、オブジェクトのキーを指定するような形でpokemonを記述している。これをフィールドと呼んだりする。必要なフィールドは波括弧で囲まれたブロックの中に記述して指定する。この波括弧で囲んだブロックのことを選択セットと呼ぶ。

クエリはスキーマによって制限されている。ここで、一旦スキーマを確認してみる。

スキーマの内、関係がある部分だけ抜き出してみた。

type Query {
  pokemons(first: Int!): [Pokemon]
  pokemon(id: String, name: String): Pokemon
}

type Pokemon {
  id: ID!
  number: String
  name: String
  weight: PokemonDimension
  height: PokemonDimension
  classification: String
  types: [String]
  resistant: [String]
  attacks: PokemonAttack
  weaknesses: [String]
  fleeRate: Float
  maxCP: Int
  evolutions: [Pokemon]
  evolutionRequirements: PokemonEvolutionRequirement
  maxHP: Int
  image: String
}


クエリの中には、pokemonsとpokemonが存在することがわかる。また、pokemonsにはfirstと言う引数を渡す必要があり、pokemonにはidとnameと言う引数を渡す必要がある。

typeによってスキーマを定義しているが、Queryは特別な名前で、query・mutation・subscriptionと言う3つのルート型の内の一つであるqueryの型を定義している。

queryはpokemonとpokemonsと言う2つのフィールドを持つ。ここでもう一度クエリを確認してみる。

query {
  pokemon(name: "Pikachu") {
    name
    types
  }
}


上記のクエリはqueryの型の内、pokemonと言うフィールドを指定している。フィールド値はこのようにスキーマに定義してある全てのフィールドを指定する必要はなく、必要なものだけ指定すれば良い。

このようにスキーマとクエリは必ず対応している。スキーマには送信できる全てのクエリが型として定義されている。クエリは、スキーマで定義されている型の内、実際にアプリケーションの要件で使用するデータのみを選択セットのフィールドに指定してクエリを発行する。

また、スキーマもクエリも入れ子になっている。pokemonと言うフィールドのvalueに相当するものは、Pokemon型になっている。Pokemon型はオブジェクト形式であり、各々のフィールド値を持っている。

例えば、スキーマで定義しているqueryの中のpokemonの中のnameのデータを取得したい場合は、オブジェクト形式の入れ子になったクエリを発行する。上記のクエリは、さながら「queryの中のpokemonの内、nameがPicachuであるもののnameとtypesを取得してきて」とお問い合わせしているかのよう。

ここまでで、GraphQLのクエリについてざっくりと解説した。
これから、もう少し細かいGraphQLのクエリの書き方について調べていく。

ルート型

まず最初に、GraphQLのルート型について解説していく。

GraphQLに存在するルート型は、データソースに対する操作を表現する型。

データの取得を行うQuery型と、データの新規登録・更新・削除をおこなうMutation型、データの更新の監視を行うSubscription型が存在する。

これら3つのルート型は、スキーマの世界で定義を行い、そのスキーマに対応するような形でクエリを発行する。
そしてクエリに応じてリゾルバが発火し、データが返却される。

ルート型のスキーマの定義は以下のようになっている。

type Query {
  pokemons(first: Int!): [Pokemon]
  pokemon(id: String, name: String): Pokemon
}

type Mutation {
  pokemons(first: Int!): [Pokemon]
  pokemon(id: String, name: String): Pokemon
}

type Subscription {
  pokemons(first: Int!): [Pokemon]
  pokemon(id: String, name: String): Pokemon
}


このルート型に対してクエリを発行する際は、query、mutation、subscriptionのいずれかを最初に記述する必要がある。

query getPikachu($name: String) {
  pokemon(name: $name) {
    name
    types
  }
}


GraphQLのクエリの書き方はこのようになっている。
次はクエリ変数について見ていく。

クエリ変数

それではクエリ変数について解説していく。先程から何度か登場しているが、クエリ変数とは名前の通り、クエリに渡す変数のこと。

具体的には以下になる。

query getPikachu($name: String) {
  pokemon(name: $name) {
    name
    types
  }
}


この場合、$nameはクエリ変数になる。クエリ変数は、ルートのクエリに対して()で渡す。上記の例では、ルートのクエリに対して()で渡す。上記の例では、ルートのクエリのgetPikachuと言う名前を設定しているが、以下のように名前を渡さなくても普通に動く。

query ($name: String) {
  pokemon(name: $name) {
    name
    types
  }
}


クエリ変数は、query、mutation、subscription等のルート型のクエリの後に()付きで設定するものだと認識しておいて大丈夫。またクエリ変数には必ず型を設定する。またクエリ変数は必ず$記号から始まる。

設定したクエリ変数は、そのクエリのブロック内で使用することができる。また、クエリ変数は以下の用意なオブジェクト形式で設定し、クエリを投げる際に応じた規定の場所に渡す。

{
  "name": "Pikachu"
}


GraphQL Playgroundでは左下の「QUERY VARIABLES」と言う部分にオブジェクト形式で設定する。Applo Client等を使用する際は、クエリ変数となるオブジェクトを変数の中に入れておいて、それらをApplo Clientの規定の場所に渡す感じで使う。

スカラー型とオブジェクト型

GraphQLには、スカラー型とオブジェクト型が存在する。

スカラー型はJavaScriptのプリミティブ型に近い概念で、GraphQLの構造を樹木構造に例えるなら葉の要素になる。GraphQLにはデフォルトで5つのスカラー型が用意されている。

デフォルトで備わっているスカラー型は以下の5つ。

  • 整数型(Int)
  • 浮動小数点型(Float)
  • 文字列型(String)
  • 論理型(Boolean)
  • ID型(ID)


IDとStringは同様に文字列で表されるが、ID型は一意な値でなければならないと言う制約が追加される。

GraphQLのオブジェクト型は、一つ以上のスキーマで定義されているフィールドの集合で、返されるJSONオブジェクトの形を規定する。

GraphQLはフィールドの配下にオブジェクトを無制限に入れ子にできる。入れ子にすることで、関連づけられたオブジェクトの詳細データを得るためのクエリを作ることができる。

スターウォーズのGraphQLAPIから具体例を見てみる。

以下のように型が定義してある。

schema {
  query: Root
}

type Root {
  allFilms(
    after: String
    first: Int
    before: String
    last: Int
  ): FilmsConnection
  film(id: ID, filmID: ID): Film
  allPeople(
    after: String
    first: Int
    before: String
    last: Int
  ): PeopleConnection
  person(id: ID, personID: ID): Person
  allPlanets(
    after: String
    first: Int
    before: String
    last: Int
  ): PlanetsConnection
  planet(id: ID, planetID: ID): Planet
  allSpecies(
    after: String
    first: Int
    before: String
    last: Int
  ): SpeciesConnection
  species(id: ID, speciesID: ID): Species
  allStarships(
    after: String
    first: Int
    before: String
    last: Int
  ): StarshipsConnection
  starship(id: ID, starshipID: ID): Starship
  allVehicles(
    after: String
    first: Int
    before: String
    last: Int
  ): VehiclesConnection
  vehicle(id: ID, vehicleID: ID): Vehicle
  node(id: ID!): Node
}


shemaと言う場所で定義することで、ルート型であるqueryとmutationとsubscriptionの型を決めることができる。今回は、queryの型にRootと言うオブジェクト型を定義した。

このように、型は入れ子にすることができる。

実際に型とクエリを確認してみる。

PersonのIDを指定して、スター・ウォーズの登場人物のデータを取得する。また、Personと言うオブジェクトの内、nameとgenderを取得するようにクエリを発行する。

まずは型の確認をする。

schema {
  query: Root
}

type Root {
  person(id: ID, personID: ID): Person
}

type Person implements Node {
  name: String
  birthYear: String
  eyeColor: String
  gender: String
  hairColor: String
  height: Int
  mass: Float
  skinColor: String
  homeworld: Planet
  filmConnection(
    after: String
    first: Int
    before: String
    last: Int
  ): PersonFilmsConnection
  species: Species
  starshipConnection(
    after: String
    first: Int
    before: String
    last: Int
  ): PersonStarshipsConnection
  vehicleConnection(
    after: String
    first: Int
    before: String
    last: Int
  ): PersonVehiclesConnection
  created: String
  edited: String
  id: ID!
}


ルート型であるqueryの型として、Rootと言う型が定義してある。schemaはルート型であるqueryとmutationとsubscriptionの型を決めることができる。Root型は選択フィールドとしてPersonをもち、Personは選択フィールドとして、String型のnameとgenderを持つので、それを取得するクエリは以下のようになる。また、personに引数を渡すことで、どの登場人物を取得するのかを指定している。

query {
  person(id: 1, personID:1){
    name
    gender
  }
}


このクエリを発行すると、以下のデータが返却される。

{
  "data": {
    "person": {
      "name": "Luke Skywalker",
      "gender": "male"
    }
  }
}


nameとgenderの型はデフォルトのスカラー型であるString型。また、personと言うフィールドには、オブジェクト型であるPerson が定義されている。

ここで登場人物の出演した映画のタイトル一覧を取得してみる。関係がある型だけ抜き出してみる。
型はこんな感じになっている。

schema {
  query: Root
}

type Root {
  person(id: ID, personID: ID): Person
}

type Person implements Node {
  filmConnection(
    after: String
    first: Int
    before: String
    last: Int
  ): PersonFilmsConnection
}

type PersonFilmsConnection {
  pageInfo: PageInfo!
  edges: [PersonFilmsEdge]
  totalCount: Int
  films: [Film]
}

type Film implements Node {
  title: String
}


この型(スキーマ)に対してクエリを発行する。

query {
  person(id: 1, personID:1){
    name
    gender
    filmConnection {
      films {
        title
      }
    }
  }
}


上記のクエリを発行すると、以下のデータが返却される。

{
  "data": {
    "person": {
      "name": "Luke Skywalker",
      "gender": "male",
      "filmConnection": {
        "films": [
          {
            "title": "A New Hope"
          },
          {
            "title": "The Empire Strikes Back"
          },
          {
            "title": "Return of the Jedi"
          },
          {
            "title": "Revenge of the Sith"
          }
        ]
      }
    }
  }
}


このように、型は樹木の構造になっている。

personというフィールドの型はPerson型になっている。これはGraphQLのオブジェクトの型。またPersonのfilmConnectionと言うフィールドの型はPersonFilmsConnectionと言うオブジェクトの型になっている。

そして、PersonFilmsConnectionのフィールドであるfilmsの型はFilmと言うオブジェクトの型のリストになっている。このように、filmとfilmConnectionには1対多の関係がある。

グラフで考えると、filmsと言うエッジ(辺)に対して、複数のFilmのノード(点)にアクセスできていることがわかる。

フラグメント

それではつぎにフラグメントについて考えていく。

フラグメントは、複数の場所で使いまわすことができる選択セット。また、フラグメントは特定の型に対応する選択セットであり、対応する型を必ず書いておく必要がある。

例えば、会社の社員の情報を提供することができる架空のGraphQLのクエリを考える。

以下のようなクエリになる。

query getPerson{
  AllPerson {
    name
    height
    age
  }
}


以下のようなスキーマが定義されている。

type Query {
  AllPerson: [Person]
}

type Person {
  name: String
  height: String
  age: Int
}


ここでフラグメントを定義する。Person型に対応するフラグメントである、personInfoと言うフラグメントを定義する。フラグメントとは、特定の方に対応する選択セットのこと。

「fragment フラグメント名 on フラグメントにしたいスキーマの名前」と言うふうに使う。
具体的には、以下のようになる。

fragment personInfo on Person {
  name
  height
  age
}


このフラグメントを使えば、クエリを以下のように定義することができる。

query {
  AllPerson {
    ...personInfo
  }
}


このようにフラグメントはスプレット演算子と非常に似通った書き方をしている。上記の書き方は、フラグメントで定義した全ての選択セットを記述するのと同様の書き方になる。

ユニオン型

複数のオブジェクトを含みうるリストが欲しい場合、複数の異なるオブジェクト型をまとめるユニオン型を定義できる。

このユニオン型はTypeScriptと同じで、A or Bである型は結局のところ、どちらかの型になる。

ここでも会社の社員の情報を提供するような架空のGraphQLを考える。

Personと言うタイプを、社員(Employee)とインターン生(Intern)の2つのタイプのユニオン型であるとする。

型は以下のようになる。

union Person = Employee | Intern

type Query {
  AllPerson: [Person]
}

type Employee {
  name: String
  height: Int
  age: Int
  email: String
}

type Intern {
  name: String
  height: String
  age: Int
  grade: Int
}


このようにPersonはEmployeeとInternのユニオン型として定義しておく。

AllPersonというクエリは、Person型を返却するが、Person型はEmployeeとInternのユニオン型を担っている。これをクエリの世界で表現するには、フラグメントを使って条件分岐のようにクエリを記述する必要がある。

イメージとしては、Personと言う型がEmployee型だった場合にはこのフィールド返却して、Personと言う型がEmployeeだった場合にはこのフィールド地を返却してください。と言う感じでクエリを記述する。

クエリの書き方としては、名前を持たないフラグメントを使用するインラインフラグメントと呼ばれる書き方と、一度ユニオン型で名前付きのフラグメントを指定して使用する書き方の二種類がある。

インラインフラグメントを使用してクエリを送る具体的な記述は以下のようになる。

query {
  AllPerson {
    ...on Employee {
      name
      height
      age
      email
    }
    ...on Intern {
      name
      height
      age
      grade
    }
  }
}


これは、今までのフラグメントの使い方と異なる。今まではフラグメントを型付きで定義しておいてから使用していた。フラグメントは、型付で定義するフィールドの集合体だった。

今回はフラグメントを型付きで定義することなく使用している。また、その際の型はスキーマのユニオン型の定義で使用した型のどれかを指定する。

上記のクエリでは、Employee型とIntern型のユニオン型であるPerson型に対して、Employee型だったら~のフィールドを、Intern型だったら~のフィールドを、Intern型だったら~のフィールドを取得する、と言う書き方になっている。

ユニオン型に対するクエリは以下のようにインラインフラグメントで記述することもできるが、下記のように名前付きのフラグメントを使用することも可能。

query {
  AllPerson {
    ...employee 
    ...intern 
  }
}

fragment employee on Employee {
  name
  height
  age
  email
}

fragment intern on Intern {
  name
  height
  age
  grade
}


インターフェース

インターフェースは複数のオブジェクトを扱うためのユニオン型とは別の選択制。
ユニオン型はX or Yという考え方だったが、インターフェース型は「必ず持つべき型を定義する」ということから始まる。

複数のオブジェクト型が必ず持つべき型をあらかじめ定義しておき、その複数のオブジェクトはクラスの継承のような形で、その「必ず持つべき型が定義されたオブジェクト」を継承すうる。厳密には継承ではないが、複数のオブジェクトを「必ず持つべき型が定義されたオブジェクト」から派生させることで、クエリの世界において、その派生オブジェクトは必ず派生元の型を持っていることが保証される。

インターフェースによる型の定義を見てみる。
社員(Employee)という型と、インターン生(Intern)という2つの型があり、それらの型は必ず「name, height, age」を持つという状態を考える。

この時、EmployeeとInternという型は、「name, height, age」を持つPersonと言う型の派生として考える。

スキーマの定義は以下のようになる。

type Query {
  AllPerson: [Person]
}

type Person {
  name: String
  height: Int
  age: Int
}

type Employee implements Person {
  name: String
  height: Int
  age: Int
  email: String
}

type Intern implements Person {
  name: String
  height: Int
  age: Int
  grade: Int
}


EmployeeとInternはPersonは「name, height, age」の3つの型を必ず保つ必要があり、この3つの型を必ず持っていることが保証される。

このようなインターフェース型に対してクエリを発行する際は以下のようになる。

query {
  AllPerson {
    name
    height
    age
    ...on Employee {
      email
    }
  }
}


AllPersonの中の型はEmployeeかInternのどちらかになるが、そのどちらも「name, height, age」の3つのフィールドを持っている。そのため、クエリの世界においてもそれは保証されているため、「~の型の場合」と言う条件分岐を書くことなく、クエリを発行することができる。


以上。あざました!

© 2021 powerd by UnReact