使用两个可选但一个必需的外键创建模型


9

我的问题是我有一个模型,可以使用两个外键之一来说明它是哪种模型。我希望它至少要拿一个,但不能两个都拿。我可以让它仍然是一个模型还是应该将其分为两种类型。这是代码:

class Inspection(models.Model):
    InspectionID = models.AutoField(primary_key=True, unique=True)
    GroupID = models.ForeignKey('PartGroup', on_delete=models.CASCADE, null=True, unique=True)
    SiteID = models.ForeignKey('Site', on_delete=models.CASCADE, null=True, unique=True)

    @classmethod
    def create(cls, groupid, siteid):
        inspection = cls(GroupID = groupid, SiteID = siteid)
        return inspection

    def __str__(self):
        return str(self.InspectionID)

class InspectionReport(models.Model):
    ReportID = models.AutoField(primary_key=True, unique=True)
    InspectionID = models.ForeignKey('Inspection', on_delete=models.CASCADE, null=True)
    Date = models.DateField(auto_now=False, auto_now_add=False, null=True)
    Comment = models.CharField(max_length=255, blank=True)
    Signature = models.CharField(max_length=255, blank=True)

问题是Inspection模型。这应该链接到组或站点,但不能链接到两者。目前,通过此设置,它同时需要两者。

我宁愿没有拆分此为两个几乎相同的车型GroupInspection,并SiteInspection,因此,保持它作为一个模型中的任何解决方案将是理想的。


也许在这里使用子类更好。你可以做一个Inspection类,然后将子类SiteInspection,并GroupInspection针对中的常见部位。
Willem Van Onsem

可能不相关,但是unique=TrueFK字段中的部分意味着Inspection一个给定GroupID或一个实例只能存在一个SiteID实例-IOW,这是一对一关系,而不是一对多关系。这真的是您想要的吗?
bruno desthuilliers

“目前,通过这种设置,它既需要兼顾。” =>从技术上讲,它不是-在数据库级别,您既可以设置两个键,也可以都不设置(具有上述注意事项)。仅当使用ModelForm(直接或通过django admin)时,这些字段才会被标记为必填字段,这是因为您没有传递'blank = True'参数。
bruno desthuilliers

@brunodesthuilliers是的,这个想法是InspectionGroupor Site和an 之间建立链接InspectionID,然后我可以InspectionReport针对该一种关系进行多次“检查” 。这样做是为了让我可以更轻松地对Date与一个GroupSite。相关的所有记录进行排序。希望是有道理的
高美

@ Cm0295恐怕看不到此间接级别的要点-将组/站点FK直接放入InspectionReport会产生完全相同的服务AFAICT-通过适当的密钥过滤您的InspectionReports(或仅遵循Site或反向描述符分组),然后按日期对它们进行排序即可。
bruno desthuilliers

Answers:


5

我建议您以Django方式进行验证

通过覆盖cleanDjango模型的方法

class Inspection(models.Model):
    ...

    def clean(self):
        if <<<your condition>>>:
            raise ValidationError({
                    '<<<field_name>>>': _('Reason for validation error...etc'),
                })
        ...
    ...

但是请注意,与Model.full_clean()一样,调用模型的save()方法时不会调用模型的clean()方法。需要手动调用它以验证模型的数据,或者您可以覆盖模型的save方法,使其始终在触发Model类save方法之前调用clean()方法。


另一个可能有用的解决方案是使用GenericRelations,以提供与多个表相关的多态字段,但是如果这些表/对象可以从一开始就在系统设计中互换使用,则可能是这种情况。


2

正如评论中提到的,“进行此设置同时需要两个”的原因只是您忘记将blank=TrueFK字段添加到FK字段中,因此您的ModelForm(自定义的或由管理员生成的默认值)将使表单字段成为必填项。在数据库模式级别,您可以同时填充这两个FK之一,也可以不填充任何FK,这是可以的,因为您使这些db字段可为空(带有null=True参数)。

