こんにちは、かつコーチです。
前回は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 / APIView | HTTPの受け口、パーミッション判定、クエリの最適化指定 |
| Serializer | 入出力の形式変換、フィールド単位・複合的な入力バリデーション |
| Model / Service層 | ドメインロジック、状態遷移、複数モデルをまたぐ処理 |
この分担を守ると、Web画面用のViewとAPI用のViewSetが同じModel・Service層を共有でき、ロジックの二重管理を避けられます。
まとめ
この記事のポイント
- シリアライザの本来の責務は「変換」と「入力バリデーション」であり、業務ロジックはModelやService層に置く
- フィールド単位は
validate_<field>、複合チェックはvalidate()で行う - ネストしたシリアライザはN+1問題を起こしやすく、ViewSetの
queryset側でselect_related・prefetch_relatedを指定する - ViewSet・Serializer・Model/Serviceで責務を分けると、Web画面とAPIでロジックを共有できる
次に読むべき記事
責務分担を踏まえて、実際にシンプルなTodo APIを組み立てる過程を次回解説します。
→ 次の記事:Django REST FrameworkでシンプルなTodo APIを作ってみる