Django(DRF)でN対NのSerializerを利用するTips

2021/12/09に公開4

こんにちは、Djangoアドベントカレンダー9日目の記事です。

自己紹介です。

大学3年生で、普段はメディア処理・情報通信系を専門に勉強しています。
大学2年後期からWeb開発にも興味が湧き、勉強しつつ趣味やインターンにも取り組んできました。

今回は、DjangoにおけるAPI作成のごく一部ですがTipsを投稿することに致しました。
テーマはDjangoRestFrameworkで少し複雑なManyToManyを扱うことです。

対象は、DjangoRestFrameworkを使ったことがありN対Nについて知らないが気になる方々です。

N対Nとは?

この記事を見ている時点でご存知の方も多いとは思いますが以下の関係性です。

音楽共有サイトなどがあったとします。

プレイリストA,B,Cというものがあり、楽曲い,ろ,はが存在している状況となります。
同じ楽曲が複数プレイリストに含まれるし、同じプレイリストで複数の楽曲を扱えるということです。

イメージ

これをER図にすると以下のようになります。一部外部テーブル省略しています。

ER図

データベースの構造上、1対Nを介する中間テーブルを作成する必要があるので上のようになります。しかし、Webバックエンドのライブラリ・フレームワークと呼ばれるものでは、これがコード上では省略されます。

しかし、実際には中間テーブルがあるという訳です。

この中間テーブルに新たなカラムを持つ場合を考えます。その場合、Djangoなどでは新たに中間テーブルのモデルを作成し、中間テーブルとして認識させる必要があります。

例えば、選んだそれぞれの楽曲にコメントを残したい!という要件だとします。
すると以下のようになるでしょう。

つまり、単にTableに中間テーブルのプライマリーキー、結合するテーブルのそれぞれのIDしか持たないなら以下での解説は不要となります。

期待するレスポンス値

設計する上で考える必要があるはずなので、先に記載します。

const sample = [
  {
    id: 1,
    name: "プレイリストA",
    owner_id: 90,
    musics: [
      {
        comment: "ハーモニーが好きです。",
        music: {
          id: 2,
          name: "曲C",
        },
      },
      {
        comment: "聴くと昔を思い出します。",
        music: {
          id: 4,
          name: "曲O",
        },
      },
    ],
  },
];

Djangoでの実装例

あくまでサンプルであり、アプリケーション全体としての動作を保証するものではありません。適宜組み込む目的で記載します。

  1. モデルの定義
  2. シリアライザーの定義
  3. ビューの定義

のサンプルです。適宜コメント加えます。

モデル定義

models.py
from django import models


class Music(models.Model):
    id = models.AutoField(primary_key=True)
    name = models.CharField(max_length=50)
    # ・・・


class Playlist(models.Model):
    id = models.AutoField(primary_key=True)
    name = models.CharField(max_length=50)
    musics = models.ManyToManyField(
        Music,
        # related_name
        through="PlaylistMusic",
        # 中間テーブルを指定する
    )
    # ・・・


class PlaylistMusic(models.Model):
    id = models.AutoField(primary_key=True)
    music_id = models.ForeignKey(
        Music,
        related_name="music_playlist_music",
        on_delete=models.CASCADE,
    )
    playlist_id = models.ForeignKey(
        Playlist,
        related_name="playlist_playlist_music",
        on_delete=models.CASCADE,
    )
    comment = models.TextField()
    # ・・・

中間テーブルの指定は公式ドキュメントを参照ください。

あとは至ってシンプルです。

シリアライザー定義

Jsonでのやり取りを行う部分です。

Music、Playlist、MusicPlaylistそれぞれの定義を行います。

serializers.py
from rest_framework import fields, serializers
from .models import Music, Playlist, PlaylistMusic


class MusicSerializer(serializers.ModelSerializer):
    class Meta:
        model = Music
        fields = "__all__"


class PlaylistMusicSerializer(serializers.ModelSerializer):
    # 中間テーブル
    music = serializers.SerializerMethodField()
    comment = serializers.SerializerMethodField()

    def get_music(self, obj):
        # 音楽テーブルのシリアライザーに値を渡す
        music_obj = MusicSerializer(instance=obj.music_id)
        return music_obj.data

    class Meta:
        model = PlaylistMusic
        fields = "__all__"


class PlaylistSerializer(serializers.ModelSerializer):
    musics = serializers.SerializerMethodField()

    def get_musics(self, obj):
        # 中間テーブルのシリアライザーに渡す
        playlist = Playlist.objects.get(pk=obj.pk)
        serializer = PlaylistMusicSerializer(
            playlist.music_playlist_music.all(),
            many=True,
        )
        return serializer.data

    class Meta:
        model = Playlist
        fields = "__all__"

ビュー定義

コントローラーに相当します。HTTPリクエストを受け取り、返すところです。

特に変わった点はないです。基本は詳細表示に用いるかと思いますので、特定のプレイリストにアクセスするGETメソッドのみ実装です。

views.py
from rest_framework.views import APIView
from rest_framework.permissions import AllowAny
from rest_framework import status
from rest_framework.response import Response
from rest_framework.generics import get_object_or_404
from .serializers import PlaylistSerializer
from .models import Music


class PlaylistDetailAPIView(APIView):
    authentication_classes = []
    permission_classes = [AllowAny, ]

    def get(self, request, pk, *args, **kwargs):
        playlist = get_object_or_404(Playlist, pk=pk)
        serializer = PlaylistSerializer(playlist)
        return Response(
            serializer.data,
            status=status.HTTP_200_OK
        )

さいごに

最後までお読みいただきありがとうございました!

何か間違いやアドバイスなどありましたら、いただけると嬉しいです。

Discussion

tettetette

初めまして。
同じようなn対nのserializerを作成したのですが、記事のようにview.pyでシリアライザー初期化後にserializer.dataで呼び出すと「is_valid()呼び出してから、.dataを使用してください」みたいなエラーになるのですが、この記事だとis_valid()呼び出していないように思います。
何かここには書いてない方法があったりしますでしょうか?

こふこふ

@tette さん、コメントありがとうございます。

GETメソッドであれば、なくても動くはずです。自分の環境ではDB設計などは多少異なりますが、同じ方針で動いています。

When deserializing data, you always need to call is_valid() before attempting to access the validated data, or save an object instance.

参考:https://www.django-rest-framework.org/api-guide/serializers/#validation

デシリアライズする際に、validated_dataにアクセス、オブジェクトの保存(POST/PATCH/PUTメソッドなど)をする場合は、modelオブジェクトに基づいてvalidationを効かせるためにis_valid()メソッドを呼ぶ必要があります。

もしGETメソッドで値に直接アクセスしていない場合でもエラーが出るようでしたら教えてくだださい🙏

tettetette

ご返信ありがとうございます!
自分のミスで下記のようにdataにクエリセット渡していたのが原因みたいでした...
「data=」と指定せずに渡すと、なんなく.dataで呼び出せるようになりましたmm

store_serializer = StoreSerializer(data=query_set, many=False)

今回自分がやりたい事と記事がマッチしてたので、大いに参考にさせていただきました!ありがとうございました!!