🍓

Pythonで始めるGraphQL【Strawberry】

に公開

はじめに

今回はStrawberryというライブラリを使って、PythonでGraphQLについて紹介したいと思います。

GraphQLは、FacebookによってRESTの課題を解決するために開発されたクエリ言語です。

具体的には以下のようなことができます。

特定のフィールドだけ取得

Bookオブジェクトはtitle, autherというフィールドを持っているとします。

この時autherが不要なら、Bookのtitle一覧のみ取得することができます。

ネストしたフィールドの取得

Userオブジェクトはid, name, follow_idsというフィールドを持っているとします。

idに基づきUserのnameを取得して、さらにfollow_idsに基づきフォローしているUserのnameを取得できます。

必要な情報を必要な分だけ取り出せるのがGraphQLの特徴ということになります。

もちろん、REST APIでもサーバー側でエンドポイントを作れば同じようなことができますが、GraphQLではエンドポイントを増やすことなくクライアント側でそれができるのがメリットです。

また、図中のGraphQLサーバーとデータベースの間に書かれているように、実際のデータの取得はGraphQLサーバーでプログラムする必要があります。

あくまで、GraphQLサーバーはクエリを解釈してデータを返すものになっています。

実行環境の作成

Getting startedに従って環境を作ります。
https://strawberry.rocks/docs

プロジェクトの準備

まず、新しいプロジェクトディレクトリを作成します。

mkdir strawberry-demo
cd strawberry-demo

仮想環境とライブラリのインストール

お好みの環境管理ツールを使用して、仮想環境を作成し、Strawberryをインストールします。

# pipの場合
pip install 'strawberry-graphql[debug-server]'

# poetryの場合
poetry add 'strawberry-graphql[debug-server]'

# uvの場合
uv add 'strawberry-graphql[debug-server]'

最初のGraphQLスキーマ

GraphQLの基本的な概念を理解するため、シンプルなヘルスチェックAPIを実装してみましょう。

スキーマの定義

schema.pyを作成し、以下のコードを記述します。

import strawberry

def get_status():
    """ステータスコード200を返すリゾルバー関数"""
    return "200"

@strawberry.type
class Query:
    """クエリのルート型定義"""
    status: str = strawberry.field(resolver=get_status)

schema = strawberry.Schema(query=Query)

ローカルサーバーの起動

スキーマを定義したら、開発用サーバーを起動します。

strawberry server schema
# Running strawberry on <http://0.0.0.0:8000/graphql> 🍓

GraphQLプレイグラウンド

この状態でブラウザでhttp://localhost:8000/graphqlを開くと、GraphQLプレイグラウンドが表示されます。

このプレイグラウンドでは、クエリを実行して試すことができます。

クエリの実行

作成したヘルスチェックAPIを試してみましょう。

基本的なクエリ

ブラウザで以下のクエリを入力し、実行ボタンを押すかCtrl+Enterで実行します。

query {
  status
}

サーバーからのレスポンス

{
  "data": {
    "status": "200"
  }
}

実行結果のスクリーンショット

GraphQLの主要コンポーネント

GraphQLスキーマは、以下の3つの主要なコンポーネントで構成されています。先ほどのコードをもとに
、それぞれの役割と実装方法を詳しく見ていきましょう。

1. Query

@strawberry.type
class Query:
    status: str = strawberry.field(resolver=get_status)

Queryは以下のFieldを集めたものになります。

2. Field

status: str = strawberry.field(resolver=get_status)

FieldはGraphQLサーバーが返すことのできるものになります。この場合は、statusというstrを返すことができます。

3. Resolver

def get_status() -> str:
    """ステータスコードを返すリゾルバー関数"""
    return "200"

ResolverはFieldが指定されたときに、実際にどのような値を返すか決める関数になります。この場合は、固定でステータスコード"200"を返しています。

コンポーネント間の関係

以下の図は、これらのコンポーネントの連携を示しています。

オブジェクトタイプ

オブジェクトを使用して値を返すこともできます。
ステータスコードだけではなくメッセージも追加したTypeを作成します。
先ほどのschema.pyに追記していきます。
まずは、ヘルスチェック用のTypeを定義します。これは出力の型になります。

