在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作为其Exits之一,Submarine也可能具有Door。是否有一种方法可以明确地防止这种情况发生?理想情况下,我希望在尝试设置无效外键时抛出异常。

location字段从Exit移动到HatchDoor不是一个选项,因为我希望能够使用如下结构:

open_locations = Location.objects.filter(exits__closed=False)

和避免重复(即为Houses和Submarines编写单独的函数)。

也许limit_choices_to约束可能有帮助,但我没有设法弄清楚如何在这里应用它。

不幸的是,Django在继承方面不是很好,因为每个子节点仍然需要自己的数据库表。所以即使你能做到这一点,它也不会看起来很好,对你的未来也没有帮助。最简单和最有姜格风格的方法是

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")

我已经添加了Metaabstract = True,因为我的直觉是你不会想要在数据库中有任何普通的LocationExit对象,但我可能是错的;Meta.abstract告诉Django抽象父模型不需要DB表。重复的Location行是不幸的,但如果有许多这样的模型,您最好使用工厂而不是继承。

就像这样:

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)

编辑2:删除不正确的方法

编辑:更DRY的方法是在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()

此外,您可以限制显示的选项,例如,在实例化Form时:

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__'

相关内容

  • 没有找到相关文章

最新更新