假设我的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
s之一,Submarine
也可能具有Door
。是否有一种方法可以明确地防止这种情况发生?理想情况下,我希望在尝试设置无效外键时抛出异常。
将location
字段从Exit
移动到Hatch
和Door
不是一个选项,因为我希望能够使用如下结构:
open_locations = Location.objects.filter(exits__closed=False)
和避免重复(即为House
s和Submarine
s编写单独的函数)。
也许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")
我已经添加了Meta
与abstract = True
,因为我的直觉是你不会想要在数据库中有任何普通的Location
和Exit
对象,但我可能是错的;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__'