[英]Validate parent model foreign key by child class in Django
假设我的 Django 应用程序中有以下父模型:
class Location(models.Model):
name = models.CharField(max_length=100)
class Exit(models.Model):
location = models.ForeignKey(Location, on_delete=models.CASCADE, related_name="exits")
closed = models.BooleanField()
以及两对对应的子模型:
class Submarine(Location):
size = models.FloatField()
class Hatch(Exit):
diameter = models.FloatField()
class House(Location):
height = models.FloatField()
class Door(Exit):
width = models.FloatField()
height = models.FloatField()
在此设置中, House
可以有一个Hatch
作为其Exit
之一,而Submarine
也可以有一个Door
。 有没有办法明确防止这种情况发生? 理想情况下,我希望在尝试设置无效外键时抛出异常。
将location
字段从Exit
移动到Hatch
和Door
不是一种选择,因为我希望能够使用如下结构:
open_locations = Location.objects.filter(exits__closed=False)
并避免重复(即为House
和Submarine
编写单独的函数)。
也许limit_choices_to
约束可能会有所帮助,但我没有设法弄清楚如何在这里应用它。
编辑 2:删除了不正确的方法
编辑:更干燥的方法是在 python 级别而不是数据库级别进行验证。 您可以使用这样的一种清洁方法:
# models.py
from django.db import models
from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import ValidationError
class Location(models.Model):
name = models.CharField(max_length=100)
class Exit(models.Model):
location = models.ForeignKey(Location, on_delete=models.CASCADE, related_name="exits")
location_type = ContentType.objects.get_for_model(Location)
closed = models.BooleanField()
def clean(self):
if self.location is not None:
actual_type = ContentType.objects.get_for_model(self.location.__class__)
expected_type = self.__class__.location_type
if (
actual_type
is not expected_type
):
raise ValidationError(
message=f'location must be a {expected_type.name}, not a {actual_type.name}'
)
class Submarine(Location):
size = models.FloatField()
class Hatch(Exit):
location_type = ContentType.objects.get_for_model(Submarine)
diameter = models.FloatField()
class House(Location):
height = models.FloatField()
class Door(Exit):
location_type = ContentType.objects.get_for_model(House)
width = models.FloatField()
height = models.FloatField()
此外,您可以限制显示的选项,例如,在实例化表单时:
from django import forms
from my_app import models as my_models
class ExitForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
loc_model = self._meta.model.location_type.model_class()
self.fields['location'].choices = loc_model.objects.values_list('location__pk', 'name')
class Meta:
model = my_models.Exit
fields = '__all__'
不幸的是,Django 与 inheritance 并不一致,因为每个孩子仍然需要自己的数据库表。 因此,即使您可以完成这项工作,它看起来也不会很好,也不会帮助您。 最简单和最 Djangesque 的方式是
class Location(models.Model):
name = models.CharField(max_length=100)
class Meta:
abstract = True
class Exit(models.Model):
closed = models.BooleanField()
class Meta:
abstract = True
class Submarine(Location):
size = models.FloatField()
class Hatch(Exit):
diameter = models.FloatField()
location = models.ForeignKey(Submarine, on_delete=models.CASCADE, related_name="exits")
class House(Location):
height = models.FloatField()
class Door(Exit):
width = models.FloatField()
height = models.FloatField()
location = models.ForeignKey(House, on_delete=models.CASCADE, related_name="exits")
我添加了带有abstract = True
的Meta
因为我的直觉是你不希望在数据库中有任何普通的Location
和Exit
对象,但我可能是错的; Meta.abstract
告诉 Django 您不需要抽象父模型的数据库表。 重复的Location
线是不幸的,但如果有很多这样的模型,你最好使用工厂而不是inheritance。
看起来像这样:
class Exit(models.Model):
closed = models.BooleanField()
class Meta:
abstract = True
def location_field_factory(exit_type):
assert isinstance(exit_type, Exit)
return models.ForeignKey(exit_type, on_delete=models.CASCADE, related_name="exits")
class Barrel(Location):
diameter = models.FloatField()
height = models.FloatField()
class Lid(Exit):
diameter = models.FloatField()
location = Exit.location_field_factory(Barrel)
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.