[DRF]apiの作成(get)part2


django-rest-frameworkの勉強中

前回の記事の続きです。
drfでgetのAPIを作成をしたので、今回は

  • listでの取得
  • 汎用APIViewを使用しての取得

を行います。

Model情報は以下のとおりです。

models.py
from django.db import models

DISTRICT_CATEGORIES = [
    (1, "地区1"),
    (2, "地区2"),
    (3, "地区3"),
    (4, "地区4"),
]


class Student(models.Model):
    """生徒情報"""

    # 生徒ID
    student_id = models.CharField(max_length=4, primary_key=True)

    # クラス
    class_no = models.CharField(max_length=1)

    # 出席番号
    attendance_no = models.IntegerField()

    # 名前
    name = models.CharField(max_length=20)

    # 地区番号
    district_no = models.CharField(max_length=1, choices=DISTRICT_CATEGORIES)

    # フリーコメント
    comment = models.CharField(max_length=200,blank=True)

    class Meta:
        constraints = [
            models.UniqueConstraint(
                fields=["class_no", "attendance_no"],
                name="class_attendance_unique"
            ),
        ]
STUDENT ID CLASS NO ATTENDANCE NO NAME DISTRICT NO COMMENT
0005 2 2 fugafuga 地区1 2組2番
0004 2 1 hogehoge 地区2 2組1番
0003 1 3 山田3番 地区4 1組3番
0002 1 2 山田花子 地区2 1組2番
0001 1 1 山田太郎 地区1 1組1番

一覧の取得をします

前回は一件だけの取得だったので、次は一覧の取得をします。
たとえば、クラスの生徒全員を返すようなAPIとしましょうか。

Serializer

ひとまず前回と同じでいきましょう。

serializers.py
class StudentSerializer(serializers.ModelSerializer):
    """Studentクラスのシリアライザ"""

    class Meta:
        model = Student
        fields = '__all__'

view

views.py
class StudentListAPIView(APIView):

    def get(self, request, class_no, *args, **kwargs):
        """Student一覧をclass_noで取得"""

        # モデルのオブジェクト取得
        queryset = Student.objects.filter(class_no=class_no)

        # シリアライズ
        serializer = StudentSerializer(instance=queryset, many=True)

        # 返却
        return Response(serializer.data, status.HTTP_200_OK)

url

urls.py
path('<class_no>/', views.StudentListAPIView.as_view())

動かしてみました

http://127.0.0.1:8000/student/1

[{"student_id":"0001","class_no":"1","attendance_no":1,"name":"山田太郎","district_no":"1","comment":"1組1番"},{"student_id":"0002","class_no":"1","attendance_no":2,"name":"山田花子","district_no":"2","comment":"1組2番"},{"student_id":"0003","class_no":"1","attendance_no":3,"name":"山田3番","district_no":"4","comment":"1組3番"}]

できましたね!
重要なところはViewsのなかでシリアライズをするときにmany=Trueのオプションをつけるところでしょうか。また、リストの場合はModelSerializerでなくてListSerializerでシリアライズすることも可能そうです。

ちなみに条件によるフィルタリングについてだいぶ調べたんですが、django-filterというライブラリが良く見つかりました。今回、class_noで検索条件を書いていますが、クエリパラメータにより動的に検索条件を決めたいときとかはdjango-filterを使うと良さそうですね。
http://127.0.0.1:8000/student/?attendance_no=2
こんな感じに書くと検索条件が動的に出席番号になるイメージ。

ListSerializerバージョン

一応やっておきます。更新系はこっち使っといたほうが良いらしい・・・?更新のときにまた調べようと思います。

Serializer

serializers.py
class StudentListSerializer(serializers.ListSerializer):
    child = StudentSerializer()

view

シリアライズする部分が変わってます。

views.py
# シリアライズ
- serializer = StudentSerializer(instance=queryset, many=True)
+ serializer = StudentListSerializer(instance=queryset)

実行

[{"student_id":"0001","class_no":"1","attendance_no":1,"name":"山田太郎","district_no":"1","comment":"1組1番"},{"student_id":"0002","class_no":"1","attendance_no":2,"name":"山田花子","district_no":"2","comment":"1組2番"},{"student_id":"0003","class_no":"1","attendance_no":3,"name":"山田3番","district_no":"4","comment":"1組3番"}]