@strawberry.type
class HealthCheck:
    status: str
    message: str

続いて、HealthCheckTypeのリゾルバを定義します。

def get_health():
    return HealthCheck(status="200", message="ok")

QueryのFieldにhealth_checkを追加します。

@strawberry.type
class Query:
    # status: str = ...
    health_check: HealthCheck = strawberry.field(resolver=get_health)

完全なコード
import strawberry

def get_status():
    return "200"

@strawberry.type
class HealthCheck:
    status: str
    message: str

def get_health():
    return HealthCheck(status="200", message="ok")

@strawberry.type
class Query:
    status: str = strawberry.field(resolver=get_status)
    health_check: HealthCheck = strawberry.field(resolver=get_health)

schema = strawberry.Schema(query=Query)

以下のようにクエリを入力して実行します。
ヘルスチェックのクエリを実行してみましょう。

query {
  healthCheck{
    status
    message
  }
}

サーバーからのレスポンス

{
  "data": {
    "healthCheck": {
      "status": "200",
      "message": "ok"
    }
  }
}

フィールドの選択

GraphQLではREST APIとは異なり、クライアント側で取得する内容を選択することができます。
例えば、messageだけ取得したい場合は以下のようにクエリを入力して実行します。
フィールドを選択して実行してみましょう。

query {
  healthCheck{
    message
  }
}

サーバーからのレスポンス

{
  "data": {
    "healthCheck": {
      "message": "ok"
    }
  }
}

なお、フィールドを1つも選択しないことはできません。
フィールドを選択せずに実行してみると、以下のようなエラーが発生します。

query {
  healthCheck
}

サーバーからのエラーレスポンス

{
  "data": null,
  "errors": [
    {
      "message": "Field 'healthCheck' of type 'HealthCheck!' must have a selection of subfields. Did you mean 'healthCheck { ... }'?",
      "locations": [
        {
          "line": 2,
          "column": 3
        }
      ]
    }
  ]
}

このエラーは、オブジェクトタイプのフィールドに対して、取得したいフィールドを少なくとも1つは指定する必要があることを示しています。

ネストしたオブジェクトタイプ

ネストしたオブジェクトも定義することができます。
infoフィールドを追加したHealthCheckWithInfoTypeを作成します。
まずはInfoTypeとResolverを定義します。

@strawberry.type
class Info:
    version: str
    description: str

def get_info():
    return Info(version="1.0", description="This is a health check")

HealthCheckWithInfoTypeを作成します。
infoフィールドはリゾルバーを使って解決するようにします。

@strawberry.type
class HealthCheckWithInfo:
    status: str
    message: str
    info: Info = strawberry.field(resolver=get_info)

HealthCheckWithInfoのリゾルバーではinfoは指定しなくてよいです。

def get_health_with_info():
    return HealthCheckWithInfo(status="200", message="ok")

完全なコード
import strawberry

def get_status():
    return "200"

@strawberry.type
class HealthCheck:
    status: str
    message: str

def get_health():
    return HealthCheck(status="200", message="ok")

@strawberry.type
class Info:
    version: str
    description: str

def get_info():
    return Info(version="1.0", description="This is a health check")

@strawberry.type
class HealthCheckWithInfo:
    status: str
    message: str
    info: Info = strawberry.field(resolver=get_info)

def get_health_with_info():
    return HealthCheckWithInfo(status="200", message="ok")

@strawberry.type
class Query:
    status: str = strawberry.field(resolver=get_status)
    health_check: HealthCheck = strawberry.field(resolver=get_health)
    health_check_with_info: HealthCheckWithInfo = strawberry.field(
        resolver=get_health_with_info
    )

schema = strawberry.Schema(query=Query)

以下のようにクエリを入力して実行します。
ネストしたオブジェクトを含むクエリを実行してみましょう。

query {
  healthCheckWithInfo{
    status
    message
    info {          # ネストしたInfoオブジェクト
      version
      description
    }
  }
}

サーバーからのレスポンス

{
  "data": {
    "healthCheckWithInfo": {
      "status": "200",
      "message": "ok",
      "info": {
        "version": "1.0",
        "description": "This is a health check"
      }
    }
  }
}

