DRF Generic Views 與 Router 自動路由配置 | Django 教學

2026/06/14 2026/05/25
DRF Generic Views 與 Router 自動路由配置 | Django 教學

在前一篇文章中,我們學會了用 APIViewViewSet 處理 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提供的方法對應操作
ListModelMixinlist()列表查詢
CreateModelMixincreate()建立資源
RetrieveModelMixinretrieve()讀取單筆
UpdateModelMixinupdate(), partial_update()完整更新 / 部分更新
DestroyModelMixindestroy()刪除資源

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')

兩者的差異:

特性DefaultRouterSimpleRouter
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 ViewsRouter 機制。Generic Views 家族透過 Mixin 組合模式 提供了九種預組合的便利類別,讓你只需定義 querysetserializer_class 就能完成標準的 CRUD 操作。DefaultRouterSimpleRouter 可以自動為 ViewSet 產生 RESTful URL,搭配 @action 裝飾器還能擴展自訂端點。

在資料處理方面,Pagination 提供了三種分頁策略(PageNumber、LimitOffset、Cursor),各有適用場景;Filtering 則透過 django-filterSearchFilterOrderingFilter 的組合,讓客戶端可以靈活地查詢資料。

掌握這些工具後,你就能用極少的程式碼建構出功能豐富的 API。下一篇文章將進入 DRF 的安全層面 – 認證機制,學習如何用 Token、JWT 和 Session 保護你的 API。

BenZ Software Developer

熱愛技術的軟體開發者,在這裡分享程式開發經驗與學習筆記。