【Django】Django REST Frameworkでシリアライザとアーキテクチャを設計する

Django

こんにちは、かつコーチです。

前回はFat Model・Skinny Viewの考え方を扱いましたが、DRFでAPIを組むと今度は「シリアライザに何を書くか」で同じ問題に突き当たります。

シリアライザはバリデーションもデータ変換もできてしまうため、雑に使うともう1つのFat Viewになりがちです。

この記事では、Django REST Framework(DRF)のシリアライザの責務をどう設計すべきか、実務目線で整理します。

シリアライザの本来の責務を整理する

シリアライザは「変換」と「入力検証」の担当

DRFのSerializerは、Pythonオブジェクト⇔JSONの相互変換と、入力値のバリデーションを担当するレイヤーです。

Modelのフィールドをそのまま公開するModelSerializerを使えば、多くのケースで数行のコードでAPIの入出力を定義できます。

# serializers.py
from rest_framework import serializers
from .models import Order

class OrderSerializer(serializers.ModelSerializer):
    total_price = serializers.IntegerField(read_only=True)

    class Meta:
        model = Order
        fields = ["id", "status", "total_price", "confirmed_at"]
        read_only_fields = ["status", "confirmed_at"]

total_priceのようにModel側で@propertyとして定義したフィールドも、read_only=Trueで指定すれば出力に含められます。

シリアライザに業務ロジックを書き始めると起こること

「注文確定のロジック」までシリアライザのvalidate()やビュー側にベタ書きすると、Web画面用のViewと処理が重複します。

前回解説した通り、業務ロジックはあくまでModel(またはService層)に置き、シリアライザはその結果を整形するだけに留めるのが原則です。

バリデーションの置き場所を設計する

フィールド単位のバリデーション

単一フィールドの形式チェックは、シリアライザのvalidate_<field名>メソッドで完結させます。

class OrderSerializer(serializers.ModelSerializer):
    class Meta:
        model = Order
        fields = ["id", "quantity"]

    def validate_quantity(self, value):
        if value <= 0:
            raise serializers.ValidationError("数量は1以上を指定してください")
        return value

複数フィールドにまたがるバリデーション

複数フィールドの整合性チェックはvalidate()(オブジェクト全体を受け取るメソッド)で行います。

def validate(self, attrs):
    if attrs["start_date"] > attrs["end_date"]:
        raise serializers.ValidationError("開始日は終了日より前である必要があります")
    return attrs

一方、「在庫が足りているか」のようなModelの状態に依存する判断は、シリアライザではなくModelやService層に問い合わせる形にします。

def validate(self, attrs):
    if not Inventory.objects.has_stock(attrs["product"], attrs["quantity"]):
        raise serializers.ValidationError("在庫が不足しています")
    return attrs

チェック自体はシリアライザから呼び出しますが、在庫の判定ロジック自体はModel側の責務として保つ、という線引きが重要です。

ネストしたシリアライザとN+1問題

関連モデルを含めて出力する

注文に紐づく商品明細を一緒に返したい場合は、シリアライザをネストします。

class OrderItemSerializer(serializers.ModelSerializer):
    class Meta:
        model = OrderItem
        fields = ["id", "product_name", "quantity", "price"]


class OrderSerializer(serializers.ModelSerializer):
    items = OrderItemSerializer(many=True, read_only=True)

    class Meta:
        model = Order
        fields = ["id", "status", "items"]

❌ Before:ViewSetでクエリを最適化せず一覧APIが遅くなる

class OrderViewSet(viewsets.ReadOnlyModelViewSet):
    queryset = Order.objects.all()
    serializer_class = OrderSerializer

筆者は最初この書き方のまま一覧APIをリリースし、注文が増えるにつれてレスポンスが数秒かかるようになった経験があります。

ネストしたitemsを出力するたびに、注文1件ごとに追加クエリが発行されるN+1問題が原因でした。

✅ After:prefetch_relatedをViewSetのqueryset側で指定する

class OrderViewSet(viewsets.ReadOnlyModelViewSet):
    queryset = Order.objects.prefetch_related("items").select_related("customer")
    serializer_class = OrderSerializer

querysetはViewSet単位で1度定義すれば、どのアクション(一覧・詳細)でも最適化が効きます。

シリアライザ側にクエリ最適化のコードを書く必要はなく、ViewSet側の責務として切り分けるのが定石です。

アーキテクチャ全体の責務分担

DRFでAPIを設計する際は、次のような役割分担を意識すると見通しがよくなります。

レイヤー責務
ViewSet / APIViewHTTPの受け口、パーミッション判定、クエリの最適化指定
Serializer入出力の形式変換、フィールド単位・複合的な入力バリデーション
Model / Service層ドメインロジック、状態遷移、複数モデルをまたぐ処理

この分担を守ると、Web画面用のViewとAPI用のViewSetが同じModel・Service層を共有でき、ロジックの二重管理を避けられます。

まとめ

この記事のポイント

  • シリアライザの本来の責務は「変換」と「入力バリデーション」であり、業務ロジックはModelやService層に置く
  • フィールド単位はvalidate_<field>、複合チェックはvalidate()で行う
  • ネストしたシリアライザはN+1問題を起こしやすく、ViewSetのqueryset側でselect_relatedprefetch_relatedを指定する
  • ViewSet・Serializer・Model/Serviceで責務を分けると、Web画面とAPIでロジックを共有できる

次に読むべき記事

責務分担を踏まえて、実際にシンプルなTodo APIを組み立てる過程を次回解説します。

→ 次の記事:Django REST FrameworkでシンプルなTodo APIを作ってみる

タグ: #Django #上級者向け #設計

タイトルとURLをコピーしました