Django REST framework（DRF）公式チュートリアル⑤ Relationships & Hyperlinked APIs

# Tutorial 5: Relationships & Hyperlinked APIs

### はじめに
本記事は、Django REST framework の公式チュートリアル[Tutorial 5: Relationships & Hyperlinked APIs](https://www.django-rest-framework.org/tutorial/5-relationships-and-hyperlinked-apis/)の内容をもとに、和訳および補足解説を行ったものです。

基本的にはチュートリアルの翻訳を軸に構成しており、`✏️補足`の箇所では、各コードセクションの補足や内部処理の解説を備忘録的に記載しています。

他のチュートリアル記事もあわせて読みたい方は、以下のまとめ記事からご覧いただけます👇  
[DRF（Django REST framework）公式チュートリアルを日本語でまとめてみた【全6回】](https://qiita.com/yutty-kov/items/1850f3e865ec1a50a7bb)

それでは、本チュートリアルを始めましょう。
- - -

現時点では、API内のリレーションは主キーによって表現されています。
本チュートリアルのこのセクションでは、APIの一貫性と可読性を向上させるために、リレーションの表現を主キーからハイパーリンク形式に変更していきます。

### APIルート用のエンドポイントを作成する
現在のところ、`snippets` と `users` に対するエンドポイントは用意されていますが、API全体のエントリーポイントとなるような共通のルートエンドポイントは存在していません。
このエントリーポイントを作成するために、通常の関数ベースビューと、前に紹介した <font color="#d14">`@api_view`</font> デコレーター を使用します。

<font color="#d14">`snippets/views.py`</font> に、次のコードを追加してください：

```python:snippets/views.py
from rest_framework.decorators import api_view
from rest_framework.response import Response
from rest_framework.reverse import reverse


@api_view(['GET'])
def api_root(request, format=None):
    return Response({
        'users': reverse('user-list', request=request, format=format),
        'snippets': reverse('snippet-list', request=request, format=format)
    })
```

ここで注目すべき点が2つあります。

まず1つ目に、完全なURL（フルパス）を返すために、Django REST framework の <font color="#d14">`reverse`</font> 関数を使用しているという点です。

そして2つ目に、URLパターンは「名前付きルート」で識別されており、これらの名前は後ほど <font color="#d14">`snippets/urls.py`</font> の中で定義します。
<br>

## ✏️補足
>現在のところ、snippets と users に対するエンドポイントは用意されていますが、API全体のエントリーポイントとなるような共通のルートエンドポイントは存在していません。

こちらですが、例えば各データにアクセスするには、以下のように個別の URL を直接指定してアクセスする必要があります。

```
http://localhost:8000/snippets/ # snippetsへのアクセス
http://localhost:8000/users/ # usersへのアクセス
```

そのため、`http://localhost:8000/` にアクセスしたときに、以下のような ルートエンドポイント（APIのホーム）が表示されるように設定を加えていきます。

![F46D0020-22A4-4A93-A2A6-B98B29FE0363.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/593085/b9762e1b-3af0-46f3-8e7f-40784e837741.jpeg)

- - -

### ハイライト表示されたSnippetのためのエンドポイントを作成する

私たちのPastebin風 APIにまだ欠けているもう１つの明らかな機能は、コードのハイライト表示用エンドポイントです。

他の全てのAPIエンドポイントと異なり、ここではJSONではなく、HTML形式の表現を返すようにしたいと考えています。
Django REST frameworkには、HTMLをレンダリングするための2種類のHTMLレンダラーが用意されています。
1つはテンプレートを使ってHTMLを生成する方法、もう1つはすでにレンダリングされたHTMLをそのまま扱う方法です。
今回のエンドポイントでは、後者の「レンダリングされたHTMLをそのまま扱う方法」を使います。

もう1つ、このコードハイライト用ビューを作成する際に注意すべき点があります。
それは、これまで使っていた汎用ビューの中に、今回のような用途にぴったり合う具体的なビュークラスが存在しないということです。
というのも、オブジェクトを返すのではなく、オブジェクトのプロパティを返すためです。

そこで、汎用ビューを使う代わりに、オブジェクトインスタンスを表現するための基本クラスを使い、独自の <font color="#d14">`.get()`</font> メソッドを定義します。

次のコードを <font color="#d14">`snippets/views.py`</font> に追加します：

```python:snippets/views.py
from rest_framework import renderers

class SnippetHighlight(generics.GenericAPIView):
    queryset = Snippet.objects.all()
    renderer_classes = [renderers.StaticHTMLRenderer]

    def get(self, request, *args, **kwargs):
        snippet = self.get_object()
        return Response(snippet.highlighted)
```

いつもと同様に、作成したViewはURL設定に追加する必要があります。ここでは、<font color="#d14">`snippets/urls.py`</font> に新しく作成したAPIルートのURLパターンを追加します：

```python:snippets/urls.py
path('', views.api_root),
```

そして、Snippetのハイライト用のURLパターンも追加します。

```python:snippets/urls.py
path('snippets/<int:pk>/highlight/', views.SnippetHighlight.as_view()),
```

## ✏️補足
`GenericAPIView` クラスには、以下のような（`Response` オブジェクトとして返される）レスポンスとしてどの形式（JSONやHTMLなど）にするかを指定できる `renderer_classes` フィールドがあります。

```python:
class SnippetHighlight(generics.GenericAPIView):
    ...
    renderer_classes = [renderers.StaticHTMLRenderer]
```
今回は以下のような **HTMLに変換されたコード** を返したいため、HTML文字列を返す `StaticHTMLRenderer` を `renderer_classes` フィールドに指定します。

```
<div class="highlight"><pre><span></span><span class="k">print</span><span class="p">(</span><span class="s">"Hello"</span><span class="p">)</span></pre></div>
```

<br>

>それは、これまで使っていた汎用ビューの中に、今回のような用途にぴったり合う具体的なビュークラスが存在しないということです。
というのも、オブジェクトを返すのではなく、オブジェクトのプロパティを返すためです。

上記のようにありますが、[チュートリアルのパート３](⭐️)で使用した `RetrieveModelMixin` クラス等の **`GenericAPIView` クラス と mixin クラス** を合わせたビュークラスは `Snippet` オブジェクトを返す用途で使用されます。

```python:
# シンプルにSnippetオブジェクトを返すクラス
class SnippetDetail(mixins.RetrieveModelMixin,
                  generics.GenericAPIView)):
    ...
    def get(self, request, *args, **kwargs):
        # retrieve()メソッドで、特定のSnippetオブジェクトを返す
        return self.retrieve(request, *args, **kwargs)
```

今回返すのは `Snippet` オブジェクトの `highlighted` プロパティに格納された以下のような HTML文字列です。
そのため、Model全体を返す汎用ビュー（例えば RetrieveModelMixin）ではこの目的に合いません（`RetrieveModelMixin` は通常、オブジェクト全体をシリアライズして返すため）。
```
<div class="highlight"><pre><span></span><span class="k">print</span><span class="p">(</span><span class="s">"Hello"</span><span class="p">)</span></pre></div>
```

そのため、`GenericAPIView` クラスの `get()` メソッドを新たに定義し、`Response()` の引数に `snippet.highlighted` を指定することで、プロパティだけを返すカスタムレスポンスを実現しています。
```python:
# Snippetオブジェクトのプロパティ(highlightedフィールド)のみを返すクラス
class SnippetHighlight(generics.GenericAPIView):
    ...
    renderer_classes = [renderers.StaticHTMLRenderer]

    def get(self, request, *args, **kwargs):
        snippet = self.get_object()
        return Response(snippet.highlighted)
```

- - -

### API のハイパーリンク化
エンティティ間のリレーションを扱うことは、Web API 設計において特に難しい部分のひとつです。リレーションの表現方法には、いくつかの選択肢があります：

- 主キーを使う方法

- エンティティ間をハイパーリンクでつなぐ方法

- 関連するエンティティの一意なスラッグフィールドを使う方法

- 関連するエンティティのデフォルトの文字列表現を使う方法

- 関連するエンティティを親の表現の中にネストして埋め込む方法

- その他のカスタムな表現方法

Django REST framework は、これら全てのスタイルに対応しており、順方向・逆方向のリレーションや、汎用外部キーのようなカスタムマネージャに対しても適用できます。


今回は、エンティティ同士の関係をハイパーリンク形式で表現したいと思います。そのために、既存の <font color="#d14">`ModelSerializer`</font> を継承するのではなく、<font color="#d14">`HyperlinkedModelSerializer`</font> を継承するようにSerializerを修正します。

<font color="#d14">`HyperlinkedModelSerializer`</font> は、<font color="#d14">`ModelSerializer`</font> と比べて次のような違いがあります：

- デフォルトでは <font color="#d14">`id`</font> フィールドを含みません

- <font color="#d14">`url`</font> フィールドが追加され、<font color="#d14">`HyperlinkedIdentityField`</font> を使います

- リレーションシップには <font color="#d14">`PrimaryKeyRelatedField`</font> の代わりに <font color="#d14">`HyperlinkedRelatedField`</font> を使います

既存のシリアライザーは簡単にハイパーリンク形式に書き換えることができます。<font color="#d14">`snippets/serializers.py`</font> に以下を追加します：

```python:snippets/serializers.py
class SnippetSerializer(serializers.HyperlinkedModelSerializer):
    owner = serializers.ReadOnlyField(source='owner.username')
    highlight = serializers.HyperlinkedIdentityField(view_name='snippet-highlight', format='html')

    class Meta:
        model = Snippet
        fields = ['url', 'id', 'highlight', 'owner',
                  'title', 'code', 'linenos', 'language', 'style']


class UserSerializer(serializers.HyperlinkedModelSerializer):
    snippets = serializers.HyperlinkedRelatedField(many=True, view_name='snippet-detail', read_only=True)

    class Meta:
        model = User
        fields = ['url', 'id', 'username', 'snippets']
```

ここで、新しく <font color="#d14">`highlight`</font> フィールド を追加している点にも注目してください。このフィールドは <font color="#d14">`url`</font> フィールドと同じ型ですが、<font color="#d14">`snippet-detail`</font> の URL パターンではなく、<font color="#d14">`snippet-highlight`</font> の URL パターンを指す点が異なります。

また、<font color="#d14">`'.json'`</font> のような フォーマット付きの URL を使用しているため、<font color="#d14">`highlight`</font> フィールドで返されるハイパーリンクも <font color="#d14">`'.html'`</font> のフォーマットを使うように指定する必要があります。
<br>

## ✏️補足
`ModelSerializer` クラスの代わりに継承している `HyperlinkedModelSerializer` クラスは、
オブジェクト間の関係を **主キーではなくURL** で表現する場合に使用するクラスです。

その中で使われている `HyperlinkedIdentityField` は、
「対象オブジェクト自身へのURL」や「関連するビューへのURL」を生成するための専用フィールドです。

以下は `HyperlinkedIdentityField` の主な引数の意味です：
- `view_name`引数: URL逆引きで使うView名(`urls.py` で定義された名前)
- `format`引数: レスポンスのフォーマット(通常は json、ここでは html を指定)

```python:
class SnippetSerializer(serializers.HyperlinkedModelSerializer):
    ...
    highlight = serializers.HyperlinkedIdentityField(
        view_name='snippet-highlight',
        format='html'
    )
```
<br>

この設定により、下記のように `highlight` フィールドに
対応するView(この場合はコードのハイライト表示) への URL が表示されます：
![98F97534-BCA1-41F2-A1CC-489BC78FDDED.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/593085/e626ba2d-0f7b-4329-b151-168350cc4d96.jpeg)

- - -

### URLパターンに名前が付いていることを確認する
ハイパーリンク形式のAPIを使用するには、URLパターンに名前を付けておく必要があります。どのURLパターンに名前が必要かを確認してみましょう。

 - APIのルートは、<font color="#d14">`'user-list'`</font> と <font color="#d14">`'snippet-list'`</font> を参照する。

 - SnippetSerializer には、<font color="#d14">`'snippet-highlight'`</font> を参照するフィールドがある。

 - UserSerializer には、<font color="#d14">`'snippet-detail'`</font> を参照するフィールドがある。

 - SnippetSerializer と UserSerializer の <font color="#d14">`'url'`</font> フィールドは、デフォルトで <font color="#d14">`'{モデル名}-detail'`</font>(この場合は <font color="#d14">`'snippet-detail'`</font> や <font color="#d14">`'user-detail'`</font>) を参照する。

これらすべての名前をURLconfに追加したあとの、最終的な <font color="#d14">`snippets/urls.py`</font> は次のようになります：

```python:snippets/urls.py
from django.urls import path
from rest_framework.urlpatterns import format_suffix_patterns
from snippets import views

# API エンドポイント
urlpatterns = format_suffix_patterns([
    path('', views.api_root),
    path('snippets/',
        views.SnippetList.as_view(),
        name='snippet-list'),
    path('snippets/<int:pk>/',
        views.SnippetDetail.as_view(),
        name='snippet-detail'),
    path('snippets/<int:pk>/highlight/',
        views.SnippetHighlight.as_view(),
        name='snippet-highlight'),
    path('users/',
        views.UserList.as_view(),
        name='user-list'),
    path('users/<int:pk>/',
        views.UserDetail.as_view(),
        name='user-detail')
])
```
<br>

### ページネーションの追加
ユーザーやコードスニペットのリストビューは、多くのインスタンスを返す可能性があります。
そのため、結果をページネーションして、APIクライアントが個別のページを順に取得できるようにするのが望ましいです。

デフォルトのリスト表示スタイルをページネーション付きに変更するには、<font color="#d14">`tutorial/settings.py`</font> ファイルを少し修正します。

以下の設定を追加してください：
```python:tutorial/settings.py
REST_FRAMEWORK = {
    'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',
    'PAGE_SIZE': 10
}
```

REST framework の設定は全て <font color="#d14">`REST_FRAMEWORK`</font> という 1つの辞書型の設定にまとめられている 点に注意してください。
これにより、プロジェクト内の他の設定と区別しやすくなります。

また、必要であればページネーションのスタイルをカスタマイズすることも可能ですが、ここではデフォルトの設定を使用することにします。
<br>

### ブラウザでAPIを操作する
ブラウザを開いてブラウズ可能なAPIにアクセスすると、リンクをたどるだけでAPIを操作できるようになっていることが分かります。

また、スニペットの各インスタンスには「highlight」リンク も表示されており、これをクリックすると ハイライトされたコードのHTML表示を見ることができます。

[チュートリアルのパート6](https://qiita.com/yutty-kov/items/6109161a308eb645467e)では、ViewSet と Routerを使って、API構築に必要なコード量を減らす方法を学びます。