Django REST Framework分页列表接口添加include参数实现问询
Lightweight JSON:API-style
include Implementation for Django REST Framework Let's walk through a simple, package-free solution to add the include functionality you want, while keeping it as a sibling to the results field in your paginated response.
Step 1: Prepare Serializers for Related Models
First, make sure you have serializers for your Author and Publisher models (if you don't already):
# serializers.py from rest_framework import serializers from .models import Book, Author, Publisher class AuthorSerializer(serializers.ModelSerializer): class Meta: model = Author fields = ['id', 'name'] # Add any other fields you want to expose class PublisherSerializer(serializers.ModelSerializer): class Meta: model = Publisher fields = ['id', 'name'] # Adjust fields as needed class BookSerializer(serializers.ModelSerializer): class Meta: model = Book fields = ['id', 'title', 'author_id', 'publisher_id'] # Keep only IDs in results
Step 2: Custom Pagination Class (Preserve include in Links)
We need to modify the default pagination to keep the include parameter in the next/previous links:
# pagination.py from rest_framework.pagination import LimitOffsetPagination from urllib.parse import urlparse, urlencode class CustomLimitOffsetPagination(LimitOffsetPagination): def get_next_link(self): if self.offset + self.limit >= self.count: return None url_parts = list(urlparse(self.request.build_absolute_uri())) query_params = dict(urlparse.parse_qsl(url_parts[4])) query_params.update({ 'offset': self.offset + self.limit, 'limit': self.limit }) # Preserve the include parameter if present include_param = self.request.query_params.get('include') if include_param: query_params['include'] = include_param url_parts[4] = urlencode(query_params) return urlparse.urlunparse(url_parts) def get_previous_link(self): if self.offset <= 0: return None url_parts = list(urlparse(self.request.build_absolute_uri())) query_params = dict(urlparse.parse_qsl(url_parts[4])) query_params.update({ 'offset': max(self.offset - self.limit, 0), 'limit': self.limit }) # Preserve the include parameter if present include_param = self.request.query_params.get('include') if include_param: query_params['include'] = include_param url_parts[4] = urlencode(query_params) return urlparse.urlunparse(url_parts)
Step 3: Modify the View to Handle include
This is where we'll collect and inject the related data into the paginated response. We'll override the get_paginated_response method to add the include field:
# views.py from rest_framework.generics import ListAPIView from .models import Book, Author, Publisher from .serializers import BookSerializer, AuthorSerializer, PublisherSerializer from .pagination import CustomLimitOffsetPagination class BookListView(ListAPIView): queryset = Book.objects.all() serializer_class = BookSerializer pagination_class = CustomLimitOffsetPagination def get_paginated_response(self, data): # Get the base paginated response response = super().get_paginated_response(data) # Parse and validate the include parameter include_param = self.request.query_params.get('include', '') requested_includes = [item.strip() for item in include_param.split(',') if item.strip()] # Map plural include params to their model/serializer pairs valid_includes = { 'authors': ('author', Author, AuthorSerializer), 'publishers': ('publisher', Publisher, PublisherSerializer) } include_data = {} for include_key in requested_includes: if include_key not in valid_includes: continue # Ignore invalid include values field_name, model_class, serializer_class = valid_includes[include_key] # Get distinct related IDs from the paginated books (efficient DB query) related_ids = list(self.paginator.page.values_list( f'{field_name}_id', flat=True ).exclude(**{f'{field_name}_id': None}).distinct()) if not related_ids: continue # Fetch and serialize the related objects related_objects = model_class.objects.filter(id__in=related_ids) include_data[include_key] = serializer_class(related_objects, many=True).data # Add include data to the response if any if include_data: response.data['include'] = include_data return response
How It Works
- Request Parsing: The view reads the
includeparameter (e.g.,include=authors,publishers) and validates it against allowed values. - Efficient Data Collection: For each valid include, we fetch only the distinct related objects present in the current pagination page using a single DB query per include.
- Response Assembly: The
includedata is added as a sibling toresultsin the paginated response, matching the structure you want. - Pagination Link Preservation: The custom pagination class ensures the
includeparameter stays in thenext/previouslinks for consistent navigation.
Example Response
For a request like https://my-api-domain/api/books?limit=5&offset=0&include=authors, you'll get:
{ "count": 100, "next": "https://my-api-domain/api/books?limit=5&offset=5&include=authors", "previous": null, "results": [ {"id": 1, "title": "Django for Beginners", "author_id": 1, "publisher_id": 1}, {"id": 2, "title": "REST API Design", "author_id": 1, "publisher_id": 2} ], "include": { "authors": [ {"id": 1, "name": "William S. Vincent"} ] } }
Answers to Your Questions
- Is handling this in the view appropriate?: Yes! The view is the right place to assemble the final response structure, as it has access to both the request parameters and paginated data.
- Should this be in the serializer?: No—serializers are meant to convert model instances to data, not to handle pagination metadata or cross-object include logic.
- How to make
includea sibling toresults: By directly adding theincludekey to the response dictionary returned byget_paginated_response, which already containscount,next,previous, andresults. - Lightweight solution: This approach uses only DRF's built-in tools, no external packages required.
内容的提问来源于stack exchange,提问作者MarkD
相关产品推荐
相关产品推荐

