アプリ開発ナレッジ

アプリ開発のナレッジを掲載します

マッチングアプリを個人開発する~Djangoバックエンドその3 REST API作成~

この記事はマッチングアプリを自作してみようと試みている記事です
バックエンドを実装しています

前回までの記事はこちらです
shinseidaiki.hatenablog.com


前回はモデル作成まで終わりました
今回はバックエンドの機能としてREST APIの機能を実装していきます

目次

Serializersの実装

SerializerとはModelとViewをつなぐインターフェースです
Webアプリケーションは究極的にはデータの作成・参照・更新・削除(通称CRUD)をするためにコードをごちゃごちゃといじっていくものなのですが、SerializerはModelを基にCRUDを自動的に実装してくれるものになっています
一昔前はModelを作成した後にまずはデータ作成機能を作り、参照機能を作り、更新機能を作り、削除機能を作り、といったように手作業で一つ一つ実装していました
しかしながらこのようなCRUD機能に関してはModelが定まれば半機械的に実装できるもののためDjangoでは実際に自動化してくれる機能があり、それがSerializerとなっています

ちなみにDjangoアーキテクチャーの役割はまだいまだに自分も理解しきれていないのですが、Webアプリの設計思想であるMVCモデル的に考えるとDjangoのViewがControllerに対応していて、SerializerがServiceに対応しているイメージでしょうか?
ちなみにDjangoはMVTモデルと言っているのでそう単純には比較できないでしょうがこのくらいの浅い知識でもアプリは実装できるのでいい時代ですね
それではアプリの方のフォルダ(basicapi/)にserializers.pyファイルを作成します

ユーザーシリアライザーの作成

まずはユーザーのCRUDをサポートするシリアライザーを作成します
そのためにModelSerializerを継承してシリアライザーを作成します
ModelSerializerを継承すると思考停止でCRUDをサポートしてくれるので、serializers.pyの実装は基本的にはModelSerializerにおんぶに抱っこされる形になろうかと思います

serializers.py

from rest_framework import serializers
from django.contrib.auth import get_user_model

class UserSerializer(serializers.ModelSerializer):

    class Meta:
        model = get_user_model()
        fields = ('email', 'password', 'username', 'id')
        extra_kwargs = {'password': {'write_only': True, 'min_length': 8}}

    def create(self, validated_data):
        user = get_user_model().objects.create_user(**validated_data)
        return user

    def update(self, use_instance, validated_data):
        for attr, value in validated_data.items():
            if attr == 'password':
                use_instance.set_password(value)
            else:
                setattr(use_instance, attr, value)
        use_instance.save()
        return use_instance

def create(self, validated_data) はModelSerializerにもともと備わっているCreateメソッドのオーバーライドをしたものになっています
基本的にはModelSerializerは思考停止でいいのですが、ユーザーに関しては思考停止ではいけません
というのもパスワードを取り扱うためで、ModelSerializerをそのまま使ってしまうと生のパスワードを保存してしまうので、パスワードをハッシュ化する必要があります
そこでユーザーモデルを作成したときに実装したUserManagerのcreate_user()を利用します
create_user()メソッド内にはset_password()のメソッドがあり、これがパスワードの値をハッシュ化しています
validated_dataはユーザー作成に必要なemailやpassword、usernameなどのデータが入っている想定です

fieldsはCRUD機能を自動適用させたいフィールドを指定します
extra_kwargsは指定したフィールドに制約条件を課すことができて上記ではpasswordの文字数は8文字以上という制約条件をつけています

update()は更新処理メソッドのオーバーライドでこれもユーザー作成時同様パスワード変更時にハッシュ化を行う必要があるためにこのような実装を行っています
extra_kwargsでパスワードを書き込み専用にしているのでREST APIを使用しているユーザーがパスワードを取得することはないので二重ハッシュ化によるパスワード破壊は起こらないはずなので大丈夫だとは思うが、ユーザー情報の更新処理は差分更新(PATCHメソッド)で呼び出すこととします
ユーザー情報でパスワード更新とメールアドレスユーザー名の更新画面は画面をそれぞれ分けて利用する設計にします

