【问题标题】:Validate parent model foreign key by child class in Django在Django中通过子类验证父模型外键
【发布时间】:2022-07-25 02:20:04
【问题描述】:

假设我的 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 约束可能会有所帮助,但我不知道如何在此处应用它。

【问题讨论】:

    标签: python django validation model


    【解决方案1】:

    不幸的是,Django 不擅长继承,因为每个孩子仍然需要自己的数据库表。因此,即使您可以完成这项工作,它看起来也不会很好,也不会帮助您。最简单和最 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")
    

    我添加了Metaabstract = True,因为我的直觉是你不会想在数据库中有任何简单的LocationExit 对象,但我可能是错的; Meta.abstract 告诉 Django 你不需要抽象父模型的数据库表。重复的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】:

      编辑 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()
      

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

      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 不允许 CheckConstraints 中的连接字段引用,正如 here 指出的那样,这会导致第一种方法不正确(导致 FieldError on migrate)。
      猜你喜欢
      • 1970-01-01
      • 2013-09-23
      • 1970-01-01
      • 1970-01-01
      • 2018-02-03
      • 2022-12-15
      • 1970-01-01
      • 1970-01-01
      • 2015-12-09
      相关资源
      最近更新 更多