【Django REST-framework】JSONで送られてきたデータをそれぞれ適切なテーブルに保存したいときにやること【Viewにロジック編】

こちらはDjango初学者の備忘録的なまとめです。
誤りやより良い方法などあればご指摘をお願いいたします。

# ✅ やりたいこと
DRFで作成したModelにフロントから送られてきたデータを適切に振り分けて保存したい。
何なら保存するときに関連付けもしちゃいたい。
**それをViewに書きたい**

というわけで、こちらは**View**に書く場合を想定した記事です。
個人的に今回のようなデータをModelに振り分けるだけの処理はSerializerで実装するのがよいと思っているのですが、興味があったのでやってみることにします。

Serializerでやる方法が気になるよという方はこちらから
[作成中]

## 💻 前提
フロントエンドで作成された入力フォームから、以下のようなデータがJSON形式で送られてくるとします。

~~~JSON
{
  "name": "Yamada Taro",
  "email": "taro@example.com",
  "role": "admin",
  "feature": "overachiever",
  "hobby": "reading"
}
~~~

この時、DRFではUserモデル, Roleモデル, Featureモデル,のような3つのモデルが存在しており、以下のような**要望**があったとします。

- Userモデルには```"name", "email"```を保存したい
- Roleモデルには```"role"```を保存したい
- Featureモデルには```"feeature","role"```を保存したい
- /api/v1/user/をGETするときに得られるデータが、それぞれどこ由来のものなのかわかるような形式にしたい

なぜこんなデータベース構成になるのかは例ということで今回は置いておいて、こんな感じのデータをとることが目標。
~~~JSON
{
    "id": "ca7d0075-a87c-46ed-80e0-eea4ddebfe5a",
    "name": "Yamada Taro",
    "email": "taro@example.com",
    "roles": [
        {
            "role": "admin"
        }
    ],
    "features": [
        {
            "hobby": "reading",
            "feature": "overachiever"
        }
    ]
}
~~~

# ✅ やったこと

それぞれのModelをまとめたSerializerを作成し、Viewsで条件分岐させる

# ✅ コード
Model, Serializer, Viewの順に作成していきます。

## Models
まずはModelを作成します。

~~~python:models.py
from django.db import models
import uuid

class User(models.Model):
    id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
    name = models.CharField(max_length=255)
    email = models.EmailField(max_length=254, unique=True)

class Role(models.Model):
    id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
    user = models.ForeignKey(User, related_name='roles', on_delete=models.CASCADE)
    name = models.CharField(max_length=255)
    role = models.CharField(max_length=255, choices=(('admin', 'admin'), ('user', 'user')))


class Feature(models.Model):
    id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
    user = models.ForeignKey(User, related_name='features', on_delete=models.CASCADE)
    hobby = models.CharField(max_length=255, blank=True, null=True)
    feature = models.CharField(max_length=255, blank=True, null=True)   
~~~
この時、Userモデルから逆参照できるように、RoleとFeatureモデルのForeignKeyにrelated_nameをつけてあげます。
~~~python:models.py
# Role
 user = models.ForeignKey(User, related_name='roles', on_delete=models.CASCADE)

 # Feature
 user = models.ForeignKey(User, related_name='roles', on_delete=models.CASCADE)
~~~
こうすることで、関連付けられたモデル（User）から関連するオブジェクト（Role）の集合にアクセスすることができるようになります。
~~~python:example.py
user = User.objects.get(id=1)  # IDが1のUserインスタンスを取得
user_roles = user.roles.all()  # このUserに関連するすべてのRoleインスタンスを取得

# 例えば、以下のようなRoleインスタンスが存在するとします：
# 1. Role(id=1, user_id=1, role_name='admin')
# 2. Role(id=2, user_id=1, role_name='editor')
# 3. Role(id=3, user_id=1, role_name='viewer')

# user_rolesは上記のRoleインスタンスを含むクエリセットを返します。
# これをループで回すことで、それぞれのRoleの詳細にアクセスできます。
for role in user_roles:
    print(role.role_name)  # これにより 'admin', 'editor', 'viewer' が順に出力されます。
~~~

ここまで出来たら一旦マイグレーションしてもいいでしょう。
~~~bash
python manage.py makemigrations
python manage.py migrate
~~~

## Serializers

次はSerializerを作成。
~~~python:serializers.py
from rest_framework import serializers
from .models import User, Role, Feature


class RoleSerializer(serializers.ModelSerializer):
    class Meta:
        model = Role
        fields = ['role']

class FeatureSerializer(serializers.ModelSerializer):
    class Meta:
        model = Feature
        fields = ['feature']

class UserSerializer(serializers.ModelSerializer):
    roles = RoleSerializer(many=True, read_only=True)
    features = FeatureSerializer(many=True, read_only=True)
    class Meta:
        model = User
        fields = ['id', 'name', 'email', 'roles', 'features']
~~~
この時、UserSerializerはRole, Featureよりも下においてあげましょう。
Syntaxが出るのですぐわかりますが、一対多の一を上に置きたい僕のような人間はたまにドツボにはまります（1敗）


