DRF Generic Views 與 Router 自動路由配置 | Django 教學
在前一篇文章中,我們學會了用 APIView 和 ViewSet 處理 API 請求。然而,當專案規模成長,你會發現許多 View 的 CRUD 邏輯幾乎一模一樣 – 只是操作的 Model 和 Serializer 不同。DRF Generic Views 正是為此而生,它透過 Mixin 組合模式 讓你用極少的程式碼完成標準操作。搭配 Router 自動路由配置,再加上 Pagination 分頁與 Filtering 過濾機制,你可以在幾分鐘內建構出功能完整、可維護且高效能的 RESTful API。
DRF View 階層總覽
在深入 Generic Views 之前,讓我們先理解 DRF 完整的 View 繼承體系。這個階層從最底層的 APIView 到最高層的 ModelViewSet,每一層都加入更多的自動化功能:
DRF View 繼承階層
═══════════════════════════════════════════════════════
Django View
└── APIView ← DRF 基礎,處理認證/權限/Content Negotiation
├── GenericAPIView ← 加入 queryset、serializer_class、分頁等支援
│ └── ListAPIView / CreateAPIView / RetrieveAPIView ...
│ (9 種便利的 Mixin 組合類別)
│
└── ViewSet ← 將相關操作組合成一個類別
└── GenericViewSet
├── ReadOnlyModelViewSet (list + retrieve)
└── ModelViewSet (完整 CRUD)
選擇哪一層取決於你需要多少 自動化 與多少 控制權:
| View 類型 | 功能 | 適用場景 |
|---|---|---|
APIView | 基礎 CBV,手動處理所有邏輯 | 完全客製化的非標準端點 |
GenericAPIView + Mixin | 特定操作的自動化組合 | 只需部分 CRUD 操作 |
Generic Views(如 ListCreateAPIView) | 預組合好的便利類別 | 常見的操作組合 |
ModelViewSet | 完整 CRUD + Router 支援 | 標準資源 CRUD |
Generic Views 家族
DRF 提供了九種預先組合好的 Generic View 類別,每一種都對應特定的 HTTP 操作:
單一操作類別
from rest_framework import generics
from .models import Article
from .serializers import ArticleSerializer
# 只提供列表功能(GET /articles/)
class ArticleListView(generics.ListAPIView):
queryset = Article.objects.all()
serializer_class = ArticleSerializer
# 只提供建立功能(POST /articles/)
class ArticleCreateView(generics.CreateAPIView):
queryset = Article.objects.all()
serializer_class = ArticleSerializer
# 只提供單筆讀取功能(GET /articles/{pk}/)
class ArticleDetailView(generics.RetrieveAPIView):
queryset = Article.objects.all()
serializer_class = ArticleSerializer
# 只提供更新功能(PUT/PATCH /articles/{pk}/)
class ArticleUpdateView(generics.UpdateAPIView):
queryset = Article.objects.all()
serializer_class = ArticleSerializer
# 只提供刪除功能(DELETE /articles/{pk}/)
class ArticleDeleteView(generics.DestroyAPIView):
queryset = Article.objects.all()
serializer_class = ArticleSerializer
複合操作類別
更常見的情況是一個 URL 需要支援多種操作,DRF 提供了四種複合類別:
# 列表 + 建立(GET + POST /articles/)
class ArticleListCreateView(generics.ListCreateAPIView):
queryset = Article.objects.all()
serializer_class = ArticleSerializer
def perform_create(self, serializer):
"""建立時自動注入當前用戶為作者"""
serializer.save(author=self.request.user)
# 讀取 + 更新 + 刪除(GET + PUT/PATCH + DELETE /articles/{pk}/)
class ArticleDetailView(generics.RetrieveUpdateDestroyAPIView):
queryset = Article.objects.all()
serializer_class = ArticleSerializer
# 讀取 + 更新(GET + PUT/PATCH /articles/{pk}/)
# class ArticleRetrieveUpdateView(generics.RetrieveUpdateAPIView):
# 讀取 + 刪除(GET + DELETE /articles/{pk}/)
# class ArticleRetrieveDestroyView(generics.RetrieveDestroyAPIView):
搭配 URL 配置:
# urls.py
from django.urls import path
from .views import ArticleListCreateView, ArticleDetailView
urlpatterns = [
path('api/articles/', ArticleListCreateView.as_view(), name='article-list'),
path('api/articles/<int:pk>/', ArticleDetailView.as_view(), name='article-detail'),
]
Mixin 組合模式
Generic Views 的背後其實是 Mixin 的組合。當預組合的類別不符合需求時,你可以自己混搭:
from rest_framework import generics, mixins
# 自訂組合:只需要列表 + 建立 + 單筆讀取(不允許更新和刪除)
class ArticleView(
mixins.ListModelMixin, # 提供 list() 方法
mixins.CreateModelMixin, # 提供 create() 方法
mixins.RetrieveModelMixin, # 提供 retrieve() 方法
generics.GenericAPIView, # 基礎類別,提供 queryset 和 serializer 支援
):
queryset = Article.objects.all()
serializer_class = ArticleSerializer
def get(self, request, *args, **kwargs):
# 根據有無 pk 決定是列表還是單筆讀取
if kwargs.get('pk'):
return self.retrieve(request, *args, **kwargs)
return self.list(request, *args, **kwargs)
def post(self, request, *args, **kwargs):
return self.create(request, *args, **kwargs)
DRF 提供的五種 Mixin:
| Mixin | 提供的方法 | 對應操作 |
|---|---|---|
ListModelMixin | list() | 列表查詢 |
CreateModelMixin | create() | 建立資源 |
RetrieveModelMixin | retrieve() | 讀取單筆 |
UpdateModelMixin | update(), partial_update() | 完整更新 / 部分更新 |
DestroyModelMixin | destroy() | 刪除資源 |
Router 自動 URL 配置
當你使用 ViewSet 時,可以透過 Router 自動產生 URL,省去手動撰寫 urlpatterns 的繁瑣工作。
DefaultRouter vs SimpleRouter
from rest_framework.routers import DefaultRouter, SimpleRouter
from .views import ArticleViewSet, CategoryViewSet
# DefaultRouter:會額外產生 API 根目錄
router = DefaultRouter()
router.register('articles', ArticleViewSet, basename='article')
router.register('categories', CategoryViewSet, basename='category')
# SimpleRouter:不產生根目錄,也不支援格式後綴
# router = SimpleRouter()
# router.register('articles', ArticleViewSet, basename='article')
兩者的差異:
| 特性 | DefaultRouter | SimpleRouter |
|---|---|---|
API 根目錄(/api/) | 自動產生,列出所有端點 | 不產生 |
格式後綴(.json、.api) | 支援 | 不支援 |
| 產生的 CRUD URL | 相同 | 相同 |
| 適用場景 | 開發階段、需要 API 瀏覽 | 正式環境、不需要額外端點 |
router.register() 使用方式
# urls.py
from django.urls import path, include
from rest_framework.routers import DefaultRouter
from .views import ArticleViewSet, CategoryViewSet, TagViewSet
router = DefaultRouter()
# register(prefix, viewset, basename)
# prefix: URL 前綴,例如 'articles' → /api/articles/
# viewset: ViewSet 類別
# basename: URL name 前綴(可選,若 ViewSet 有 queryset 會自動推斷)
router.register('articles', ArticleViewSet, basename='article')
router.register('categories', CategoryViewSet) # basename 自動為 'category'
router.register('tags', TagViewSet)
urlpatterns = [
path('api/', include(router.urls)),
]
# 自動產生的 URL:
# GET/POST /api/articles/ → article-list
# GET/PUT/DELETE /api/articles/{pk}/ → article-detail
# GET/POST /api/categories/ → category-list
# GET/PUT/DELETE /api/categories/{pk}/ → category-detail
# GET /api/ → api-root(僅 DefaultRouter)
自訂 action 的 URL
ViewSet 中使用 @action 裝飾器定義的自訂端點,Router 也會自動產生對應的 URL:
from rest_framework.decorators import action
from rest_framework.response import Response
class ArticleViewSet(viewsets.ModelViewSet):
queryset = Article.objects.all()
serializer_class = ArticleSerializer
@action(detail=True, methods=['post'], url_path='publish')
def publish(self, request, pk=None):
"""POST /api/articles/{pk}/publish/"""
article = self.get_object()
article.is_published = True
article.save()
return Response({'status': '文章已發布'})
@action(detail=False, methods=['get'], url_path='trending')
def trending(self, request):
"""GET /api/articles/trending/"""
articles = self.get_queryset().filter(
is_published=True
).order_by('-view_count')[:10]
serializer = self.get_serializer(articles, many=True)
return Response(serializer.data)
巢狀路由(drf-nested-routers)
當資源之間有巢狀關係(例如文章底下的留言),可以使用 drf-nested-routers 套件來產生巢狀 URL:
pip install drf-nested-routers
# urls.py
from rest_framework_nested import routers
from .views import ArticleViewSet, CommentViewSet
# 父層路由
router = routers.DefaultRouter()
router.register('articles', ArticleViewSet, basename='article')
# 巢狀路由:文章底下的留言
# /api/articles/{article_pk}/comments/
# /api/articles/{article_pk}/comments/{pk}/
articles_router = routers.NestedDefaultRouter(
router, # 父層 router
'articles', # 父層 prefix
lookup='article', # URL 參數名稱(article_pk)
)
articles_router.register('comments', CommentViewSet, basename='article-comments')
urlpatterns = [
path('api/', include(router.urls)),
path('api/', include(articles_router.urls)),
]
# views.py
class CommentViewSet(viewsets.ModelViewSet):
serializer_class = CommentSerializer
def get_queryset(self):
"""根據父層文章過濾留言"""
return Comment.objects.filter(
article_id=self.kwargs['article_pk']
)
def perform_create(self, serializer):
"""建立留言時自動關聯到文章和用戶"""
serializer.save(
article_id=self.kwargs['article_pk'],
author=self.request.user,
)
Pagination 分頁設定
API 回傳大量資料時,分頁是必要的效能優化措施。DRF 內建三種分頁策略。
全域分頁設定
# settings.py
REST_FRAMEWORK = {
'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',
'PAGE_SIZE': 20, # 預設每頁 20 筆
}
PageNumberPagination(頁碼分頁)
最常見的分頁方式,使用 ?page=2 指定頁碼:
# pagination.py
from rest_framework.pagination import PageNumberPagination
class StandardPagination(PageNumberPagination):
page_size = 20 # 預設每頁筆數
page_size_query_param = 'page_size' # 允許客戶端指定:?page_size=50
max_page_size = 100 # 客戶端最大可請求筆數
回應格式:
{
"count": 150,
"next": "http://api.example.com/articles/?page=3",
"previous": "http://api.example.com/articles/?page=1",
"results": [
{"id": 21, "title": "..."},
{"id": 22, "title": "..."}
]
}
LimitOffsetPagination(偏移分頁)
使用 ?limit=20&offset=40 指定起點和數量,更靈活但大偏移量效能較差:
from rest_framework.pagination import LimitOffsetPagination
class FlexiblePagination(LimitOffsetPagination):
default_limit = 20 # 預設筆數
max_limit = 100 # 最大限制
CursorPagination(游標分頁)
使用不透明的游標進行分頁,無論資料量多大效能都很穩定,特別適合時間線型的資料:
from rest_framework.pagination import CursorPagination
class TimelinePagination(CursorPagination):
page_size = 20
ordering = '-created_at' # 必須指定排序欄位
cursor_query_param = 'cursor'
在 ViewSet 中使用分頁
# views.py
from .pagination import StandardPagination, TimelinePagination
class ArticleViewSet(viewsets.ModelViewSet):
queryset = Article.objects.all()
serializer_class = ArticleSerializer
pagination_class = StandardPagination # 覆寫全域設定
class FeedViewSet(viewsets.ReadOnlyModelViewSet):
queryset = FeedItem.objects.all()
serializer_class = FeedSerializer
pagination_class = TimelinePagination # 時間線使用游標分頁
三種分頁策略的比較:
| 策略 | 查詢參數 | 優點 | 缺點 | 適用場景 |
|---|---|---|---|---|
| PageNumber | ?page=2 | 直觀、可跳頁 | 資料變動時可能重複 | 傳統列表頁面 |
| LimitOffset | ?limit=20&offset=40 | 靈活、可任意指定範圍 | 大偏移量效能差 | 需要靈活範圍的 API |
| Cursor | ?cursor=xxx | 效能穩定、無重複 | 無法跳頁 | 時間線、動態消息 |
Filtering 過濾與搜尋
DRF 提供強大的過濾機制,讓客戶端可以精確查詢所需的資料。
django-filter 整合
pip install django-filter
# settings.py
INSTALLED_APPS = [
# ...
'django_filters',
]
REST_FRAMEWORK = {
'DEFAULT_FILTER_BACKENDS': [
'django_filters.rest_framework.DjangoFilterBackend',
],
}
# views.py
from django_filters.rest_framework import DjangoFilterBackend
class ArticleViewSet(viewsets.ModelViewSet):
queryset = Article.objects.all()
serializer_class = ArticleSerializer
filter_backends = [DjangoFilterBackend]
# 簡單用法:精確過濾
# GET /api/articles/?category=1&is_published=true
filterset_fields = ['category', 'is_published', 'author']
進階用法 – 自訂 FilterSet:
# filters.py
import django_filters
from .models import Article
class ArticleFilter(django_filters.FilterSet):
# 範圍查詢:?created_after=2026-01-01
created_after = django_filters.DateFilter(
field_name='created_at', lookup_expr='gte'
)
created_before = django_filters.DateFilter(
field_name='created_at', lookup_expr='lte'
)
# 模糊匹配:?title=django
title = django_filters.CharFilter(lookup_expr='icontains')
# 多值過濾:?tags=1&tags=2
tags = django_filters.ModelMultipleChoiceFilter(
queryset=Tag.objects.all()
)
class Meta:
model = Article
fields = ['category', 'is_published', 'author']
# views.py
class ArticleViewSet(viewsets.ModelViewSet):
filter_backends = [DjangoFilterBackend]
filterset_class = ArticleFilter # 使用自訂 FilterSet
SearchFilter(模糊搜尋)
from rest_framework import filters
class ArticleViewSet(viewsets.ModelViewSet):
queryset = Article.objects.all()
serializer_class = ArticleSerializer
filter_backends = [filters.SearchFilter]
# GET /api/articles/?search=django
search_fields = [
'title', # 預設 icontains(模糊匹配)
'content',
'^author__username', # ^ 開頭匹配(startswith)
'=category__name', # = 精確匹配(exact)
]
OrderingFilter(動態排序)
from rest_framework import filters
class ArticleViewSet(viewsets.ModelViewSet):
queryset = Article.objects.all()
serializer_class = ArticleSerializer
filter_backends = [filters.OrderingFilter]
# GET /api/articles/?ordering=-created_at,title
ordering_fields = ['created_at', 'title', 'view_count']
ordering = ['-created_at'] # 預設排序
組合使用所有 Filter Backend
在實務中,通常會同時啟用多種過濾機制:
from rest_framework import viewsets, filters
from django_filters.rest_framework import DjangoFilterBackend
from .pagination import StandardPagination
from .filters import ArticleFilter
class ArticleViewSet(viewsets.ModelViewSet):
queryset = Article.objects.select_related(
'author', 'category'
).prefetch_related('tags').order_by('-created_at')
serializer_class = ArticleSerializer
pagination_class = StandardPagination
# 同時啟用精確過濾、模糊搜尋、動態排序
filter_backends = [
DjangoFilterBackend, # ?category=1&is_published=true
filters.SearchFilter, # ?search=django
filters.OrderingFilter, # ?ordering=-created_at
]
filterset_class = ArticleFilter
search_fields = ['title', 'content']
ordering_fields = ['created_at', 'title', 'view_count']
ordering = ['-created_at']
def perform_create(self, serializer):
serializer.save(author=self.request.user)
動態選擇 Serializer
在實務中,列表和詳情頁面往往需要不同的欄位。你可以覆寫 get_serializer_class() 來動態選擇:
class ArticleViewSet(viewsets.ModelViewSet):
queryset = Article.objects.all()
def get_serializer_class(self):
if self.action == 'list':
return ArticleListSerializer # 輕量版,只含摘要
elif self.action == 'create':
return ArticleCreateSerializer # 含嚴格驗證
return ArticleDetailSerializer # 完整版,含巢狀資料
總結
本文深入探討了 DRF 的 Generic Views 與 Router 機制。Generic Views 家族透過 Mixin 組合模式 提供了九種預組合的便利類別,讓你只需定義 queryset 和 serializer_class 就能完成標準的 CRUD 操作。DefaultRouter 和 SimpleRouter 可以自動為 ViewSet 產生 RESTful URL,搭配 @action 裝飾器還能擴展自訂端點。
在資料處理方面,Pagination 提供了三種分頁策略(PageNumber、LimitOffset、Cursor),各有適用場景;Filtering 則透過 django-filter、SearchFilter 和 OrderingFilter 的組合,讓客戶端可以靈活地查詢資料。
掌握這些工具後,你就能用極少的程式碼建構出功能豐富的 API。下一篇文章將進入 DRF 的安全層面 – 認證機制,學習如何用 Token、JWT 和 Session 保護你的 API。