Tutorial 5: Relationships & Hyperlinked APIs
はじめに
本記事は、Django REST framework の公式チュートリアルTutorial 5: Relationships & Hyperlinked APIsの内容をもとに、和訳および補足解説を行ったものです。
基本的にはチュートリアルの翻訳を軸に構成しており、✏️補足の箇所では、各コードセクションの補足や内部処理の解説を備忘録的に記載しています。
他のチュートリアル記事もあわせて読みたい方は、以下のまとめ記事からご覧いただけます👇
DRF(Django REST framework)公式チュートリアルを日本語でまとめてみた【全6回】
それでは、本チュートリアルを始めましょう。
現時点では、API内のリレーションは主キーによって表現されています。
本チュートリアルのこのセクションでは、APIの一貫性と可読性を向上させるために、リレーションの表現を主キーからハイパーリンク形式に変更していきます。
APIルート用のエンドポイントを作成する
現在のところ、snippets と users に対するエンドポイントは用意されていますが、API全体のエントリーポイントとなるような共通のルートエンドポイントは存在していません。
このエントリーポイントを作成するために、通常の関数ベースビューと、前に紹介した @api_view デコレーター を使用します。
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 の reverse 関数を使用しているという点です。
そして2つ目に、URLパターンは「名前付きルート」で識別されており、これらの名前は後ほど snippets/urls.py の中で定義します。
✏️補足
現在のところ、snippets と users に対するエンドポイントは用意されていますが、API全体のエントリーポイントとなるような共通のルートエンドポイントは存在していません。
こちらですが、例えば各データにアクセスするには、以下のように個別の URL を直接指定してアクセスする必要があります。
http://localhost:8000/snippets/ # snippetsへのアクセス
http://localhost:8000/users/ # usersへのアクセス
そのため、http://localhost:8000/ にアクセスしたときに、以下のような ルートエンドポイント(APIのホーム)が表示されるように設定を加えていきます。
ハイライト表示されたSnippetのためのエンドポイントを作成する
私たちのPastebin風 APIにまだ欠けているもう1つの明らかな機能は、コードのハイライト表示用エンドポイントです。
他の全てのAPIエンドポイントと異なり、ここではJSONではなく、HTML形式の表現を返すようにしたいと考えています。
Django REST frameworkには、HTMLをレンダリングするための2種類のHTMLレンダラーが用意されています。
1つはテンプレートを使ってHTMLを生成する方法、もう1つはすでにレンダリングされたHTMLをそのまま扱う方法です。
今回のエンドポイントでは、後者の「レンダリングされたHTMLをそのまま扱う方法」を使います。
もう1つ、このコードハイライト用ビューを作成する際に注意すべき点があります。
それは、これまで使っていた汎用ビューの中に、今回のような用途にぴったり合う具体的なビュークラスが存在しないということです。
というのも、オブジェクトを返すのではなく、オブジェクトのプロパティを返すためです。
そこで、汎用ビューを使う代わりに、オブジェクトインスタンスを表現するための基本クラスを使い、独自の .get() メソッドを定義します。
次のコードを 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設定に追加する必要があります。ここでは、snippets/urls.py に新しく作成したAPIルートのURLパターンを追加します:
path('', views.api_root),
そして、Snippetのハイライト用のURLパターンも追加します。
path('snippets/<int:pk>/highlight/', views.SnippetHighlight.as_view()),
✏️補足
GenericAPIView クラスには、以下のような(Response オブジェクトとして返される)レスポンスとしてどの形式(JSONやHTMLなど)にするかを指定できる renderer_classes フィールドがあります。
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>
それは、これまで使っていた汎用ビューの中に、今回のような用途にぴったり合う具体的なビュークラスが存在しないということです。
というのも、オブジェクトを返すのではなく、オブジェクトのプロパティを返すためです。
上記のようにありますが、チュートリアルのパート3で使用した RetrieveModelMixin クラス等の GenericAPIView クラス と mixin クラス を合わせたビュークラスは Snippet オブジェクトを返す用途で使用されます。
# シンプルに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 を指定することで、プロパティだけを返すカスタムレスポンスを実現しています。
# 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 は、これら全てのスタイルに対応しており、順方向・逆方向のリレーションや、汎用外部キーのようなカスタムマネージャに対しても適用できます。
今回は、エンティティ同士の関係をハイパーリンク形式で表現したいと思います。そのために、既存の ModelSerializer を継承するのではなく、HyperlinkedModelSerializer を継承するようにSerializerを修正します。
HyperlinkedModelSerializer は、ModelSerializer と比べて次のような違いがあります:
-
デフォルトでは
idフィールドを含みません -
urlフィールドが追加され、HyperlinkedIdentityFieldを使います -
リレーションシップには
PrimaryKeyRelatedFieldの代わりにHyperlinkedRelatedFieldを使います
既存のシリアライザーは簡単にハイパーリンク形式に書き換えることができます。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']
ここで、新しく highlight フィールド を追加している点にも注目してください。このフィールドは url フィールドと同じ型ですが、snippet-detail の URL パターンではなく、snippet-highlight の URL パターンを指す点が異なります。
また、'.json' のような フォーマット付きの URL を使用しているため、highlight フィールドで返されるハイパーリンクも '.html' のフォーマットを使うように指定する必要があります。
✏️補足
ModelSerializer クラスの代わりに継承している HyperlinkedModelSerializer クラスは、
オブジェクト間の関係を 主キーではなくURL で表現する場合に使用するクラスです。
その中で使われている HyperlinkedIdentityField は、
「対象オブジェクト自身へのURL」や「関連するビューへのURL」を生成するための専用フィールドです。
以下は HyperlinkedIdentityField の主な引数の意味です:
-
view_name引数: URL逆引きで使うView名(urls.pyで定義された名前) -
format引数: レスポンスのフォーマット(通常は json、ここでは html を指定)
class SnippetSerializer(serializers.HyperlinkedModelSerializer):
...
highlight = serializers.HyperlinkedIdentityField(
view_name='snippet-highlight',
format='html'
)
この設定により、下記のように highlight フィールドに
対応するView(この場合はコードのハイライト表示) への URL が表示されます:

URLパターンに名前が付いていることを確認する
ハイパーリンク形式のAPIを使用するには、URLパターンに名前を付けておく必要があります。どのURLパターンに名前が必要かを確認してみましょう。
-
APIのルートは、
'user-list'と'snippet-list'を参照する。 -
SnippetSerializer には、
'snippet-highlight'を参照するフィールドがある。 -
UserSerializer には、
'snippet-detail'を参照するフィールドがある。 -
SnippetSerializer と UserSerializer の
'url'フィールドは、デフォルトで'{モデル名}-detail'(この場合は'snippet-detail'や'user-detail') を参照する。
これらすべての名前をURLconfに追加したあとの、最終的な 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')
])
ページネーションの追加
ユーザーやコードスニペットのリストビューは、多くのインスタンスを返す可能性があります。
そのため、結果をページネーションして、APIクライアントが個別のページを順に取得できるようにするのが望ましいです。
デフォルトのリスト表示スタイルをページネーション付きに変更するには、tutorial/settings.py ファイルを少し修正します。
以下の設定を追加してください:
REST_FRAMEWORK = {
'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',
'PAGE_SIZE': 10
}
REST framework の設定は全て REST_FRAMEWORK という 1つの辞書型の設定にまとめられている 点に注意してください。
これにより、プロジェクト内の他の設定と区別しやすくなります。
また、必要であればページネーションのスタイルをカスタマイズすることも可能ですが、ここではデフォルトの設定を使用することにします。
ブラウザでAPIを操作する
ブラウザを開いてブラウズ可能なAPIにアクセスすると、リンクをたどるだけでAPIを操作できるようになっていることが分かります。
また、スニペットの各インスタンスには「highlight」リンク も表示されており、これをクリックすると ハイライトされたコードのHTML表示を見ることができます。
チュートリアルのパート6では、ViewSet と Routerを使って、API構築に必要なコード量を減らす方法を学びます。