## Views
ここまででModelとSerializerを作成できました。
最後にViewを作成します。

~~~python:views.py
from rest_framework import viewsets, status
from rest_framework.response import Response
from rest_framework.decorators import action
from .models import User, Role, Feature
from .serializers import UserSerializer, RoleSerializer, FeatureSerializer

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

    def list(self, request, *args, **kwargs):
        queryset = User.objects.all()
        user_serializer = UserSerializer(queryset, many=True)
        return Response(user_serializer.data)

    def create(self, request, *args, **kwargs):
        user_data = {'name': request.data.get('name'), 'email': request.data.get('email')}
        user_serializer = UserSerializer(data=user_data)

        if user_serializer.is_valid():
            user = user_serializer.save()

            role_data = {
                'role': request.data.get('role')}
            role_serializer = RoleSerializer(data=role_data)
            if role_serializer.is_valid():
                role_serializer.save()

            feature_data = {
                'hobby': request.data.get('hobby'),
                'feature': request.data.get('feature')
            }
            feature_serializer = FeatureSerializer(data=feature_data)
            if feature_serializer.is_valid():
                feature_serializer.save()
            return Response({"status": "success"}, status=status.HTTP_201_CREATED)
        else:
            return Response({"status": "failed", "errors": user_serializer.errors}, status=status.HTTP_400_BAD_REQUEST)
~~~

DRFにはModelViewSetという大変便利な機能があるので、こちらを使っていくことにします。
まずは一覧を取得するlistをオーバーライドして、目標としているデータの形にしていきます。
~~~python:views.py
def list(self, request, *args, **kwargs):
    queryset = User.objects.all()
    user_serializer = UserSerializer(queryset, many=True)
    return Response(user_serializer.data)
~~~
今回はRole, Featureと複数のインスタンスを処理する必要があるため、many=Trueにしてあげましょう。
これでGETは作成できました。次はPOSTです。
ModelViewSetのcreateをオーバーライドしていきます。

~~~python:views.py
def create(self, request, *args, **kwargs):
user_data = {'name': request.data.get('name'), 'email': request.data.get('email')}
user_serializer = UserSerializer(data=user_data)

if user_serializer.is_valid():
    user = user_serializer.save()

    role_data = {
        'role': request.data.get('role')}
    role_serializer = RoleSerializer(data=role_data)
    if role_serializer.is_valid():
        role_serializer.save()

    feature_data = {
        'hobby': request.data.get('hobby'),
        'feature': request.data.get('feature')
    }
    feature_serializer = FeatureSerializer(data=feature_data)
    if feature_serializer.is_valid():
        feature_serializer.save()
    return Response({"status": "success"}, status=status.HTTP_201_CREATED)
else:
    return Response({"status": "failed", "errors": user_serializer.errors}, status=status.HTTP_400_BAD_REQUEST)
~~~
Serialzierを作成したことにより、[ModelName]_serializerが使用できるようになっているのでこれで条件分岐を作成します。
Userが正常に作成されたらRole, Featureを作成しUserにリレーションさせるようにしたいので、先にuser_serializerで条件分岐させることに注意します。

これでModel, Serializer, Viewは作成できました。
最後はurls.pyを作成します。
Djangoプロジェクト名をconfigとすることにはまっているので、今回はconfig/urls.pyでURLを作成することにします。

~~~python:config/urls.py
from django.contrib import admin
from django.urls import path, include
from rest_framework.routers import DefaultRouter
from sampleapp.views import UserView

router = DefaultRouter()
router.register('users', UserView, basename='users')

urlpatterns = [
    path('admin/', admin.site.urls),
    path("api/v1/", include(router.urls)),
]

~~~

ここまで作成出来たら```runserver```でDjangoプロジェクトを実行し、POSTMANなどで確認します。

POST: ```http://{YOUR_IP}:localhost:8000/api/v1/users/```
~~~JSON:postman-requestbody
{
  "name": "Yamada Taro",
  "email": "taro@example.com",
  "role": "admin",
  "feature": "overachiever",
  "hobby": "reading"
}
~~~

~~~JSON:postman-response
{
    "status": "success"
}
~~~

GET: ```http://{YOUR_IP}:localhost:8000/api/v1/users/```
~~~JSON:postman-response
{
    "id": "ca7d0075-a87c-46ed-80e0-eea4ddebfe5a",
    "name": "Yamada Taro",
    "email": "taro@example.com",
    "roles": [
        {
            "role": "admin"
        }
    ],
    "features": [
        {
            "hobby": "reading",
            "feature": "overachiever"
        }
    ]
}
~~~


# 最後に
今回はDRFの勉強がてらに記事を作成してみました。

ChatGPTの登場から初学者が新しい言語を学ぶ障壁がぐぐっと低くなりありがたい限りです。
とはいっても、初学者の浅い知識で完璧なものを作れるほどChatGPTは万能ではないので驕らずに学んでいければと思います。

修正点、よりよい方法などあればぜひコメントをお願いいたします。