ネストしていてもフィールドの選択は同じようにできます。
特定のフィールドのみを選択して実行してみましょう。

query {
  healthCheckWithInfo{
    message           # メッセージのみ
    info {           # infoオブジェクトから
      description    # descriptionのみを選択
    }
  }
}

サーバーからのレスポンス

{
  "data": {
    "healthCheckWithInfo": {
      "message": "ok",
      "info": {
        "description": "This is a health check"
      }
    }
  }
}

このように、必要なフィールドのみを選択的に取得できます。

GraphQLによるCRUD操作の実装

データモデリングとスキーマ設計

GraphQLでのCRUD(Create, Read, Update, Delete)操作を、書籍管理システムを例に実装していきます。

データモデルの定義

まず、bookディレクトリにschema.pyを作成し、基本的なデータ構造を定義します。

from datetime import datetime
import strawberry

# サンプルデータ
book_list = [
    {
        "title": "The Great Gatsby",
        "author": "F. Scott Fitzgerald",
        "publication_date": datetime(1925, 4, 10),
    },
    {
        "title": "Jurassic Park",
        "author": "Michael Crichton",
        "publication_date": datetime(1990, 11, 20),
    },
]

@strawberry.type
class Book:
    """書籍を表すGraphQLオブジェクトタイプ"""
    title: str
    author: str
    publication_date: datetime

def list_books() -> list[Book]:
    """全書籍を取得するリゾルバー"""
    return [Book(**book) for book in book_list]

@strawberry.type
class Query:
    """クエリのルート型"""
    list_books: list[Book] = strawberry.field(
        resolver=list_books,
        description="全書籍のリストを取得"
    )

# スキーマの定義
schema = strawberry.Schema(query=Query)

今回はQueryのフィールドとして、Bookタイプを使うのではなくそのリストとしています。
リストの場合でも、Bookのフィールドを指定してクエリします。

query {
  listBooks{
    title
    author
    publicationDate
  }
}

{
  "data": {
    "listBooks": [
      {
        "title": "The Great Gatsby",
        "author": "F. Scott Fitzgerald",
        "publicationDate": "1925-04-10T00:00:00"
      },
      {
        "title": "Jurassic Park",
        "author": "Michael Crichton",
        "publicationDate": "1990-11-20T00:00:00"
      }
    ]
  }
}

必要なフィールドのみを選択して取得することもできます。

query {
  listBooks{
    title    # タイトルのみを取得
  }
}

サーバーからのレスポンス

{
  "data": {
    "listBooks": [
      {
        "title": "The Great Gatsby"
      },
      {
        "title": "Jurassic Park"
      }
    ]
  }
}

このように、必要なフィールドだけを指定することで、効率的なデータ取得が可能です。

引数の使用

タイトルを使用してBookを取得するgetBookフィールドを作成します。
そのために引数のあるリゾルバーを使用します。

def get_book(title: str):
    """タイトルに一致する書籍を取得するリゾルバー"""
    return next(Book(**book) for book in book_list if book["title"] == title)

@strawberry.type
class Query:
    list_books: list[Book] = strawberry.field(resolver=list_books)
    get_book: Book = strawberry.field(resolver=get_book)

タイトルを指定して特定の書籍を取得してみましょう。

query {
  book(title: "The Great Gatsby"){    # タイトルで書籍を指定
    title                            # 取得したいフィールド
    author
    publicationDate
  }
}

サーバーからのレスポンス

{
  "data": {
    "book": {
      "title": "The Great Gatsby",
      "author": "F. Scott Fitzgerald",
      "publicationDate": "1925-04-10T00:00:00"
    }
  }
}

このように、引数を使用して特定のデータを取得できます。

データ更新操作の実装

GraphQLでは、データの更新操作をMutation型として定義します。使い方としてはほとんどQueryと変わりません。

まず、書籍を追加するリゾルバーを定義します。

