繁体   English   中英

通过 Django 中的子 class 验证父 model 外键

[英]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移动到HatchDoor不是一种选择,因为我希望能够使用如下结构:

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

并避免重复(即为HouseSubmarine编写单独的函数)。

也许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 = TrueMeta因为我的直觉是你不希望在数据库中有任何普通的LocationExit对象,但我可能是错的; 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.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM