Django ListView获取最新关联对象 解决缓存时长不符预期问题
问题排查与解决方案
问题核心现象
- 已定义
Campaign与CampaignStatus模型,CampaignStatus通过外键关联Campaign,每次Campaign状态流转都会生成新的CampaignStatus记录 - 基于Django类视图实现用户Campaign列表页,需要向模板传递每个Campaign的最新状态
- 异常表现:自定义列表视图、Django Admin的Campaign列表页均存在状态缓存问题,新状态生成后不会即时更新;但Admin的
CampaignStatus列表页正常,新记录可即时展示 - 预期缓存时长为5秒,实际缓存生效时长约3分钟
当前视图代码如下:
@method_decorator(cache_page(5), name="dispatch") # single arg is seconds class CampaignsListView(LoginRequiredMixin, ListView): model = Campaign paginate_by = 100 template_name = "writing/user_campaigns.html" context_object_name = "user_campaigns" def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) for i, _ in enumerate(context.get("user_campaigns")): campaign = context["user_campaigns"][i] campaign_status = CampaignStatus.objects.filter(campaign=campaign).latest("-status") context["user_campaigns"][i].status = campaign_status.get_status_display() return context def get_queryset(self): return Campaign.objects.filter(user=self.request.user).order_by("-modified") #... some other methods too
修复步骤
1. 修正缓存时长不符合预期的问题
cache_page装饰器传入的缓存时长会被全局缓存配置覆盖,优先检查settings.py中的两项配置:
- 若开启了
UpdateCacheMiddleware和FetchFromCacheMiddleware站点缓存中间件,全局配置项CACHE_MIDDLEWARE_SECONDS会覆盖视图上设置的5秒缓存,默认值为600秒,若之前手动设置为180秒就会出现3分钟缓存的现象。如果不需要全局站点缓存,直接移除这两个中间件,仅给需要缓存的视图单独加装饰器,可控性更高;如果需要保留全局缓存,将该值调整为5,或给目标视图单独设置缓存优先级。 - 若使用Redis、Memcached等第三方缓存后端,检查后端配置中是否设置了全局默认TTL,覆盖Django传入的过期时间。
另外当前cache_page默认按URL生成缓存key,会导致不同登录用户访问同一路径时串数据,需要加用户维度的key前缀:
from django.utils.decorators import method_decorator from django.views.decorators.cache import cache_page def get_user_cache_prefix(request, *args, **kwargs): return f"campaign_list_{request.user.id}" @method_decorator(cache_page(5, key_prefix=get_user_cache_prefix), name="dispatch") class CampaignsListView(LoginRequiredMixin, ListView): # 原有代码逻辑
2. 解决状态数据不刷新的问题
当前代码在get_context_data中循环查询每个Campaign的最新状态,既存在N+1查询性能问题,动态给模型实例绑定属性的写法也会在QuerySet缓存命中时跳过数据库查询,导致状态数据不更新。更合理的实现是在查询集阶段通过注解预加载最新状态,从根源避免该问题:
from django.db.models import OuterRef, Subquery def get_queryset(self): # 子查询取每个Campaign关联的最新状态记录,按状态创建时间倒序取第一条 latest_status = CampaignStatus.objects.filter( campaign=OuterRef("pk") ).order_by("-created_at").values("status")[:1] return Campaign.objects.filter( user=self.request.user ).annotate( current_status=Subquery(latest_status) ).order_by("-modified")
对应简化get_context_data逻辑,无需额外查库:
def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) # 直接读取模型定义的状态choices映射 status_choices = dict(CampaignStatus._meta.get_field("status").choices) for campaign in context["user_campaigns"]: campaign.status = status_choices.get(campaign.current_status, "") return context
注意:原有代码中
CampaignStatus.objects.filter(campaign=campaign).latest("-status")存在逻辑错误:latest()方法不需要在字段名前加负号,且取最新状态应该按状态记录的创建时间字段排序,而非status字段本身,否则无法拿到最新生成的流转记录。
3. 修复Admin页面缓存异常
Django Admin默认不会开启页面缓存,若Admin列表也出现3分钟缓存,说明全局缓存中间件对Admin路径生效。可以在缓存规则中配置排除/admin/路径,或者给Admin视图单独设置短缓存/关闭缓存,不建议对Admin后台做长缓存。
内容的提问来源于stack exchange,提问作者John
相关产品推荐
相关产品推荐

