2021/08/03

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を指定しています。これは、データの取得を行うという意味でしたね。

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

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

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


また、上記のようにクエリ変数を使用した場合は、GraphQL Playgroundの左下のQUERY VARIABLESからオブジェクト形式でクエリ変数を定義する必要があります。今回は、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
}


クエリの中にはpokemonts と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がPikachuであるもののnameとtypesを取得してきて」とお問い合わせしているかのようですね。

ここまでで、GraphQLのクエリについてざっくりと解説しました。

これから、もう少し細かいGraphQLのクエリの書き方について調べていきます。

ルート型


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

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

データの取得を行うQuery 型と、データの新規登録・更新・削除を行うMutaion 型、データの更新の監視を行う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 、suscription のいずれかを最初に記述する必要があります。

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はフィールドの配下にオブジェクトを無制限に入れ子にできます。入れ子にすることで、関連付けられたオブジェクトの詳細データを得るためのクエリを作ることができます。

スターウォーズのGraphQL APIから、具体例を見てみましょう。

以下のように型が定義してあります。

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
}


schema という場所を定義することで、ルート型である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のfimlConnection というフィールドの型はPersonFilmsConnection というオブジェクトの型です。

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

グラフで考えると、filmsというエッジを介して、複数のFilmのノードにアクセスできていることが分かります。

フラグメント

それでは次に、フラグメントについて考えていきましょう。

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

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

以下のようなうクエリになります。

query getPerson{
  AllPerson {
    name
    height
    age
  }
}




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

type Query {
  AllPerson: [Person]
}

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


ここで、フラグメントを定義しましょう。Person型に対応するフラグメントである、personInfo というフラグメントを定義します。フラグメントとは、特定の型に対応する選択セットのことでしたね。

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という型がIntern型だった場合にはこのフィールド値を返却して下さい、という感じでクエリを記述します。

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

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

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



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

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

上記のクエリでは、Employee型とIntern型のユニオン型であるPerson型に対して、Employee型だったら~のフィールドを、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というインターフェースから作成しています。EmployeeとPersonは[「name, height, age」の3つの型を必ず保つ必要があり、この3つの型を必ず持っていることが保証されます。

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

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



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

終わりに

今回の記事はここまでになります。次回からMutationやSubscriptionなども取り扱っていく予定です。

© 2021 powerd by UnReact