DRFチュートリアル要旨5

## APIのエンドポイントを作る

#### 何をやるのか

現在ユーザーモデルとスニペットでは主キーでリレーションされている。
そうなると例えばユーザーの情報をjsonで引っ張ってきた場合、そのユーザーに紐付くスニペットに関する情報も当然返ってくるのだが、要旨4までの場合で

```json

{
    "count": 1,
    "next": null,
    "previous": null,
    "results": [
        {
            "url": "http://127.0.0.1:8000/users/1/",
            "id": 1,
            "username": "kamille",
            "snippets": [
              1,
              2,
              3,
            ]
        }
    ]
}

```


と返ってくるところを

```json

{
    "count": 1,
    "next": null,
    "previous": null,
    "results": [
        {
            "url": "http://127.0.0.1:8000/users/1/",
            "id": 1,
            "username": "kamille",
            "snippets": [
                "http://127.0.0.1:8000/snippets/1/",
                "http://127.0.0.1:8000/snippets/2/",
                "http://127.0.0.1:8000/snippets/3/"
            ]
        }
    ]
}


```

と返ってくるようにしたいというのが今回の話である。
DRFでのエンティティ(データの塊あるいは情報の集合といったイメージ)同士のリレーションについて、これまでの主キーでの方法を含め以下のように多様に用意されているが、
今回は後者の例のようにハイパーリンクでのリレーションにすることで直接リレーション先のURIを叩けるようにしてみる。

＊ DRFでのエンティティのリレーションの方法

- 主キーの使用。
- エンティティ間のハイパーリンクの使用
- 関連するエンティティの一意の識別スラッグ・フィールドを使用する。
- 関連するエンティティの既定の文字列表現を使用する。
- 関連するエンティティを親表現の中に入れ子にする。
- その他のカスタム表現。


#### APIのエンドルートポイントを作る。

スニペットとユーザーのエンティティはそれぞれエンドポイント(ルートポイント)が設定されている(User List及びSnippet List)。
しかし、その2つを含めたAPI全体に対してのエンドポイントは設定されていないのでそれを作成する。

```python

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)
    })

```

関数ベースのビューを`views.py`に作成する。
各エンティティのエンドポイントのルートにあたるのでリクエストはGET指定のみでいいだろう。
returnされるURLは完全なものでありたい(http://から始まる形)ので`reverse()`を使う。
URLパターンは第一引数で`snippets/urls.py`で設定したname属性を指定する。

#### ハイライトされたスニペットのエンドポイントを作る

今回はスニペットをハイライトしているのでそれに対してもエンドポイントを作ってみる。
ただし、ハイライトに対してはJSONではなく、HTMLフォーマットで返るようにしたい。
DRFではテンプレートを使うか、HTMLを最初にレンダリングしておくかのどちらかで実現できる。
今回は後者を使う。

```python

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)


```

クラスベースで`views.py`に定義をする。

#### リレーションをハイパーリンクに変更する

シリアライザーをリファクタリングする。

```python

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']

```

まず、`ModelSerializer`の派生である`HyperlinkedModelSerializer`に継承先を変更する。
そしてハイライトはスニペットと、またユーザーとスニペットとのリレーションを定義し直す。
まず、新しくurlフィールドと`SnippetSerializer`にはhighlightフィールドも設定する。
そして、リレーションの方法を`HyperlinkedIdentityField`に変更する。
引数にはリレーション先のview_name(`snippets/urls.py`で設定したname属性を指定する)とハイライトのようにHTMLで返したいというようにフォーマットに指定があるならば、format属性を指定する。
urlフィールドには下記のようにそのエンティティ自身のURLが入る。


```json

{
    "count": 3,
    "next": null,
    "previous": null,
    "results": [
        {
            "url": "http://127.0.0.1:8000/snippets/1/",
            "id": 1,
            "highlight": "http://127.0.0.1:8000/snippets/1/highlight/",
            "owner": "kamille",
            "title": "",
            "code": "print(789)",
            "linenos": false,
            "language": "python",
            "style": "friendly"
        },

```

#### ルーティングを行う

最後に新しく作ったビューに対してルーティングの設定及び、これまでのルーティングに対してname属性をつける

```python


from django.urls import path
from rest_framework.urlpatterns import format_suffix_patterns
from snippets import views
from django.conf.urls import include

urlpatterns = format_suffix_patterns([
    path('', views.api_root),
    path('users/', views.UserList.as_view(), name='user-list'),
    path('users/<int:pk>/', views.UserDetail.as_view(), name='user-detail'),
    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('api-auth/', include('rest_framework.urls')),
])


```

このとき`urlpatterns`全体に対して`format_suffix_patterns`でラッピングを行う。
`format_suffix_patterns`は引数にformat属性が指定されていた場合、そのformatの値で自動的にURLを指定できるというもの(例えばJSONならhttps://~~~~.jsonと叩かなくても、JSON形式で画面に表示される)。
今回はハイライトにおいてhtmlでフォーマットを指定しているのでこれが適用され、highlightプロパティのリンクを叩くと以下のように表示される。

<details><summary>JSONデータ</summary><div>


</div></details>

```json

        {
            "url": "http://127.0.0.1:8000/snippets/3/",
            "id": 3,
            "highlight": "http://127.0.0.1:8000/snippets/3/highlight/",
            "owner": "kamille",
            "title": "test",
            "code": "print(10)",
            "linenos": true,
            "language": "abap",
            "style": "abap"
        }


```

![2020-08-18_14h48_24.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/428779/9662f59e-245d-7caa-f7f9-5e42597d4e7c.png)


#### ペジネーションの設定

`User List`や`Snippet List`などエンティティの一覧にあたるページはGETで一気にデータを取得するわけだが、indexにあたるページになるわけで当然データ量は増えていくことが想定される。
そうすると、一度に返すデータが膨大になりパフォーマンスに影響が出るので、1ページあたりのデータの量のを制限しようというのがペジネーションの設定である。
`settings.py`で以下のように設定する。

```python

REST_FRAMEWORK = {
    'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',
    'PAGE_SIZE': 10
}

```

デフォルトの設定がこれである。
`'PAGE_SIZE': 10`は1ページあたり10オブジェクトの表示までという意味で、つまるところ例えば`User List`であるならば1ページにつき、10人分のユーザーのエンティティまでしか表示できませんという設定になる。

## 参考

[DRF公式](https://www.django-rest-framework.org/tutorial/5-relationships-and-hyperlinked-apis/)