【问题标题】:Django - Unique Collection of Foreign Keys ConstraintDjango - 外键约束的唯一集合
【发布时间】:2021-09-30 04:01:25
【问题描述】:

我有两个这样的模型:

class Group(...):
    pass

class Identifier(...):
    value = models.CharField(...)
    group = models.ForeignKey('Group', ..., related_named = 'identifiers')

我该怎么做:

  1. Group 限制为最多只有4 个Identifiers
  2. 确保最多 4 个Identifiers(标识符的值)的任意组合在所有Groups 中都是唯一的?

对于第 2 部分,这里是扁平化 Groups 表的示例:

row | id__0__val | id__1__val | id__2__val | id__3__val 
--- | ---------- | ---------- | ---------- | ----------
  0 |       abc  |        123 |        xyz |        456
  1 |       abc  |        123 |        xyz |          -   <-- valid (nulls are okay)
  2 |       123  |        abc |        xyz |        456   <-- invalid (same combo as row 0)   

以前我尝试过(类似的)这个,但它看起来很乱,功能有限,我不确定它是否能工作:

class Group(...):
    id__0 = models.OneToOneField('Identifier', blank = True, null = True, ...)
    id__1 = models.OneToOneField('Identifier', blank = True, null = True, ...)
    id__2 = models.OneToOneField('Identifier', blank = True, null = True, ...)
    id__3 = models.OneToOneField('Identifier', blank = True, null = True, ...)

    class Meta: 
        unique_together = ('id__0__value', 'id__1__value', 'id__2__value', 'id__3__value')

处理这种约束的更好方法是什么?

【问题讨论】:

  • 我不确定它是否适用于反向关系(或像 count 这样的聚合),但您是否尝试过使用 Check 约束(参见 docs.djangoproject.com/en/3.2/ref/models/constraints/…)?您也没有指定它,但是如果您使用的是 Postgres,您可能会摆脱 ArrayField 而不是以硬编码的方式枚举关系,即使它也不是很干净? (docs.djangoproject.com/en/3.2/ref/contrib/postgres/fields/…)
  • 不,我以前没有见过检查约束,是的,我正在使用 Postgres - ArrayField 如何解决其中一些问题?
  • 您可以限制 ArrayField 的大小(即限制为 4 个项目),并且您可以对其执行检查。但缺点是:它不能真正保存相关字段引用,并且最终会出现弱匹配。最后,我认为这个缺点是相当严重的。可悲的是,Django 似乎不允许处理关系列表上的唯一性。顺便提一句。我认为您的第二个解决方案也可以,因为 Postgres 将 NULL 视为不同的值(虽然不是空白,所以 blank=False 应该是更好的 IMO)
  • 您需要数据库来强制执行此约束吗?还是仅在 Python 中处理验证就足够了?

标签: python django django-models


【解决方案1】:

可以通过validate_unique方法实现:

class Group(models.Model):
    ...

    def validate_unique(self, exclude=None):
        qs = Identifier.objects.filter(group_id=self.id)
        # restrict a Group to only have at most 4 Identifiers
        if qs.count() >= 4:
            raise ValidationError("group already has 4 Identifiers")
        # Ensure that any combination of up to 4 Identifiers is unique across all Groups
        values = []
        for group in Group.objects.all():
            values.append([i.value for i in group.identifiers])
        current_values = [i.value for i in self.identifiers]
        # https://stackoverflow.com/questions/22483730/python-check-if-list-of-lists-of-lists-contains-a-specific-list
        if current_values in values:
            raise ValidationError("group with these identifiers already exists")

        

这是伪代码,所以可能不起作用 - 但你明白了 :)

【讨论】:

  • 听起来不错,但它不会在正确的时刻被调用。它将在组保存时被调用,如果它应该在标识符保存上(因为它拥有引用,它将是被保存的那个)。也许从标识符 clean 方法调用 self.group.validate_unique() 可以做到,但我不确定此时列表是否是最新的(因为标识符尚未保存)
【解决方案2】:

我对此的看法,但通过clean 方法进行:

class Group(models.Model):
    MAX_IDENTIFIERS_PER_GROUP = 4

class Identifier(models.Model):
    value = models.CharField()
    group = models.ForeignKey('Group', related_named='identifiers')

    def clean(self):
        identifiers = self.group.identifiers.values_list('value', flat=True)

        # If a new identifier is being created, but the current group already has identifiers == MAX_IDENTIFIERS_PER_GROUP
        if not self.pk and len(identifiers) == Group.MAX_IDENTIFIERS_PER_GROUP:
            raise ValidationError(f'Cannot have more than {Group.MAX_IDENTIFIERS_PER_GROUP} identifiers for group {self.group.pk}')

        # If there is another group with the same values in identifiers as the current identifier's group
        conflicting_identifier = Identifier.objects
            .filter(value__in=set(identifiers + [self.value, ]))
            .exclude(group=self.group)
            .values('group')
            .annotate(value_count=Count('value'))
            .filter(value_count__gte=Group.MAX_IDENTIFIERS_PER_GROUP).first()

        if conflicting_identifier:
            raise ValidationError(f'Current identifier combination is already present in group {conflicting_identifier.group.pk}')

请注意,clean 不会在 save 中自动运行,因此需要手动使用。 (尚未对此进行测试,但要点应该很清楚;))

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-01-06
    • 1970-01-01
    • 2017-04-20
    • 2019-09-25
    • 1970-01-01
    • 2011-05-28
    相关资源
    最近更新 更多