GraphQL PR

【GraphQL】StrawberryでGraphQLに入門「Schema basics編」

【GraphQL】StrawberryでGraphQLに入門「Schema basics編」
記事内に商品プロモーションを含む場合があります

こんにちは!٩( ‘ω’ )و

受託中心に開発を行なっている企業で、Webエンジニアをしているモリヤス(@_moriyas)です!

サーバー側の開発で、PythonとStrawberryでGraghQLを使うことになったので、公式サイトを使って入門していきたいと思います!

A modern GraphQL library for Python | 🍓 Strawberry GraphQL

今回の記事では公式サイトのSchema basics(Schema basics | 🍓 Strawberry GraphQL)をやっていきたいと思います!

なお記事の内容は下記に注意してご覧くださいm(_ _)m

  • 主にGoogle翻訳を使い翻訳し、僕自身の解釈や理解を伝えるものになります。
  • わかりやすくするために、翻訳通りの説明になっていない部分や、僕自身が調べて追加説明してある部分があります。
  • 公式に書かれていることを全て書いていませんし(重要ではないなと思った箇所は省いている)、公式の見解通りに書いてるつもりですが、一致しているとは限りません。

※何か間違いなどあれば、教えていただけると幸いですm(_ _)m

環境・バージョン

  • Python 3.11(公式サイトではPython 3.8以上という説明がされています)
  • Mac
  • VSCode

※このチュートリアルではPythonについてとコマンドラインがそれなりにわかっているという前提で進めていきます。

スキーマ定義言語(SDL)

スキーマ定義言語(SDL)とはGraphQLのスキーマ(GraphQLサーバーの仕様を表現するもの)を定義する言語のことです。

SDLで下記のようにスキーマを定義します。

type Book {
  title: String!
  author: Author!
}
 
type Author {
  name: String!
  books: [Book!]!
}

!マークはnon-nullableという意味です。

スキーマは、すべての型とそれらの間の関係を定義します。

クライアント(フロントエンド)開発者は正確にどのようなデータかを確認することができるようになったり、そのデータの特定のサブセット(詳しくはまだわからないが、Schema定義で色々情報を付与できるのだと思っていて、そのことだと思う)を要求できるようになります。

スキーマの作成方法

GraphQLサーバーのスキーマを作るには下記2つの方法があります。

  • スキーマファースト
  • コードファースト

Strawberryではコードファーストのみサポートしているため、上記のSDLで書かれたスキーマは下記のように定義することになります。

import typing
import strawberry
 
 
@strawberry.type
class Book:
    title: str
    author: "Author"
 
 
@strawberry.type
class Author:
    name: str
    books: typing.List["Book"]

サポートしている型

GraphQLでは下記の型をサポートしています。

  1. Scalar型
  2. Object型
  3. Query型
  4. Mutation型
  5. Input型

Scalar(スカラー)型

Scalar型はPythonのプリミティブ型に似ています。 GraphQLのデフォルトのScalar型のリストは次のとおりです。

  • Int:Pythonのintにマップされる
  • Float:Pythonのfloatにマップされる
  • String:Pythonのstrにマップされる
  • Boolean(trueまたはfalse):Pythonのboolにマップされる
  • ID:オブジェクトを再フェッチする際や、キャッシュのキーとして使用される一意の識別子。文字列としてシリアル化され、strawberry.ID(“value”) として利用可能
  • UUID:文字列としてシリアル化されたUUID値
注意

GraghQLの仕様に正式には含まれていませんが、dateやtime、datetime のオブジェクトもサポートに含まれています。

また、独自にScalar型を指定することもできます。(「Scalars | 🍓 Strawberry GraphQL」)

上記でわからなかった用語まとめ
  • シリアル化・・・単純な構造にすること(要はキーバリューの形にすることだと思う)
  • UUID・・・世界中で重複しないことになっているID

参考

Object型

各フィールドはScalar型または別のObject型のいずれかになります。(フィールドは下記で言うとtitleやauthor、nameやbooksのこと)

また、Objecct型は下記のように相互に参照できます。

import typing
import strawberry
 
 
@strawberry.type
class Book:
    title: str
    author: "Author"
 
 
@strawberry.type
class Author:
    name: str
    books: typing.List["Book"]

※上記で言うと、Bookクラスのauthorフィールドは”Author”クラス型だし、Authorクラスのbooksフィールドは “Book”クラスのList型というようになっており、オブジェクト型を相互に参照している。

フィールドへのデータの提供

上記のスキーマでは、Bookクラスにはauthorフィールドがあり、Authorクラスにはbooks フィールドがありますが、定義されたスキーマの構造を満たすデータがどのように割り当てられるかがわかりません。

Strawberryでは、関数を通じてフィールドにデータを提供するためにresolverというものを使います。

booksとauthorでは、フィールドに値を提供するresolverを下記のように定義できます。

import typing
import strawberry


def get_author_for_book(root) -> "Author":
    return Author(name="Michael Crichton")
 
 
@strawberry.type
class Book:
    title: str
    author: "Author" = strawberry.field(resolver=get_author_for_book)
 
 
def get_books_for_author(root):
    return [Book(title="Jurassic Park")]
 
 
@strawberry.type
class Author:
    name: str
    books: typing.List[Book] = strawberry.field(resolver=get_books_for_author)


def get_authors(root) -> typing.List[Author]:
    return [Author(name="Michael Crichton")]
 
 
@strawberry.type
class Query:
    authors: typing.List[Author] = strawberry.field(resolver=get_authors)
    books: typing.List[Book] = strawberry.field(resolver=get_books_for_author)