另外,(请参阅我的其他评论),您可能想检查一下您真正想让FK具有唯一性。从技术上讲,这将您的一对多关系变成一对一关系-您只允许给定GroupID或SiteId的一个“检查”记录(对于一个GroupId或SiteId,不能有两个或多个“检查”记录) 。如果这确实是您想要的,则可能需要使用显式的OneToOneField(数据库模式将相同,但模型将更显式,并且相关的描述符在该用例中更有用)。

附带说明:在Django模型中,ForeignKey字段实现为相关的模型实例,而不是原始ID。IOW,鉴于此:

class Foo(models.Model):
    name = models.TextField()

class Bar(models.Model):
    foo = models.ForeignKey(Foo)


foo = Foo.objects.create(name="foo")
bar = Bar.objects.create(foo=foo)

然后bar.foo会解决foo,而不是foo.id。因此,您当然希望将InspectionIDand SiteID字段重命名 为适当的inspectionsite。顺便说一句,在Python中,除类名和伪常量外,其他任何东西的命名约定都是'all_lower_with_underscores'。

现在,您的核心问题是:在数据库级别没有强制执行“一个或另一个”约束的特定标准SQL方法,因此通常使用CHECK约束来完成,该约束在Django模型中使用模型的元“约束”来完成。选项

这是说,限制实际上是如何支持和执行的分贝级别取决于您的数据库供应商(MySQL的<8.0.16只是简单的忽略它们为例),你将在这里需要的那种约束不会在形式强制执行或模型级验证,仅当尝试保存模型时,因此,您希望在(分别)模型或窗体方法的两种情况下(最好)在模型级或表单级验证中添加验证clean()

因此,总而言之:

  • 首先,仔细检查您是否确实需要此unique=True约束,如果是,则用OneToOneField替换FK字段。

  • blank=True向您的FK(或OneToOne)字段添加arg

  • 在模型的元数据中添加适当的检查约束 -文档简洁明了,但如果您知道要使用ORM进行复杂的查询(并且如果不是,则该学习的时候就足够了;-))仍然足够明确
  • clean()向您的模型添加一种方法来检查您是否拥有一个或另一个字段,并引发验证错误,否则

假设您的RDBMS遵守检查约束,那么您应该没事。

只需注意,使用这种设计,您的Inspection模型是完全无用的(但代价很高!)间接寻址-通过将FK(以及约束,验证等)直接移入,您将以较低的成本获得完全相同的功能InspectionReport

现在可能还有另一种解决方案-保留“检查”模型,但将FK作为OneToOneField放在关系的另一端(在“站点”和“组”中):

class Inspection(models.Model):
    id = models.AutoField(primary_key=True) # a pk is always unique !

class InspectionReport(models.Model):
    # you actually don't need to manually specify a PK field,
    # Django will provide one for you if you don't
    # id = models.AutoField(primary_key=True)

    inspection = ForeignKey(Inspection, ...)
    date = models.DateField(null=True) # you should have a default then
    comment = models.CharField(max_length=255, blank=True default="")
    signature = models.CharField(max_length=255, blank=True, default="")


class Group(models.Model):
    inspection = models.OneToOneField(Inspection, null=True, blank=True)

class Site(models.Model):
    inspection = models.OneToOneField(Inspection, null=True, blank=True)

然后,您可以使用来获取给定站点或组的所有报告yoursite.inspection.inspectionreport_set.all()

这避免了必须添加任何特定的约束或验证,但要付出额外的间接级别(SQL join子句等)的代价。

这些解决方案中哪一个是“最佳”的,实际上取决于上下文,因此您必须了解两者的含义,并检查通常如何使用模型来找出哪种模型更适合自己的需求。就我而言,在没有更多上下文(或不确定性)的情况下,我宁愿使用具有较少间接级别(YMMV)的解决方案。

注意:关于通用关系:当您确实有很多可能的相关模型和/或事先不知道要与您自己关联的模型时,这些便会很方便。这对于可重用的应用程序(如“评论”或“标签”等功能)或可扩展的应用程序(内容管理框架等)特别有用。缺点是,它会使查询变得更加繁重(当您想对数据库执行手动查询时,这是不切实际的)。从经验来看,它们可以迅速成为PITA机器人,其中包含代码和性能,因此在没有更好的解决方案时(和/或在维护和运行时开销不成问题的情况下),最好保留它们。

我的2美分。