ProfileSerializerの作成

次にプロフィールのシリアライザーを作成する
ユーザークラス以外のモデルに関しては基本的にはMetaクラスの属性を編集することで実装できる
fieldの作成日時と更新日時はミリ秒以下は不要のためフォーマットしてデータを格納します
作成日時は編集する必要がないので読み取り専用にしておきます
また特に外部キーに使用している user も変更されると一大事となるのでこちらも読み取り専用にしておきます

serializers.py

class ProfileSerializer(serializers.ModelSerializer):

    created_at = serializers.DateTimeField(format='%Y-%m-%d %H:%M:%S', read_only=True)
    updated_at = serializers.DateTimeField(format='%Y-%m-%d %H:%M:%S', read_only=True)

    class Meta:
        model = Profile
        fields = (
            'user', 'is_special', 'is_kyc', 'top_image', 'nickname', 'created_at', 'updated_at',
            'age', 'sex', 'height', 'location', 'work', 'revenue', 'graduation',
            'hobby', 'passion', 'tweet', 'introduction',
            'send_favorite', 'receive_favorite', 'stock_favorite'
        )
        extra_kwargs = {'user': {'read_only': True}}

マッチングシリアライザーの作成

同様にマッチングシリアライザーを作成します

serializers.py

class MatchingSerializer(serializers.ModelSerializer):
    
    created_at = serializers.DateTimeField(format='%Y-%m-%d %H:%M:%S', read_only=True)

    class Meta:
        model = Matching
        fields = ('id', 'approaching', 'approached', 'approved', 'created_at')
        extra_kwargs = {'approaching': {'read_only': True}}

ダイレクトメッセージシリアライザーの作成

ダイレクトメッセージはマッチング中のユーザーのみがやりとりできるという制約があるので受け手のユーザーをあらかじめマッチングしたユーザーのみにフィルタリングしておきます
フィルタリングしておくことで送り手はマッチングしたユーザーのみを受け手に指定できることになるのでマッチングしたユーザーにのみダイレクトメッセージを送れる機能要件を満たします

フィルタリングの機能の実装はややこしいのでコードを眺めているだけでは解読不能かと思いますので図を作成しました
適当な図ですが理解の助けになれば幸いです

マッチング中のユーザーのみをフィルタリングする

DMを送ることができるのはマッチングしているユーザーのみなので、マッチングしているユーザーのリストを手元に持っておきたいのでマッチング中のユーザーをフィルタリングして取得します
DMの送り手のユーザーのデータはログインユーザーであるので送り手のユーザーはrequest.userで取得ができます
ここでマッチングしているユーザーとは視点を変えれば送り手にいいねを送り返しているユーザーでもあるので、受け手視点の受け手側に送り手が含まれているマッチングモデルのデータをまずは取得してloversに格納します
loversには受け手が送り手(approaching)となっている視点のマッチングモデルのデータが含まれているので、そのapproachingのユーザーIDを使用してユーザーモデルをフィルタリングすると、マッチングしているユーザーのデータだけを取得することができます
上記の内容を実装した結果は以下の通りになります

serializers.py

from django.db.models import Q


class MatchingFilter(serializers.PrimaryKeyRelatedField):

    def get_queryset(self):
        request = self.context['request']
        lovers = Matching.objects.filter(Q(approached=request.user) & Q(approved=True))

        list_lover = []
        for lover in lovers:
            list_lover.append(lover.approaching.id)

        queryset = get_user_model().objects.filter(id__in=list_lover)
        return queryset