strawberry.field(resolver=関数名)というように書くことで、フィールド(上記で言うとauthorやbooks)にデータを割り当てることができるようになります(関数の戻り値が割り当てられる)

Query型

Query型は、データに対してのGraphQLでの読み取り(CRUD処理で言うと、R)操作を定義するものです。

Query型の各フィールドは、クエリの名前と戻り値の型(GraphQLでサポートされている)を定義します。今回の例のスキーマのQuery型は次のようになります。

@strawberry.type
class Query:
    books: typing.List[Book]
    authors: typing.List[Author]

上記のQuery型は、booksとauthorsという2つのクエリを定義します。各クエリは、対応するタイプ(BookとAuthor)のリストを返します。

REST APIとGraphQLの違い
  • REST API:複数のリソースを取得するには異なるエンドポイントにアクセスすることになるので、複数のリクエストを必要とする。(上記の場合例えば、「/api/books」と「 /api/authors」というような感じ)
  • GraphQL:1回のリクエストで複数のリソースを取得できる

クエリの構造化

クライアントがデータに対して実行するクエリを作成すると、それらのクエリはスキーマで定義したObject型の形状と一致します。

上記までの例で考えると、下記のクエリで、すべてのbookのtitleのリストとすべてのauthorのnameのリストの両方を取得できます。

query {
  books {
    title
  }
 
  authors {
    name
  }
}

サーバーは次のようにクエリの構造に一致する下記の結果を返します。

{
  "data": {
    "books": [{ "title": "Jurassic Park" }],
    "authors": [{ "name": "Michael Crichton" }]
  }
}

これら2つの別々のリスト(bookとauthor)を取得することが便利な場合もありますが、各bookのauthorが結果に含まれるbookのリストを取得したいと思うかもしれません。

そういった場合は、Book型にはAuthor型のauthorフィールドがあるため、クエリを次のように構成できます。

query {
  books {
    title
    author {
      name
    }
  }
}

すると、サーバーはクエリの構造と一致する下記の結果を返します。

{
  "data": {
    "books": [
      { "title": "Jurassic Park", "author": { "name": "Michael Crichton" } }
    ]
  }
}

Mutation型

Mutation型は、データに対してのGraphQLでの書き込み(CRUD処理で言うと、C,U,D)操作を定義するものです。

Mutation型の各フィールドは、異なるミューテーションのシグネチャ(関数やメソッドの名前、引数の数やデータ型、返り値の型などの組み合わせのこと)と戻り値の型を定義します。今回の例のスキーマのMutation型は次のようになります。

@strawberry.type
class Mutation:
    @strawberry.field
    def add_book(self, title: str, author: str) -> Book: ...

上記のMutation型は、addBook を定義します。このミューテーションは2つの引数 (titleとauthor) を受け取り、新しく作成されたBookオブジェクトを返します。

注意

Strawberry は、デフォルトでフィールド名をスネークケースからキャメルケースに変換します。これは、スキーマでカスタム StrawberryConfig を指定することで変更できます。

ミューテーションの構造化

クエリと同様に、ミューテーションはスキーマの型定義の構造と一致します。次のミューテーションは、新しいBookを作成し、作成されたオブジェクトの特定のフィールドを戻り値として要求します。

mutation {
  addBook(title: "Fox in Socks", author: "Dr. Seuss") {
    title
    author {
      name
    }
  }
}

クエリと同様に、サーバーはこのミューテーションに対して、下記ように、ミューテーションの構造に一致する下記の結果を返します。

{
  "data": {
    "addBook": {
      "title": "Fox in Socks",
      "author": {
        "name": "Dr. Seuss"
      }
    }
  }
}

Input型

Input型は、(スカラー型のみを渡すのではなく) クエリやミューテーションに引数としてオブジェクトを渡すことができる特別なオブジェクト型です。Input型は、操作するためのシグネチャをクリーンに保つのに役立ちます。

Bookを追加するための前のミューテーションを考えてみましょう。

@strawberry.type
class Mutation:
    @strawberry.field
    def add_book(self, title: str, author: str) -> Book: ...

このミューテーションでは、2つの引数を受け入れる代わりに、これらのフィールドをすべて含む単一の入力タイプを受け入れることができます。これは、「publication date」(出版日)のような、将来追加の引数を受け入れることにした場合に特に便利です。(add_bokを複数回使っているとそれぞれのadd_bookの引数に出版日を追加しないといけなくなるが、inputで指定しておけば、inputのフィールドに出版日を追加すれば全てのadd_bookに適応される)

入力タイプの定義はオブジェクト型の定義と似ていますが、inputキーワードが使用されます。

@strawberry.input
class AddBookInput:
    title: str
    author: str
 
 
@strawberry.type
class Mutation:
    @strawberry.field
    def add_book(self, book: AddBookInput) -> Book: ...

これにより、スキーマ内でのAddBookInput型の受け渡しが容易になるだけでなく、GraphQL対応ツールによって自動的に公開される説明でフィールドに注釈を付けるための基礎も提供されます。(下記のdescriptonのことを言っているのだと思う)

@strawberry.input
class AddBookInput:
    title: str = strawberry.field(description="The title of the book")
    author: str = strawberry.field(description="The name of the author")

Input型は、複数の操作でまったく同じ情報セットが必要な場合に便利な場合がありますが、再利用は慎重に行う必要があります。操作は最終的に、必要な引数のセットにおいて分岐する可能性があります。

終わりに

これでGraghQLのSchemaの基礎についての入門完了d( ̄  ̄)