Django查询集ID缓存与多Viewset复用及数据返回方案咨询
首先,你提到的第一种扁平JSON方案确实存在严重的数据冗余问题——长描述重复、图片导致数据条目爆炸,不仅浪费带宽,前端处理也会很麻烦,完全不推荐。
第二种分请求的方案是可行的,但实现时需要注意HTTP的无状态特性,而且其实还有更简单高效的替代方案,我先给你拆解清楚:
最优方案:嵌套序列化器+预查询(单请求无冗余)
Django Rest Framework(DRF)的嵌套序列化器可以完美解决你的问题:一次请求获取所有关联数据,返回结构化的嵌套JSON,完全避免数据冗余。
步骤1:优化查询集
使用select_related(处理多对一关联:商品→店铺→城市)和prefetch_related(处理一对多反向关联:商品→图片)预加载所有关联数据,避免N+1查询问题:
products = Product.objects.filter(name__icontains=keyword) \ .select_related('shop', 'shop__city') \ .prefetch_related('related_product') # 对应Image模型的related_name
步骤2:定义嵌套序列化器
从最底层的City开始,逐层向上嵌套:
from rest_framework import serializers from .models import City, Shop, Product, Image class CitySerializer(serializers.ModelSerializer): class Meta: model = City fields = ('id', 'name') class ShopSerializer(serializers.ModelSerializer): city = CitySerializer() # 嵌套城市序列化器 class Meta: model = Shop fields = ('id', 'name', 'city') class ImageSerializer(serializers.ModelSerializer): class Meta: model = Image fields = ('id', 'image') # image字段会返回配置的媒体文件路径 class ProductSerializer(serializers.ModelSerializer): shop = ShopSerializer() # 嵌套店铺序列化器 related_product = ImageSerializer(many=True) # 嵌套多图片序列化器 class Meta: model = Product fields = ('id', 'name', 'description', 'shop', 'related_product')
步骤3:在Viewset中使用
from rest_framework import viewsets from .models import Product from .serializers import ProductSerializer class ProductSearchViewSet(viewsets.ReadOnlyModelViewSet): serializer_class = ProductSerializer queryset = Product.objects.all() def get_queryset(self): keyword = self.request.query_params.get('keyword', '') if keyword: return Product.objects.filter(name__icontains=keyword) \ .select_related('shop', 'shop__city') \ .prefetch_related('related_product') return super().get_queryset()
返回的JSON结构会是这样的(无任何冗余):
[ { "id": 1, "name": "iPhone 15", "description": "最新款苹果手机,搭载A17 Pro芯片...", "shop": { "id": 1, "name": "Apple官方旗舰店", "city": { "id": 1, "name": "北京" } }, "related_product": [ { "id": 1, "image": "/media/images/iphone15_front.jpg" }, { "id": 2, "image": "/media/images/iphone15_back.jpg" } ] } ]
如果你坚持要使用第二种分请求方案:实现方式
如果因为某些原因必须分多次请求获取数据,有两种可行的实现思路:
思路1:客户端携带商品ID列表
让客户端在获取商品列表后,保存商品ID,然后在请求图片/店铺数据时,将ID列表作为参数传递(比如用逗号分隔):
# 图片Viewset示例 class ImageViewSet(viewsets.ReadOnlyModelViewSet): serializer_class = ImageSerializer queryset = Image.objects.all() def get_queryset(self): product_ids = self.request.query_params.get('product_ids', '') if product_ids: id_list = [int(id) for id in product_ids.split(',') if id.isdigit()] return self.queryset.filter(product_id__in=id_list) return super().get_queryset()
客户端请求示例:GET /api/images/?product_ids=1,2,3
这种方式简单无状态,但如果商品ID过多,可能会超出URL长度限制(一般浏览器支持的URL长度在2000字符左右)。
思路2:缓存搜索结果ID(用Redis)
通过生成唯一搜索token,将商品ID列表缓存到Redis中,客户端后续请求携带token即可:
- 先安装依赖:
pip install redis django-redis - 配置settings.py的缓存:
CACHES = { "default": { "BACKEND": "django_redis.cache.RedisCache", "LOCATION": "redis://127.0.0.1:6379/1", "OPTIONS": { "CLIENT_CLASS": "django_redis.client.DefaultClient", } } }
- 商品搜索Viewset:
import uuid from django.core.cache import cache from rest_framework import viewsets from rest_framework.response import Response from .models import Product from .serializers import ProductWithoutImagesSerializer # 不含图片的商品序列化器 class ProductSearchViewSet(viewsets.ViewSet): def list(self, request): keyword = request.query_params.get('keyword', '') if not keyword: return Response([]) products = Product.objects.filter(name__icontains=keyword) \ .select_related('shop', 'shop__city') product_ids = list(products.values_list('id', flat=True)) # 生成唯一搜索token search_token = str(uuid.uuid4()) # 缓存ID列表,10分钟过期 cache.set(search_token, product_ids, 60*10) # 返回商品数据和token serializer = ProductWithoutImagesSerializer(products, many=True) return Response({ 'products': serializer.data, 'search_token': search_token })
- 图片Viewset:
from django.core.cache import cache from rest_framework import viewsets from rest_framework.response import Response from .models import Image from .serializers import ImageSerializer class ImageViewSet(viewsets.ViewSet): def list(self, request): search_token = request.query_params.get('search_token', '') if not search_token: return Response([]) product_ids = cache.get(search_token) if not product_ids: return Response([]) images = Image.objects.filter(product_id__in=product_ids) serializer = ImageSerializer(images, many=True) return Response(serializer.data)
这种方式避免了客户端存储大量ID,但引入了缓存依赖,增加了系统复杂度,适合ID数量极大的场景。
后续ElasticSearch适配
如果后续引入ElasticSearch,嵌套序列化的方案也能很好适配:你可以将商品、店铺、城市、图片的嵌套结构直接存储到ES中,搜索时直接返回结构化结果,无需再查询数据库(或仅补充少量数据库数据),性能会更优。
内容的提问来源于stack exchange,提问作者dejandenib