ちなみにMatchingFilterはPrimaryKeyRelatedFieldを継承していますがこれは、ModelSerializer を使用する場合に扱うフィールドが外部キーで別テーブルを参照している場合があるので、紐付いたレコードをどのように表示するかを指定するためのものです
基本的には今回のようにフィルタリングをするために使用されることが多そうです
ModelSerializer を使っていて外部キーのフィールドを扱っていてもフィルタリングなどの処理をする必要がなければPrimaryKeyRelatedFieldを特に意識せずともDjangoがよしなに全件取得を行ってくれるようです

DMシリアライザー実装

ダイレクトメッセージシリアライザーでは受け手receiverに格納されているデータは上記で作成したフィルタリングを使用してマッチングされているユーザーのみを扱うようにする
他は同じです

serializers.py

from .models import DirectMessage


class DirectMessageSerializer(serializers.ModelSerializer):

    created_at = serializers.DateTimeField(format='%Y-%m-%d %H:%M:%S', read_only=True)
    receiver = MatchingFilter()

    class Meta:
        model = DirectMessage
        fields = ('id', 'sender', 'receiver', 'message', 'created_at')
        extra_kwargs = {'sender': {'read_only': True}}


これでシリアライザーの実装は一通り完了しました
次はViewを実装していきましょう

Viewsの実装

リアライザーの次はViewを実装していきましょう
Viewの大きな役割としてはURIのエンドポイントごとにどういった処理を実装させるかを記述します
リアライザーで処理のCRUDのインターフェースは作成されているので、Viewで記述する処理内容としてはCRUDのうち何を使用するのかとパーミッションを制御するのが主な実装内容となります
Djangoではビューの実装方法として関数ベースビューとクラスベースビューの二種類がありますが、本アプリではクラスベースビューを利用します
Djangoを利用する際はクラスベースビューを使用したほうが楽で見通しが良いです
処理はブラックボックス化しますが、非本質な処理を徹底的に排除しようという設計思想のDjangoフレームワークを利用する限りはブラックボックスは正義です
関数ベースビューは何をやっているかの処理が分かりやすくなりますがコード量が増えたり冗長になりがちなため特段理由がない限りはクラスベースビューを使用すればいいと思います
ブラックボックスを避けたい場合はSpringBootなどの別のフレームワークを採用するなど技術スタックの選定から検討をした方がいいです

それではViewの実装をしていきましょう
アプリ(basicapi)フォルダ下のviews.pyを編集していきます

クラスベースビューで使用するモジュール

クラスベースビューで使用するdjango-rest-frameworkのモジュールの機能を予習しておく
分からない場合はいったん流して後で眺めてもらったら良いです

generic viewsで使用可能なクラス
クラス 操作
CreateAPIView 登録(POST)
ListAPIView 一覧取得(GET)
RetrieveAPIView 取得(GET)
UpdateAPIView 更新(PUT、PATCH)
DestroyAPIView 削除(DELETE)

他のクラスもあるが例えば以下のように上記のクラスの組み合わせとなる
ListCreateAPIView (POST、GET) = ListAPIView + CreateAPIView

ドキュメント:Generic views - Django REST framework

viewsetsで使用可能なクラス
クラス 操作
ModelViewSet 登録取得更新削除(GETPOSTPUTPATCHDELETE)
ReadOnlyModel 一覧取得・取得(GET)

ModelViewSetはCRUDすべてのメソッドがサポートされているメソッドで、特別な理由がない限りは思考停止でModelViewSetを使用すればよいクラスとなっています
ReadOnlyModelはデータの取得と一覧取得のみができるクラスで読み取り専用にしたい場合に利用します
特別な理由がなければこちらのviewsetsのどちらかのクラスを継承してViewは構築していけばよさそうです

ドキュメント:Viewsets - Django REST framework

TIPS データ操作に関する"安全"や"危険"という用語の使い方について

