在 Django 中通过子 class 验证父模型外键
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
约束可能有用,但我没弄清楚如何在此处应用它。
您可以使用 CheckConstraint:
# models.py
from django.db import models
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 Meta:
constraints = [
CheckConstraint(
check=Q(location__submarine__is_null=False),
name='hatches_must_be_submarine_exits'
),
]
class House(Location):
height = models.FloatField()
class Door(Exit):
width = models.FloatField()
height = models.FloatField()
class Meta:
constraints = [
CheckConstraint(
check=Q(location__house__is_null=False),
name='doors_must_be_house_exits'
),
]
编辑:
一种更 DRY 的方法是在 python 级别而不是数据库级别进行验证。您可以像这样使用一个 clean-method:
# 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__'
不幸的是,Django 在继承方面不是很好,因为每个 child 仍然需要自己的数据库 table。所以即使你可以完成这项工作,它看起来也不会很好,也不会帮助你走下去。最简单和最 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")
我添加了 Meta
和 abstract = True
因为我的直觉是你不会想要任何普通的 Location
和 Exit
objects数据库,但我可能错了; Meta.abstract
告诉 Django 你不需要 DB tables 用于抽象 parent 模型。重复的 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)
假设我的 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
约束可能有用,但我没弄清楚如何在此处应用它。
您可以使用 CheckConstraint:
# models.py
from django.db import models
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 Meta:
constraints = [
CheckConstraint(
check=Q(location__submarine__is_null=False),
name='hatches_must_be_submarine_exits'
),
]
class House(Location):
height = models.FloatField()
class Door(Exit):
width = models.FloatField()
height = models.FloatField()
class Meta:
constraints = [
CheckConstraint(
check=Q(location__house__is_null=False),
name='doors_must_be_house_exits'
),
]
编辑: 一种更 DRY 的方法是在 python 级别而不是数据库级别进行验证。您可以像这样使用一个 clean-method:
# 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__'
不幸的是,Django 在继承方面不是很好,因为每个 child 仍然需要自己的数据库 table。所以即使你可以完成这项工作,它看起来也不会很好,也不会帮助你走下去。最简单和最 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")
我添加了 Meta
和 abstract = True
因为我的直觉是你不会想要任何普通的 Location
和 Exit
objects数据库,但我可能错了; Meta.abstract
告诉 Django 你不需要 DB tables 用于抽象 parent 模型。重复的 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)