def add_book(title: str, author: str, publication_date: datetime) -> Book:
    """新しい書籍を追加するリゾルバー

    Args:
        title: 書籍のタイトル
        author: 著者名
        publication_date: 出版日

    Returns:
        Book: 作成された書籍オブジェクト(推奨)
    """
    book = Book(title=title, author=author, publication_date=publication_date)
    book_list.append(vars(book))
    return book

次に、Mutation型を定義します。

@strawberry.type
class Mutation:
    """データ更新操作のルート型"""
    add_book: Book = strawberry.mutation(
        resolver=add_book,
        description="新しい書籍を追加"
    )

最後に、スキーマにMutationを追加します。

schema = strawberry.Schema(
    query=Query,
    mutation=Mutation,
    description="書籍管理システムのGraphQLスキーマ"
)

完全なコード
from datetime import datetime

import strawberry

book_list = [
    {
        "title": "The Great Gatsby",
        "author": "F. Scott Fitzgerald",
        "publication_date": datetime(1925, 4, 10),
    },
    {
        "title": "Jurassic Park",
        "author": "Michael Crichton",
        "publication_date": datetime(1990, 11, 20),
    },
]

@strawberry.type
class Book:
    title: str
    author: str
    publication_date: datetime

def get_book(title: str):
    return next(Book(**book) for book in book_list if book["title"] == title)

def list_books():
    return [Book(**book) for book in book_list]

@strawberry.type
class Query:
    list_books: list[Book] = strawberry.field(resolver=list_books)
    get_book: Book = strawberry.field(resolver=get_book)

def add_book(title: str, author: str, publication_date: datetime) -> Book:
    book = Book(title=title, author=author, publication_date=publication_date)
    book_list.append(vars(book))
    return book

@strawberry.type
class Mutation:
    add_book: Book = strawberry.mutation(resolver=add_book)

schema = strawberry.Schema(query=Query, mutation=Mutation)

新しい書籍を追加してみましょう。

mutation {
  addBook(                                                              # 書籍追加のMutation
    title: "Little Prince",                                            # 書籍のタイトル
    author: "Antoine de Saint-Exupéry",                               # 著者名
    publicationDate: "1943-04-01T00:00:00"                           # 出版日
  ) {
    title           # 追加した書籍の情報を取得
    author
    publicationDate
  }
}

サーバーからのレスポンス

{
  "data": {
    "addBook": {
      "title": "Little Prince",
      "author": "Antoine de Saint-Exupéry",
      "publicationDate": "1943-04-01T00:00:00"
    }
  }
}

このように、Mutationを使用してデータを追加できます。

なお、GraphQLではデータの更新後はそのオブジェクトを返すことがベストプラクティスとされています。

念のため、リストを確認しましょう。

query {
  listBooks{
    title
    author
    publicationDate
  }
}

{
  "data": {
    "listBooks": [
      {
        "title": "The Great Gatsby",
        "author": "F. Scott Fitzgerald",
        "publicationDate": "1925-04-10T00:00:00"
      },
      {
        "title": "Jurassic Park",
        "author": "Michael Crichton",
        "publicationDate": "1990-11-20T00:00:00"
      },
      {
        "title": "Little Prince",
        "author": "Antoine de Saint-Exupéry",
        "publicationDate": "1943-04-01T00:00:00"
      }
    ]
  }
}

同じようにして、削除・更新も実装することができます。簡単なのでやってみてください。

完全なコード
from datetime import datetime

import strawberry

book_list = [
    {
        "title": "The Great Gatsby",
        "author": "F. Scott Fitzgerald",
        "publication_date": datetime(1925, 4, 10),
    },
    {
        "title": "Jurassic Park",
        "author": "Michael Crichton",
        "publication_date": datetime(1990, 11, 20),
    },
]

@strawberry.type
class Book:
    title: str
    author: str
    publication_date: datetime

def get_book(title: str):
    return next(Book(**book) for book in book_list if book["title"] == title)

def list_books():
    return [Book(**book) for book in book_list]

@strawberry.type
class Query:
    list_books: list[Book] = strawberry.field(resolver=list_books)
    get_book: Book = strawberry.field(resolver=get_book)

def add_book(title: str, author: str, publication_date: datetime) -> Book:
    book = Book(title=title, author=author, publication_date=publication_date)
    book_list.append(vars(book))
    return book