先ほどから出しているCRUDという単語はデータ操作の4種類、登録・取得・更新・削除の操作のことを言っているのですが、このうち元々のデータを破壊する可能性があるデータ操作を"危険"と表現します
データ操作に対して元々のデータは変化しないことが保証されている場合は"安全"と表現されます
この中で元々のデータを破壊する可能性がある"危険"なデータ操作は「更新」と「削除」であり、"安全"なデータ操作は「登録」と「取得」です
REST APIパーミッション切り分けの場合にこの用語が使われたりするので知っておくとよいでしょう

RESTAPI ユーザー作成機能の実装

クラスベースビューを使用するためにはベースビュークラスを継承して利用します
基本的には思考停止でrest_framework.viewsets.ModelViewSetを継承したクラスを作成すればよいですが、ModelViewSetはCRUDすべてがサポートされているためユーザーのようにREST API経由で変更削除されたくないような場合にはCRUDの一部に特化したクラスを継承する方がいい場合があります
今回はREST APIではユーザー作成ができれば十分であるので汎用APIビューのCreateのみをサポートするgenerics.CreateAPIViewを継承したクラスを作成します
また今回settings.pyのREST_FRAMEWORK設定でREST APIはログインユーザー以外はデフォルトで使用禁止にしているので、ユーザー作成の処理だけは例外としてログイン不要で使用可能にする設定を付与します

クラスベースビューで実装したユーザー作成機能は以下のようなコードになります

views.py

from rest_framework.generics import CreateAPIView
from .serializers import UserSerializer
from rest_framework.permissions import AllowAny


class CreateUserView(CreateAPIView):
    serializer_class = UserSerializer
    permission_classes = (AllowAny,)

処理を2行という圧倒的な短さで記述できるのがDjangoの驚異的なところです

余談 Djangoテンプレートの話

ちなみに余談ですが、今回はフロントエンドをNext.jsで実装する予定のためREST API用のベースビューのクラスを rest_framework モジュールから継承していますが、実はNext.jsを使用せずにDjangoだけで画面も作成できるのですが、Djangoだけで画面まで作成する場合はdjango.views.genericのモジュールから各種ベースクラスのビューを同様に継承することで同様に実装することも可能です
この場合はserialiserも不要となったりしてものすごく簡単にアプリを実装することができます
この辺の実装手順はDjangoチュートリアルに載っています
Djangoだけでもアプリ開発は十分に作成できるんだよという話でした
下手なローコードツールを使うよりよっぽど便利で楽だと思います

RESTAPI ユーザー取得更新機能の実装

こちらも取得と更新だけに特化したいのでgenerics.RetrieveUpdateAPIViewを継承します
RetrieveUpdateAPIViewはリクエストにpkのURLのリクエストパラメータがあることが要求されることには注意が必要です
pkと言っているのは/users/{pk}/の{pk}の部分です
ユーザーの場合はUUIDがここに入ってきます

views.py

class UserView(RetrieveUpdateAPIView):
    queryset = User.objects.all()
    serializer_class = UserSerializer

    def get_queryset(self):
        return self.queryset.filter(id=self.request.user.id)

RESTAPI プロフィールCRUD機能の実装

プロフィールは登録・一覧取得・取得・更新・削除のすべての操作を行いたいためModelViewSetを継承します

使用ケースは以下を想定します

登録 ユーザー作成時
一覧取得 異性のプロフィール一覧を取得
取得 特定の異性のプロフィール詳細を取得
更新 自身のプロフィール情報を変更する
削除 拒否する

perform_create メソッドはデータ作成時に登録するデータを指定できるメソッドのことでProfileViewSetではログイン中のユーザーをProfileモデルの外部キーであるuserに登録します
get_querysetでは異性のユーザーをフィルタリングしています
ログインユーザーが男性の場合は女性、ログインユーザーが女性の場合は男性のプロフィール一覧をフィルタリングして取得します

views.py

from rest_framework.viewsets import ModelViewSet
from .models import Profile
from .serializers import ProfileSerializer
from rest_framework import status
from rest_framework.response import Response

