Django で GraphQL 実装してみた Query編

## Django で GraphQL 実装してみた
- [環境構築編](https://qiita.com/dbxkazuxdb/items/77a9c6d809a1c706e2db)
- Query編 ← この記事  
- [Mutation編](https://qiita.com/dbxkazuxdb/items/0d8e79cdba10349b47ec)
- [リファクタ編](https://qiita.com/dbxkazuxdb/items/b356912e1968fa8d02bb)


## 実装

基本的に、graphene の[チュートリアル](https://docs.graphene-python.org/projects/django/en/latest/tutorial-plain/)をなぞっていく.

### モデルの作成

app/models.py

```python
from django.db import models


class Category(models.Model):
    name = models.CharField(max_length=100)

    def __str__(self):
        return self.name


class Ingredient(models.Model):
    name = models.CharField(max_length=100)
    notes = models.TextField()
    category = models.ForeignKey(Category, related_name='ingredients')

    def __str__(self):
        return self.name
```

### マイグレート

```bash
docker compose exec app-back python manage.py makemigrations
docker compose exec app-back python manage.py migrate
```

### テストデータ生成

app/fixtures/ingredients.json  
[ここからダウンロード](https://raw.githubusercontent.com/graphql-python/graphene-django/master/examples/cookbook/cookbook/ingredients/fixtures/ingredients.json)

```bash
docker compose exec app-back python manage.py loaddata ingredients
```

### スキーマ作成

config/scheme.py

```python
import graphene
import app.schema


class Query(app.schema.Query, graphene.ObjectType):
    pass


schema = graphene.Schema(query=Query)
```

app/schema.py

```python
from graphene import relay
from graphene_django import DjangoObjectType
from graphene_django.filter import DjangoFilterConnectionField

from .models import Category, Ingredient


class CategoryNode(DjangoObjectType):
    class Meta:
        model = Category
        fields = ("name", "ingredients")
        filter_fields = ["name", "ingredients"]
        interfaces = (relay.Node,)


class IngredientNode(DjangoObjectType):
    class Meta:
        model = Ingredient
        interfaces = (relay.Node,)
        filter_fields = {
            "name": ["exact", "icontains", "istartswith"],
            "notes": ["exact", "icontains"],
            "category": ["exact"],
            "category__name": ["exact"],
        }


class Query:
    category = relay.Node.Field(CategoryNode)
    categories = DjangoFilterConnectionField(CategoryNode)

    ingredient = relay.Node.Field(IngredientNode)
    ingredients = DjangoFilterConnectionField(IngredientNode)
```

以下のようにすることで、クエリの詳細設定ができる

```diff_python
...
            "category__name": ["exact"],
            "category__is_active": ["exact"],
        }


+ class IngredientConnection(relay.Connection):
+     class Meta:
+         node = IngredientNode

class Query:
    category = relay.Node.Field(CategoryNode)
    categories = DjangoFilterConnectionField(CategoryNode)

    ingredient = relay.Node.Field(IngredientNode)
-   ingredients = DjangoFilterConnectionField(IngredientNode)
+   ingredients = relay.ConnectionField(IngredientNode)
+
+   def resolve_ingredients(root, info, **kwargs):
+       return Question.objects.all()
```

### 設定を追加する

config/setting.py

```python

INSTALLED_APPS = [
    ...
    "graphene_django", # 追加
    "app",   # 追加
]

GRAPHENE = {"SCHEMA": "config.schema.schema"} # 追加


MIDDLEWARE = [
    "django.middleware.security.SecurityMiddleware",
    "whitenoise.middleware.WhiteNoiseMiddleware", # 追加
    ...
]
```

config/urls.py

```python

from django.contrib import admin
from django.urls import path

from graphene_django.views import GraphQLView # 追加
from config.schema import schema # 追加

urlpatterns = [
    path("admin/", admin.site.urls),
    # 以下追加
    path(
        "graphql/",
        GraphQLView.as_view(
            graphiql=True,
            schema=schema,
        ),
    ),
]
```

### 実行してみる

http://localhost/graphql にアクセス

左上のペインにクエリを入力する

```graphql
query {
  ingredients {
    edges {
      node {
        id
        name
      }
    }
  }
}
```

再生ボタンを押下することで、右側ペインに結果が表示される

```json
{
  "data": {
    "ingredients": {
      "edges": [
        {
          "node": {
            "id": "SW5ncmVkaWVudE5vZGU6MQ==",
            "name": "Eggs"
          }
        },
        {
          "node": {
            "id": "SW5ncmVkaWVudE5vZGU6Mg==",
            "name": "Milk"
          }
        },
        {
          "node": {
            "id": "SW5ncmVkaWVudE5vZGU6Mw==",
            "name": "Beef"
          }
        },
        {
          "node": {
            "id": "SW5ncmVkaWVudE5vZGU6NA==",
            "name": "Chicken"
          }
        }
      ]
    }
  }
}
```

#### 絞り込み

app/schema.py の filter_fileds で設定できる

```python
class IngredientNode(DjangoObjectType):
    class Meta:
        model = Ingredient
        interfaces = (relay.Node,)
        filter_fields = {
            "name": ["exact", "icontains", "istartswith"],
            "notes": ["exact", "icontains"],
            "category": ["exact"],
            "category__name": ["exact"],
        }
```

| 設定値      | 動作             |
| ----------- | ---------------- |
| exact       | 完全一致         |
| icontains   | 指定値を含む     |
| istartswith | 指定値から始まる |

category_name 完全一致の場合  
リレーション先のカラム名は、パスカルケースで指定する

```graphql
query {
  ingredients(category_Name: "Dairy") {
    edges {
      node {
        id
        name
      }
    }
  }
}
```

```json
{
  "data": {
    "ingredients": {
      "edges": [
        {
          "node": {
            "id": "SW5ncmVkaWVudE5vZGU6MQ==",
            "name": "Eggs"
          }
        },
        {
          "node": {
            "id": "SW5ncmVkaWVudE5vZGU6Mg==",
            "name": "Milk"
          }
        }
      ]
    }
  }
}
```

#### ページネーション

| 条件                | 動作                               |
| ------------------- | ---------------------------------- |
| first: n            | n 行取得する                       |
| offset: n           | n 行移行を取得する                 |
| after: id or cursor | 指定の id や cursor 以降を取得する |

https://graphql.org/learn/pagination/

```
query {
  ingredients(first:2) {
    pageInfo {
      startCursor
      endCursor
    }
    edges {
      cursor
      node {
        id
        name
      }
    }
  }
}
```

#### 発見.GraphQL のいいところ

同時にリクエストすることが可能  
:::note info
category(id: "\d")は、query categories を実行して表示された id を指定すること
:::

```
query {
  categories{
    edges {
      node {
        id
        name
      }
    }
  }
  category(id:"\d") {
    name
  }
  ingredients(category_IsActive: false) {
    pageInfo {
      startCursor
      endCursor
    }
    edges {
      cursor
      node {
        id
        name
      }
    }
  }
}
```

## 所感
セキュリティなどは何もしていないが「だいぶいいぞ」って感じがする.
なんといっても、必要なデータのみが取れる, 複数のqueryを同時に実行できるのが良い.
grapheneはquerysetをよしなにやってくれるため、rest frameworkより簡単な印象を受けた.

次は、[Mutation編](https://qiita.com/dbxkazuxdb/items/0d8e79cdba10349b47ec)