如何用外键过滤Django场地预订记录?遇RelatedObjectDoesNotExist报错
足球场地预订系统时间冲突校验问题解决方案
问题描述
我开发了一款足球场地预订系统,用户登录后选择目标场地、预订日期及时长完成下单,核心需求是避免同一场地在同一日期的同一时段被重复预订。尝试在OpeningHours模型的clean方法中添加外键pitche过滤条件来排查冲突时,触发报错:RelatedObjectDoesNotExist: OpeningHours has no pitche。移除pitche=self.pitche条件后,过滤逻辑会校验所有场地的时间冲突,不符合业务需求。
# 报错的过滤逻辑 filter = OpeningHours.objects.filter(made_on=self.made_on,pitche=self.pitche,period = self.period).filter(Q(from_hour__range=in_range) | Q(to_hour__range=in_range)).exists()
现有代码
模型代码
from django.db import models from django.contrib.auth.models import User from django.core.exceptions import ValidationError from django.db.models import Q import datetime class Pitche(models.Model): Name = models.CharField(max_length=50) city = models.CharField(max_length=50) photo = models.ImageField(upload_to='photos') price = models.DecimalField(max_digits=5,decimal_places=2) location = models.CharField(max_length=500) name_of_supervisor = models.CharField(max_length=50) phone_of_supervisor = models.CharField(max_length=11) manager = models.ForeignKey(Manager,on_delete=models.DO_NOTHING) class OpeningHours(models.Model): user = models.ForeignKey(User,on_delete=models.CASCADE) pitche = models.ForeignKey(Pitche,on_delete=models.DO_NOTHING) made_on = models.DateField() period = models.CharField(max_length=50,choices=period) from_hour = models.TimeField() to_hour = models.TimeField() timing = models.CharField(max_length=50,choices=timing) def __str__(self): return f"{self.pitche}-{self.from_hour} to {self.to_hour}" def clean(self): if self.from_hour == self.to_hour: raise ValidationError("Wrong Time") if self.made_on < datetime.date.today(): raise ValidationError("InVaild Date") ##################### in_range = (self.from_hour, self.to_hour) filter = OpeningHours.objects.filter(made_on=self.made_on,period = self.period).filter(Q(from_hour__range=in_range) | Q(to_hour__range=in_range)).exists() if filter: raise ValidationError(f"The booked time ({self.from_hour} to {self.to_hour}) is occupied.") return super().clean()
视图代码
from django.shortcuts import render, get_object_or_404 from django.contrib import messages from .models import Pitche, OpeningHours from .forms import RestaurantForm def pitche_page(request,id): if request.method == 'POST' and request.user.is_authenticated : pit_id = Pitche.objects.get(id =id ) add_date = RestaurantForm(request.POST) if add_date.is_valid(): info = add_date.save(commit=False) login_user = User.objects.get(username=request.user.username) info.user = login_user info.pitche = pit_id info.save() messages.success(request,"Booking Successful") else: messages.error(request,"this time is booked befor") context ={ 'pit':get_object_or_404(Pitche,id=id), 'form':RestaurantForm(), } return render(request,'pitches/pitche.html',context)
问题原因
视图中add_date.is_valid()表单验证执行时,info.pitche = pit_id这一步还未执行——pitche字段是在表单验证通过后才绑定到OpeningHours实例上的。因此clean方法运行时,当前实例的pitche外键还未赋值,导致触发RelatedObjectDoesNotExist错误。
解决方案
1. 提前绑定pitche到表单实例
修改视图代码,在调用is_valid()之前就把pitche和user绑定到表单实例上,确保clean方法执行时能获取到pitche字段的值:
def pitche_page(request,id): if request.method == 'POST' and request.user.is_authenticated : pit_id = get_object_or_404(Pitche, id=id) add_date = RestaurantForm(request.POST) # 提前绑定pitche和user到表单实例 add_date.instance.pitche = pit_id add_date.instance.user = request.user if add_date.is_valid(): add_date.save() # 无需再用commit=False,已完成字段赋值 messages.success(request,"预订成功") else: messages.error(request,"该时间段已被预订") context ={ 'pit':get_object_or_404(Pitche,id=id), 'form':RestaurantForm(), } return render(request,'pitches/pitche.html',context)
2. 修正clean方法的过滤逻辑
在clean方法中添加pitche=self.pitche条件,同时增加排除当前实例的逻辑(处理更新预订的场景):
def clean(self): # 基础校验 if self.from_hour == self.to_hour: raise ValidationError("时间起止不能相同") if self.made_on < datetime.date.today(): raise ValidationError("预订日期不能早于今天") # 构建时间冲突查询 in_range = (self.from_hour, self.to_hour) conflict_qs = OpeningHours.objects.filter( made_on=self.made_on, pitche=self.pitche, period=self.period ).filter( Q(from_hour__range=in_range) | Q(to_hour__range=in_range) ) # 如果是更新操作,排除当前实例本身 if self.pk: conflict_qs = conflict_qs.exclude(pk=self.pk) if conflict_qs.exists(): raise ValidationError(f"时间段 {self.from_hour} 到 {self.to_hour} 已被占用") return super().clean()
额外优化点
- 视图中无需通过
User.objects.get(username=request.user.username)获取用户,直接使用request.user即可(用户已登录)。 - 使用
get_object_or_404替代Pitche.objects.get(id=id),避免因场地ID不存在触发异常。
内容的提问来源于stack exchange,提问作者Osama meky
相关产品推荐
相关产品推荐