class ProfileViewSet(ModelViewSet):
    queryset = Profile.objects.all()
    serializer_class = ProfileSerializer

    def get_queryset(self):
        if hasattr(self.request.user, 'profile'):
            sex = self.request.user.profile.sex
            # Profile.SEX[0][0] = 'male', Profile.SEX[1][0] = 'female'
            if sex == Profile.SEX[0][0]:
                reversed_sex = Profile.SEX[1][0]
            if sex == Profile.SEX[1][0]:
                reversed_sex = Profile.SEX[0][0]
            return self.queryset.filter(sex=reversed_sex)
        return self.queryset.filter(user=self.request.user)

    def perform_create(self, serializer):
        serializer.save(user=self.request.user)

    def destroy(self, request, *args, **kwargs):
        response = {'message': 'Delete is not allowed !'}
        return Response(response, status=status.HTTP_400_BAD_REQUEST)

    def update(self, request, *args, **kwargs):
        response = {'message': 'Update DM is not allowed'}
        return Response(response, status=status.HTTP_400_BAD_REQUEST)

    def partial_update(self, request, *args, **kwargs):
        response = {'message': 'Patch DM is not allowed'}
        return Response(response, status=status.HTTP_400_BAD_REQUEST)

プロフィール削除に関してはAPIを経由して削除できない設計にします
なお、本マッチングアプリでのユーザーの削除はユーザーからなんらかの連絡手段を介して削除要請があった場合に該当するユーザーのis_activeを管理画面からfalseに変更する運用とします

RESTAPI 自己プロフィール取得更新機能の実装

次に自分のプロフィールを参照したり更新したりするためのviewの機能を作成します
クラスベースビューで単独データをとる場合もまずは queryset に [Model].objects.all() すなわち全件取得のクエリを置いておいてその後 get_queryset(self) のメソッドでデータをフィルタリングする実装が一般的なようです

views.py

from rest_framework.generics import RetrieveUpdateAPIView


class MyProfileListView(RetrieveUpdateAPIView):
    queryset = Profile.objects.all()
    serializer_class = ProfileSerializer

    def get_queryset(self):
        return self.queryset.filter(user=self.request.user)

RESTAPI マッチング機能の実装

さてマッチングアプリで最も重要な機能であるマッチング機能を実装していきます
マッチングモデルのデータはログインユーザーがいいねを送ったデータとログインユーザーがいいねを受け取ったデータを取得します
またマッチングデータ作成時、すなわち、いいねを送る際はperform_createで送り手にログインユーザーを指定しておきます
またマッチングモデルには組み合わせ制約の条件を設定しているのでマッチングデータ作成時にバリデーションエラーが発生した場合に備えてtry exceptでエラーハンドリングを行います
また削除(DELETE)と差分更新(PATCH)は使用できないようにしておきます

views.py

from rest_framework.exceptions import ValidationError
from .models import Matching
from .serializers import MatchingSerializer
from django.db.models import Q


class MatchingViewSet(ModelViewSet):
    queryset = Matching.objects.all()
    serializer_class = MatchingSerializer

    def get_queryset(self):
        return self.queryset.filter(Q(approaching=self.request.user) | Q(approached=self.request.user))

    def perform_create(self, serializer):
        try:
            serializer.save(approaching=self.request.user)
        except ValidationError:
            raise ValidationError("User cannot approach unique user a number of times")

    def destroy(self, request, *args, **kwargs):
        response = {'message': 'Delete is not allowed !'}
        return Response(response, status=status.HTTP_400_BAD_REQUEST)

    # partial_update(patch)は使用できるようにする
    # def partial_update(self, request, *args, **kwargs):
    #    response = {'message': 'Patch is not allowed !'}
    #    return Response(response, status=status.HTTP_400_BAD_REQUEST)

RESTAPI メッセージ送信機能の実装

メッセージのREST APIは送信を受信で機能が違うのでクラスビューを分けて実装します
まずはダイレクトメッセージの送信機能を実装します