def remove_book(title: str) -> Book:
    book = next(book for book in book_list if book["title"] == title)
    book_list.remove(book)
    return Book(**book)

def update_book(title: str, author: str, publication_date: datetime) -> Book:
    book = next(book for book in book_list if book["title"] == title)
    book["author"] = author
    book["publication_date"] = publication_date
    return Book(**book)

@strawberry.type
class Mutation:
    add_book: Book = strawberry.mutation(resolver=add_book)
    remove_book: Book = strawberry.mutation(resolver=remove_book)
    update_book: Book = strawberry.mutation(resolver=update_book)

schema = strawberry.Schema(query=Query, mutation=Mutation)

ここで見たように、QueryとMutationは本質的に違いはありません。Queryのリゾルバーに削除処理を入れることも可能です。しかし、クライアントが使いやすいAPIになるように参照系はQuery、それ以外はMutationと分けることが、GraphQLのベストプラクティスとされています。

応用的なクエリ

Userを扱うAPIを例にGraphQLでの応用的なクエリを扱っていきます。
ここからはuserディレクトリでschema.pyを作成して作業していきます。
ローカルサーバーはstrawberry server user.schemaで起動します。

import strawberry

user_list = [
    {"id": 1, "name": "Alice", "favorite": "music", "follow_ids": [2, 3, 4]},
    {"id": 2, "name": "Bob", "favorite": "reading", "follow_ids": []},
    {"id": 3, "name": "Charlie", "favorite": "traveling", "follow_ids": [2, 4]},
    {"id": 4, "name": "David", "favorite": "cooking", "follow_ids": [1]},
    {"id": 5, "name": "Eva", "favorite": "movies", "follow_ids": [1, 3]},
    {"id": 6, "name": "Frank", "favorite": "gaming", "follow_ids": []},
]

@strawberry.type
class User:
    id: int
    name: str
    favorite: str

    @staticmethod
    def get_user(user_id: int):
        user = next(user for user in user_list if user["id"] == user_id)
        return User(
            id=user["id"],
            name=user["name"],
            favorite=user["favorite"],
        )

@strawberry.type
class Query:
    @strawberry.field
    def get_user(self, user_id: int) -> User:
        return User.get_user(user_id)

schema = strawberry.Schema(query=Query)

user_idをもとにUserを取得するようになっています。
今までと異なる点として、フィールドをメソッドで定義しています。

@strawberry.type
class Query:
    @strawberry.field
    def get_user(self, user_id: int) -> User:
        # ここに処理が書ける
        return User.get_user(user_id)

    # 今までの記法
    get_user: User = strawberry.field(resolver=User.get_user(user_id))

この記法ではメソッド自体がリゾルバーになっているような形で、処理を差し込みやすいです。

自身のフィールドを使ったフィールド

Userのfavoriteからシェア用の文言を返すフィールドを作ってみたいと思います。

@strawberry.type
class UserWithShere:
    id: int
    name: str
    favorite: str

    @strawberry.field
    def share(self) -> str:
        return f"{self.name} likes {self.favorite}, follow me!"

    @staticmethod
    def get_user(user_id: int):
        user = next(user for user in user_list if user["id"] == user_id)
        return UserWithShere(
            id=user["id"],
            name=user["name"],
            favorite=user["favorite"],
        )

@strawberry.type
class Query:
    @strawberry.field
    def get_user_with_shere(self, user_id: int) -> UserWithShere:
        return UserWithShere.get_user(user_id)

selfを使うことで自身のフィールドを使用することができます。

クエリを実行してみましょう。

query {
  getUserWithShere(userId: 1){
    id
    name
    share
  }
}

サーバーからのレスポンス

{
  "data": {
    "getUserWithShere": {
      "id": 1,
      "name": "Alice",
      "share": "Alice likes music, follow me!"
    }
  }
}

自身を返すフィールド

https://strawberry.rocks/docs/guides/accessing-parent-data

follow_idsを使ってフォロワーを返すようなフィールドを作ってみます。
また、そのフォロワーのリストもユーザーのタイプを持たせたいと思います。