できました。

汎用APIViewを用いる

汎用APIViewを用いると、getのAPIがもっと簡単に記述できそうです。
APIの用途に合わせてクラスを継承したViewを作るともっとコード量が少なく書けそうです。
例えば一覧ではなく一件だけ取得するようなAPIを考えてみます。RetrieveAPIViewを使ってみましょう。

url

urls.py
path('<pk>/retrieve/', views.StudentRetrieveAPIView.as_view())

view

views.py
from rest_framework import generics

from .models import Student
from .serializers import StudentSerializer


class StudentRetrieveAPIView(generics.RetrieveAPIView):
    queryset = Student.objects.all()
    serializer_class = StudentSerializer

実行

{"student_id":"0001","class_no":"1","attendance_no":1,"name":"山田太郎","district_no":"1","comment":"1組1番"}

できました。

えっこれだけ!?
viewの実装が意味わからないくらい少なくなってる・・・。

views.py
- from django.shortcuts import get_object_or_404
- from rest_framework import status
- from rest_framework.views import APIView
- from rest_framework.response import Response
+ from rest_framework import generics

from .models import Student
from .serializers import StudentSerializer


- class StudentRetrieveAPIView(APIView):
+ class StudentRetrieveAPIView(generics.RetrieveAPIView):

-     def get(self, request, pk, *args, **kwargs):
-         """Studentをpkで取得"""
-         # モデルのオブジェクト取得
-         instance = get_object_or_404(Student, pk=pk)
-         # シリアライズ
-         serializer = StudentSerializer(instance)
-         # 返却
-         return Response(serializer.data, status.HTTP_200_OK)

+         queryset = Student.objects.all()
+         serializer_class = StudentSerializer

コード量がすごく少なくなりましたね。。
RetrieveAPIViewでいろいろとやってくれてるって感じですね。pkでの取得も裏でやってくれていそうです。url.pyでpkと書いてあるところを勝手に読み込んでくれています。
RetrieveAPIViewで継承しているRetrieveModelMixinGenericAPIViewがいろいろやってくれていそうです。まぁ確かに取得してシリアライズするだけだから汎用的な処理に落とし込めるやろってことですね。

RetrieveAPIViewで複数条件検索

今度はpkではなくclass_no、attendance_noから検索をしてみます。

url

url.py
path('<class_no>/<int:attendance_no>/retrieve/', views.StudentRetrieveAPIView.as_view())

view

view.py
class StudentRetrieveAPIView(generics.RetrieveAPIView):

    serializer_class = StudentSerializer
    queryset = Student.objects.all()

    def retrieve(self, request, class_no, attendance_no, *args, **kwargs):
        instance = get_object_or_404(self.queryset, class_no=class_no, attendance_no=attendance_no)
        serializer = StudentSerializer(instance)

        return Response(serializer.data)

今回は、検索条件がpkではないので独自の実装が必要になります。
RetrieveViewのアクションメソッドであるretrieveをオーバーライドして実装しています。
(ハンドラメソッド・・・get、アクションメソッド・・・retrieve)

汎用APIViewを用いるときはクラスフィールドにserializer_classquerysetという変数が必要になってくるようです。

実行

{"student_id":"0002","class_no":"1","attendance_no":2,"name":"山田花子","district_no":"2","comment":"1組2番"}

無事取得できました。

うーん・・・

なかなか難しい・・・というか理解がしづらい。
例えばqueryset = Student.objects.all()のところ。最初に全件とってきてるけど、今回したいのってフィルタリングだから全件とかいらないんだけどって思って調べました。
→queryの遅延評価というものがあるようです。実際にORMでSQLが流れるのは、queryの値を使うときだけのよう。今度記事にしよっと。

あとは
model.object.all()とか、model.object.filter(xx=xx)で戻ってくるのはqueryset型で、それはserializeできない(リストのときにやるやつ)とか、
serializeするときはget_objectで取得(retrieveのとき)とか、いろいろ気にすることはありそうですね。

とりあえずgetはここまでにしようかな。
次はpostあたりをやろうと思います。