データ取得はログインユーザーでフィルタリングします
データ作成時はログインユーザーを送り手に格納します
データ削除は出来ないようにします
メッセージ修正ができるようにし、メッセージ変更は基本的に差分変更(PATCH)で行う想定とします

views.py

from .models import DirectMessage
from .serializers import DirectMessageSerializer


class DirectMessageViewSet(ModelViewSet):
    queryset = DirectMessage.objects.all()
    serializer_class = DirectMessageSerializer

    def get_queryset(self):
        return self.queryset.filter(sender=self.request.user)

    def perform_create(self, serializer):
        serializer.save(sender=self.request.user)

    def destroy(self, request, *args, **kwargs):
        response = {'message': 'Delete DM is not allowed'}
        return Response(response, status=status.HTTP_400_BAD_REQUEST)

RESTAPI メッセージ受信機能の実装

受信したダイレクトメッセージは編集が出来てしまっては困るため読み取り専用とするのでReadOnlyModelViewSetを使用します
取得するデータは受信側がログインユーザーとなっているデータをフィルタリングします

views.py

from rest_framework.viewsets import ReadOnlyModelViewSet


class InboxListView(ReadOnlyModelViewSet):
    queryset = DirectMessage.objects.all()
    serializer_class = DirectMessageSerializer

    def get_queryset(self):
        return self.queryset.filter(receiver=self.request.user)


以上で一通りのViewの実装は完了です

URL ルーティングを設定する

ではフロントが操作するためのURLのエンドポイントとどのViewの処理を実行するかを関連づけるルーティングの設定を行っていきましょう
では、アプリ(basicapi)フォルダのurls.pyを編集して実装していきましょう

ここで注意点として、Viewの実装で rest_framework.generics のモジュールのクラスを継承したクラスベースビュー(今回はCreateAPIView, RetrieveUpdateAPIViewを継承しているもの)はルーティングではurlpatternsに直接記載することができるが、rest_framework.viewsets モジュールクラスを継承したクラスベースビュー(ModelViewSet, ReadOnlyModelViewSetを継承しているもの)はrest_framework.routersを使用してルーティングしなければいけないという違いがある
routers.registerの第一引数はURLのパス名となる
urls.pyのコードは全量を記載しておく

urls.py

from rest_framework.routers import DefaultRouter
from django.urls import path
from django.conf.urls import include
from .views import CreateUserView
from .views import ProfileViewSet
from .views import MyProfileListView
from .views import MatchingViewSet
from .views import DirectMessageViewSet
from .views import InboxListView

app_name = 'basicapi'

router = DefaultRouter()
router.register('profiles', ProfileViewSet)
router.register('favorite', MatchingViewSet)
router.register('dm-message', DirectMessageViewSet)
router.register('dm-inbox', InboxListView)

urlpatterns = [
    path('users/create/', CreateUserView.as_view(), name='users-create'),
    path('users/<pk>', UserView.as_view(), name='users'),
    path('users/profile/', MyProfileListView.as_view(), name='users-profile'),
    path('', include(router.urls)),
]

Djasorを利用したときのJWTでのユーザー認証のエンドポイントについて

なお、ユーザー認証すなわちログインログアウトのための機能は djoser のモジュールを最初にインストールしたので、すでに/authen/のエンドポイントで利用することができるようになっている
今回はJWTを利用するので/authen/jwt/create/を利用する
JWTでのユーザーログイン認証の仕組みとしては、リクエストbodyにemail: [email]、password: [password]を正しく入力して、/authen/jwt/create/のエンドポイントを呼ぶと、JWTが生成されログイン成功となります
ログイン後は生成されたトークンを利用してユーザーを認証する仕組みとなっています


以上でREST APIの一通りの実装は完了です
次は動作確認を行っていきましょう

次回予告

長くなったのでここで一度区切ります
次回の記事はこちらです

shinseidaiki.hatenablog.com