@strawberry.type
class UserWithFollows:
    id: int
    name: str
    favorite: str
    follow_ids: list[int]

    @strawberry.field
    def follows(self) -> list["UserWithFollows"]:
        print(type(self))
        return get_follows(self)

    @staticmethod
    def get_user(user_id: int):
        user = next(user for user in user_list if user["id"] == user_id)

        return UserWithFollows(
            id=user["id"],
            name=user["name"],
            favorite=user["favorite"],
            follow_ids=user["follow_ids"],
        )

@strawberry.type
class Query:
    @strawberry.field
    def get_user_with_follows(self, user_id: int) -> UserWithFollows:
        return UserWithFollows.get_user(user_id)

まず、基本的なフォロー関係を取得してみましょう。

query {
  getUserWithFollows(userId: 1){
    id
    name
    follows {
      id
      name
    }
  }
}

サーバーからのレスポンス
{
  "data": {
    "getUserWithFollows": {
      "id": 1,
      "name": "Alice",
      "follows": [
        {
          "id": 2,
          "name": "Bob"
        },
        {
          "id": 3,
          "name": "Charlie"
        },
        {
          "id": 4,
          "name": "David"
        }
      ]
    }
  }
}

次に、フォロー関係を再帰的に取得してみましょう。

query {
  getUserWithFollows(userId: 1){
    id
    name
    follows {
      id
      name
      follows {
        id
        name
      }
    }
  }
}

サーバーからのレスポンス
{
  "data": {
    "getUserWithFollows": {
      "id": 1,
      "name": "Alice",
      "follows": [
        {
          "id": 2,
          "name": "Bob",
          "follows": []
        },
        {
          "id": 3,
          "name": "Charlie",
          "follows": [
            {
              "id": 2,
              "name": "Bob"
            },
            {
              "id": 4,
              "name": "David"
            }
          ]
        },
        {
          "id": 4,
          "name": "David",
          "follows": [
            {
              "id": 1,
              "name": "Alice"
            }
          ]
        }
      ]
    }
  }
}

循環参照するフィールド

複雑なデータ関係を持つAPIを実装する際、循環参照の問題が発生することがあります。ここでは、ソーシャルメディアの投稿システムを例に、その解決方法を見ていきましょう。

まず、投稿データの基本構造を定義します。

# サンプルデータ:投稿リスト
post_list = [
    {
        "id": 1,
        "content": "What a beautiful day today!",
        "user_id": 1,
        "liked_by_ids": [2, 3],  # いいねしたユーザーのID
    },
    {
        "id": 2,
        "content": "Started reading a new book",
        "user_id": 2,
        "liked_by_ids": [1],
    },
    {
        "id": 3,
        "content": "Found a great restaurant",
        "user_id": 3,
        "liked_by_ids": [1, 2, 4],
    },
]

投稿(Post)には以下の要素が含まれます。

フィールド 説明
id int 投稿の一意識別子
content str 投稿内容
user User 投稿者への参照
liked_by User[] いいねしたユーザーのリスト

このような相互参照を含むモデルでは、以下のような循環参照が発生します。

以下のように、循環参照を考慮した実装を行います。

@strawberry.type
class Post:
    """投稿を表すGraphQLオブジェクトタイプ"""
    id: int
    content: str
    # 文字列で型を参照することで循環参照を回避
    user: "UserWithPosts" = strawberry.field(
        resolver=lambda self: UserWithPosts.get_user(self.user_id),
        description="投稿者の情報"
    )
    liked_by: list["UserWithPosts"] = strawberry.field(
        resolver=lambda self: [UserWithPosts.get_user(user_id) for user_id in self.liked_by_ids],
        description="いいねしたユーザーのリスト"
    )

    @strawberry.field(description="いいねの総数")
    def like_count(self) -> int:
        """いいねの数を返す計算フィールド"""
        return len(self.liked_by_ids)

    def __init__(self, **kwargs):
        """投稿オブジェクトの初期化

        Args:
            kwargs: 投稿データの辞書
                - id: 投稿ID
                - content: 投稿内容
                - user_id: 投稿者のID
                - liked_by_ids: いいねしたユーザーのIDリスト
        """
        self.id = kwargs["id"]
        self.content = kwargs["content"]
        self.user_id = kwargs["user_id"]
        self.liked_by_ids = kwargs["liked_by_ids"]

