第 7 章 · Django REST Framework 入门
本章目标:安装并配置 Django REST Framework(DRF);掌握 Serializer 序列化与反序列化;使用 ViewSet 与 Router 快速暴露 REST API;配置分页;完成与前端 JSON 对接的完整链路;在虚构电商 shop-demo 上实现商品 API。
学时建议:4~5 小时(含 2 小时 API 联调)
前置:本模块 ch01 MTV 与项目结构;ch04 CBV;ch05 ORM 与 Product 模型;ch06 分页与 Admin。Python 基础见 python-dev ch11 requests 与 JSON。
7.1 为什么需要 DRF
传统 Django 视图返回 HTML 模板;前后端分离架构中,后端需提供 JSON API,供 Vue/React 或移动端消费。
浏览器 / SPA 前端
→ HTTP GET/POST/PUT/DELETE
→ DRF ViewSet
→ Serializer(校验 + 转换)
→ Model ↔ 数据库
→ JSON Response
| 对比项 | Django 视图 + JsonResponse | DRF |
|---|---|---|
| 序列化 | 手写 dict | Serializer 声明字段 |
| 校验 | 手动 if 判断 | is_valid() 统一处理 |
| 路由 | 每个 URL 单独写 | Router 自动生成 REST 路由 |
| 分页 | 自己算 offset | PageNumberPagination 内置 |
| Browsable API | 无 | 开发期可视化调试 |
说明:本章示例均基于教学项目 shop-demo,API 基址使用https://api.example.com或本地http://127.0.0.1:8000,不引用任何企业内部仓库。
模型演进提示:从本章起,Product模型升级为企业常用字段——sku更名为slug(SEO 友好标识)、is_active更名为is_published,并补充description、updated_at。请先执行字段迁移再继续:
>
```bash
python manage.py makemigrations catalog --name evolve_product_fields
python manage.py migrate
```
>
字段对照表(ch01~ch06 练习 → ch07+ 正式模型):
>
| ch01~ch06 | ch07+ | 说明 |
|------------|-------|------|
|sku|slug| 商品 URL 标识 |
|is_active|is_published| 是否上架 |
| — | description | 详情大字段 |
| — | updated_at | 自动更新时间 |
>
本章及后续章节(ch07~ch19)均以新字段为准;ch01~ch06 的旧字段示例请按上表对照理解。
7.2 安装与全局配置
在 shop-demo 虚拟环境中:
cd ~/python-learn/shop-demo
pip install "djangorestframework>=3.14,<4" "django-filter>=24,<26"
pip freeze | grep -iE "rest|filter" >> requirements.txt
shopdemo/settings.py:
INSTALLED_APPS = [
# ...
"rest_framework",
"django_filters", # 7.4 的过滤后端依赖
"catalog", # 商品应用,ch05 已建
]
REST_FRAMEWORK = {
"DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.PageNumberPagination",
"PAGE_SIZE": 10,
"DEFAULT_RENDERER_CLASSES": [
"rest_framework.renderers.JSONRenderer",
"rest_framework.renderers.BrowsableAPIRenderer", # 开发期保留
],
"DEFAULT_PERMISSION_CLASSES": [
"rest_framework.permissions.IsAuthenticatedOrReadOnly",
],
}
| 配置项 | 作用 |
|---|---|
PAGE_SIZE | 默认每页条数 |
IsAuthenticatedOrReadOnly | 未登录可读,写操作需登录 |
| Browsable API | 浏览器访问 /api/products/ 可点选调试 |
7.3 Serializer 基础
catalog/serializers.py:
from rest_framework import serializers
from .models import Product, Category
class CategorySerializer(serializers.ModelSerializer):
class Meta:
model = Category
fields = ["id", "name", "slug"]
class ProductListSerializer(serializers.ModelSerializer):
category_name = serializers.CharField(source="category.name", read_only=True)
class Meta:
model = Product
fields = [
"id", "name", "slug", "price", "stock",
"is_published", "category", "category_name",
"created_at",
]
read_only_fields = ["created_at"]
class ProductDetailSerializer(serializers.ModelSerializer):
"""详情接口:比列表多 description 等大字段,列表接口保持轻量"""
category_name = serializers.CharField(source="category.name", read_only=True)
class Meta:
model = Product
fields = [
"id", "name", "slug", "description", "price", "stock",
"is_published", "category", "category_name",
"created_at", "updated_at",
]
read_only_fields = ["created_at", "updated_at"]
要点:
ModelSerializer根据 Model 自动生成字段映射source="category.name"嵌套只读字段- 列表与详情可用不同 Serializer,减少列表接口 payload
手动校验示例:
class ProductWriteSerializer(serializers.ModelSerializer):
class Meta:
model = Product
fields = ["name", "slug", "price", "stock", "category", "description"]
def validate_price(self, value):
if value <= 0:
raise serializers.ValidationError("价格必须大于 0")
return value
def validate_slug(self, value):
if Product.objects.filter(slug=value).exists():
raise serializers.ValidationError("slug 已存在")
return value
7.4 ViewSet 与 Router
catalog/views.py:
from rest_framework import viewsets, filters
from rest_framework.decorators import action
from rest_framework.response import Response
from django_filters.rest_framework import DjangoFilterBackend
from .models import Product
from .serializers import ProductListSerializer, ProductDetailSerializer, ProductWriteSerializer
class ProductViewSet(viewsets.ModelViewSet):
queryset = Product.objects.select_related("category").all()
filter_backends = [DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter]
filterset_fields = ["category", "is_published"]
search_fields = ["name", "slug"]
ordering_fields = ["price", "created_at", "stock"]
ordering = ["-created_at"]
def get_serializer_class(self):
if self.action == "list":
return ProductListSerializer
if self.action in ("create", "update", "partial_update"):
return ProductWriteSerializer
return ProductDetailSerializer
@action(detail=False, methods=["get"], url_path="on-sale")