2

Django具有一个用于创建数据库约束的新接口(自2.2开始):https : //docs.djangoproject.com/zh-CN/3.0/ref/models/constraints/

您可以使用CheckConstraint强制一个非空值。为了清楚起见,我使用两个:

class Inspection(models.Model):
    InspectionID = models.AutoField(primary_key=True, unique=True)
    GroupID = models.OneToOneField('PartGroup', on_delete=models.CASCADE, blank=True, null=True)
    SiteID = models.OneToOneField('Site', on_delete=models.CASCADE, blank=True, null=True)

    class Meta:
        constraints = [
            models.CheckConstraint(
                check=~Q(SiteID=None) | ~Q(GroupId=None),
                name='at_least_1_non_null'),
            ),
            models.CheckConstraint(
                check=Q(SiteID=None) | Q(GroupId=None),
                name='at_least_1_null'),
            ),
        ]

这只会在数据库级别强制执行约束。您将需要手动验证表单或序列化器中的输入。

另外,您可能应该使用OneToOneField代替ForeignKey(unique=True)。您还会想要blank=True


0

我认为您是在谈论通用关系文档。您的答案与类似。

前一段时间,我需要使用通用关系,但是我在书中读到了其他地方,应该避免使用这种用法,我认为这是Django的Two Scoops。

我最终创建了一个这样的模型:

class GroupInspection(models.Model):
    InspectionID = models.ForeignKey..
    GroupID = models.ForeignKey..

class SiteInspection(models.Model):
    InspectionID = models.ForeignKey..
    SiteID = models.ForeignKey..

我不确定这是否是一个很好的解决方案,并且正如您提到的那样,您宁愿不使用它,但是在我的情况下这是可行的。


“我在书中和其他地方读书”是关于做某事(或避免做某事)的最糟糕的原因。
bruno desthuilliers

@brunodesthuilliers我认为《 Django的两个独家新闻》是一本好书。
Luis Silva

不能说,我还没看过。但这无关紧要:我的意思是,如果您不理解书中为什么这么说,那么这不是知识,也不是经验,而是宗教信仰。我不介意宗教信仰,但是在CS中却没有地位。您要么了解某项功能的优缺点,然后就可以判断它在给定的上下文中是否合适,要么您不了解它,然后就不应该盲目模仿已经阅读的内容。通用关系有一个非常有效的用例,重点不是完全避免它们,而是知道何时避免它们。
bruno desthuilliers

注意:我完全理解一个人不能完全了解CS-在某些领域,除了信任一本书,我别无选择。但是,那时我可能不会回答有关该主题的问题;-)
bruno desthuilliers

0

回答您的问题可能为时已晚,但我认为我的解决方案可能适合其他人的情况。

我将创建一个新模型,我们称之为Dependency,并在该模型中应用逻辑。

class Dependency(models.Model):
    Group = models.ForeignKey('PartGroup', on_delete=models.CASCADE, null=True, unique=True)
    Site = models.ForeignKey('Site', on_delete=models.CASCADE, null=True, unique=True)

然后,我将非常明确地编写适用的逻辑。

class Dependency(models.Model):
    group = models.ForeignKey('PartGroup', on_delete=models.CASCADE, null=True, unique=True)
    site = models.ForeignKey('Site', on_delete=models.CASCADE, null=True, unique=True)

    _is_from_custom_logic = False

    @classmethod
    def create_dependency_object(cls, group=None, site=None):
        # you can apply any conditions here and prioritize the provided args
        cls._is_from_custom_logic = True
        if group:
            _new = cls.objects.create(group=group)
        elif site:
            _new = cls.objects.create(site=site)
        else:
            raise ValueError('')
        return _new

    def save(self, *args, **kwargs):
        if not self._is_from_custom_logic:
            raise Exception('')
        return super().save(*args, **kwargs)

现在,您只需要为模型创建一个ForeignKey即可Inspection

view函数中,您需要创建一个Dependency对象,然后将其分配给您的Inspection记录。确保create_dependency_objectview功能中使用。

这几乎可以使您的代码显式且可证明错误。可以非常容易地绕过执法。但是关键是它需要绕过此确切限制的先验知识。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.