@strawberry.type
class UserWithPosts:
    """投稿情報を含むユーザーを表すGraphQLオブジェクトタイプ"""
    id: int
    name: str
    favorite: str

    @strawberry.field(description="ユーザーの投稿一覧")
    def posts(self) -> list[Post]:
        """ユーザーが作成した投稿を取得

        Returns:
            list[Post]: ユーザーの投稿リスト
        """
        return [Post(**post) for post in post_list if post["user_id"] == self.id]

    @strawberry.field(description="ユーザーがいいねした投稿一覧")
    def liked_posts(self) -> list[Post]:
        """ユーザーがいいねした投稿を取得

        Returns:
            list[Post]: いいねした投稿のリスト
        """
        return [Post(**post) for post in post_list if self.id in post["liked_by_ids"]]

    @staticmethod
    def get_user(user_id: int) -> "UserWithPosts":
        """ユーザーIDからユーザー情報を取得

        Args:
            user_id: 取得対象のユーザーID

        Returns:
            UserWithPosts: ユーザー情報
        """
        user = next(user for user in user_list if user["id"] == user_id)
        return UserWithPosts(
            id=user["id"],
            name=user["name"],
            favorite=user["favorite"],
        )

@strawberry.type
class Query:
    """ソーシャルメディアAPIのクエリルート"""

    @strawberry.field(
        description="ユーザーと投稿情報を取得",
    )
    def get_user_with_posts(self, user_id: int) -> UserWithPosts:
        """指定されたIDのユーザーと、その投稿情報を取得

        Args:
            user_id: 取得対象のユーザーID

        Returns:
            UserWithPosts: ユーザーと関連する投稿情報
        """
        return UserWithPosts.get_user(user_id)

# スキーマの定義
schema = strawberry.Schema(
    query=Query,
    description="ソーシャルメディアAPIのGraphQLスキーマ",
    types=[Post, UserWithPosts]  # 明示的に型を登録
)

ユーザーの投稿情報と関連データを取得する複雑なクエリを実行してみましょう。

query GetUserWithPosts {
  getUserWithPosts(userId: 1) {
    id                # ユーザーID
    name             # ユーザー名
    posts {          # ユーザーの投稿一覧
      id
      content
      likeCount      # いいねの数(計算フィールド)
      likedBy {      # いいねしたユーザー
        id
        name
        posts {      # いいねしたユーザーの投稿
          id
          content
        }
      }
    }
    likedPosts {     # ユーザーがいいねした投稿
      id
      content
      user {         # 投稿者の情報
        id
        name
      }
    }
  }
}

サーバーからのレスポンス
{
  "data": {
    "getUserWithPosts": {
      "id": 1,
      "name": "Alice",
      "posts": [
        {
          "id": 1,
          "content": "What a beautiful day today!",
          "likeCount": 2,
          "likedBy": [
            {
              "id": 2,
              "name": "Bob",
              "posts": [
                {
                  "id": 2,
                  "content": "Started reading a new book"
                }
              ]
            },
            {
              "id": 3,
              "name": "Charlie",
              "posts": [
                {
                  "id": 3,
                  "content": "Found a great restaurant"
                }
              ]
            }
          ]
        }
      ],
      "likedPosts": [
        {
          "id": 2,
          "content": "Started reading a new book",
          "user": {
            "id": 2,
            "name": "Bob"
          }
        },
        {
          "id": 3,
          "content": "Found a great restaurant",
          "user": {
            "id": 3,
            "name": "Charlie"
          }
        }
      ]
    }
  }
}

実際にはリゾルバーで効率的にデータを取得するために、データローダーを使うなどの工夫が必要ですが、このような複雑なフィールドも取得することができます。

おわりに

この記事では、StrawberryによるGraphQL APIの実装について見てきました。
GraphQLのメリットや使い方が伝われば幸いです。

参考

https://graphql.org/learn/

https://strawberry.rocks/docs

Discussion