【问题标题】:Django - validate unique for a calculated field in the Model and also in the ModelFormDjango - 验证模型和模型表单中计算字段的唯一性
【发布时间】:2018-10-15 04:34:53
【问题描述】:

TL;DR 我的模型和表单都计算字段 number_as_char 的值。我可以避免双重工作,但在使用没有表单的模型时仍然检查唯一性吗?

我使用 Python 3 和 Django 1.11


我的模型如下所示:

class Account(models.Model):
    parent_account = models.ForeignKey(
        to='self',
        on_delete=models.PROTECT,
        null=True,
        blank=True)
    number_suffix = models.PositiveIntegerField()
    number_as_char = models.CharField(
        max_length=100,
        blank=True,
        default='',
        unique=True)

    @classmethod
    def get_number_as_char(cls, parent_account, number_suffix):
        # iterate over all parents
        suffix_list = [str(number_suffix), ]
        parent = parent_account
        while parent is not None:
            suffix_list.insert(0, str(parent.number_suffix))
            parent = parent.parent_account

        return '-'.join(suffix_list)

    def save(self, *args, **kwargs):
        self.number_as_char = self.get_number_as_char(
            self.parent_account, self.number_suffix)
        super().save(*args, **kwargs)

number_as_char字段不应该由用户设置,因为它是根据选择的parent_account计算的:它是通过链接所有父帐户的字段number_suffix的值和当前实例。

这是一个包含三个帐户的示例:

ac1 = Account()
ac1.parent_account = None
ac1.number_suffix = 2
ac1.save()
# ac1.number_as_char is '2'

ac2 = Account()
ac2.parent_account = ac1
ac2.number_suffix = 5
ac2.save()
# ac2.number_as_char is '2-5'

ac3 = Account()
ac3.parent_account = ac2
ac3.number_suffix = 1
ac3.save()
# ac3.number_as_char is '2-5-1'

不是删除字段并改用模型属性的选项,因为我需要确保唯一性并使用该字段对带有order_by() 的查询集进行排序。


我的表格如下:

class AccountForm(forms.ModelForm):

    class Meta:
        model = Account
        fields = [
            'parent_account', 'number_suffix', 'number_as_char',
        ]
        widgets = {
            'number_as_char': forms.TextInput(attrs={'readonly': True}),
        }

    def clean(self):
        super().clean()
        self.cleaned_data['number_as_char'] = self.instance.get_number_as_char(
            self.cleaned_data['parent_account'], self.cleaned_data['number_suffix'])

我将number_as_char 包含在带有小部件属性readonly 的表单中,并使用表单clean() 方法计算number_as_char(必须在验证唯一性之前计算)。


这一切都有效(模型和表单),但在验证表单后,number_as_char 的值将由模型save() 方法再次计算。问题不大,但有没有办法避免这种双重计算?

  1. 如果我从表单clean() 方法中删除计算,则不会使用新值验证唯一性(它只会检查旧值)。
  2. 我不想从模型中完全删除计算,因为我在没有表单的其他部分使用模型。

您有什么建议可以采取不同的方式来避免重复计算字段吗?

【问题讨论】:

  • 根据您的模型构建方式,您可以通过检查 (parent, suffix) 对是否唯一来确保唯一性,但它可能会比您正在做的更昂贵。它不需要对number_as_char 进行双重计算,但我认为你不会赢太多,如果有的话。这是 django 的一个超级常见的头痛......
  • @LaurentS 像您建议的那样使用unique_together 也不是一个完整的解决方案,因为带有parent_account=None 的行可以重复(SQL NULL 不等于另一个SQL NULL)。有关该问题,请参阅 this question
  • 啊,抱歉,我错过了parent_account 可能是None 的事实...您能否设置一个所有没有父链接的帐户的“root”帐户?根据您的应用程序,这可能有助于使unique_together 工作?
  • @LaurentS 一个伪 root 帐户听起来有点复杂。我必须在显示列表时排除该列表以避免混淆最终用户。有可能,但听起来比我目前的解决方案更复杂。
  • 它对验证没有帮助,但是有什么理由反对将number_as_char 设为属性。因为它是你的模型是非规范化的。使用属性,您可以对其进行规范化并仅在需要时执行计算,而不是每次保存时。

标签: python django


【解决方案1】:

我看不出有什么办法可以在两个地方(save()clean())这样做,因为您也需要它来处理非基于表单的保存)。

但是,我可以为您的 get_number_as_char 方法提供两个效率改进:

  1. 将其设为cached_property,以便第二次调用它时,您只需返回一个缓存值并消除重复计算。显然,您需要注意不要在更新实例之前调用它之前,否则旧的number_as_char 将被缓存。这应该没问题,只要 get_number_as_char() 仅在保存/清理期间被调用。

  2. 根据您在上面提供的信息,您不必遍历所有祖先,而只需将 number_as_char 作为父级并附加到它。

以下内容包含两者:

@cached_property
def get_number_as_char(self, parent_account, number_suffix):
    number_as_char = str(number_suffix)
    if parent_account is not None:
        number_as_char = '{}-{}'.format(parent_account.number_as_char, number_as_char)

    return number_as_char

为确保缓存不会导致问题,您可以在完成保存后清除缓存值:

def save(self, *args, **kwargs):
    self.number_as_char = self.get_number_as_char(
        self.parent_account, self.number_suffix)
    super().save(*args, **kwargs)
    # Clear the cache, in case something edits this object again.
    del self.get_number_as_char

【讨论】:

  • 我不知道cached_property。谢谢,我会调查的。
  • 你说得对,我可以跳过所有祖先的迭代,只看第一个父母。
【解决方案2】:

我稍微修改了一下,我想我找到了更好的方法。

通过在模型表单的number_as_char 字段上使用disabled 属性,您可以完全忽略用户输入(并在一个步骤中禁用该字段)。

您的模型已经在 save 方法中计算了 number_as_char 属性。但是,如果唯一约束失败,那么您的管理 UI 将引发 500 错误。但是,您可以将字段计算移至 clean() 方法,而将 save() 方法保留原样。

所以完整的例子看起来类似于:

形式:

class AccountForm(forms.ModelForm):

    class Meta:
        model = Account
        fields = [
            'parent_account', 'number_suffix', 'number_as_char',
        ]
        widgets = {
            'number_as_char': forms.TextInput(attrs={'disabled': True}),
        }

型号:

class Account(models.Model):
    # ...

    def clean(self):
        self.number_as_char = self.get_number_as_char(
            self.parent_account, self.number_suffix
        )
        super().clean()

这样任何基于你的模型生成表单的东西都会抛出一个很好的验证错误(前提是它使用了内置的模型验证,模型表单就是这种情况)。

唯一的缺点是,如果你保存一个触发验证错误的模型,你会看到一个空字段而不是验证失败的值——但我想也有一些很好的方法来解决这个问题——如果我也找到解决方案,我会编辑我的答案。

【讨论】:

  • 听起来很有趣。我会尝试一下,并在本周末(赏金到期之前)回复您。
  • 您的想法并没有完全解决问题,但为我指明了正确的方向。我在我的最终解决方案中添加了一个答案(并将其标记为已接受),但赏金适合你。谢谢。
【解决方案3】:

在阅读了所有答案并深入研究文档后,我最终使用了以下内容:

  1. @samu 建议使用模型clean() 方法,@Laurent S 建议使用unique_together 用于(parent_account, number_suffix)。因为只使用unique_together 对我不起作用,因为parent_account 可以是null,所以我选择结合这两个想法:检查模型clean() 方法中现有的(parent_account, number_suffix) 组合。
  2. 作为结果,我从表单中删除了number_as_char,现在它只在save() 方法中计算。顺便说一句:感谢@solarissmoke 建议仅根据第一个父项计算它,而不是一直迭代到链的顶部。
  3. 另一个后果是我现在需要显式调用模型full_clean() 方法来验证在使用没有表单的模型时的唯一性(否则我将获得数据库IntegrityError),但我可以接受。李>

所以,现在我的模型看起来像这样:

class Account(models.Model):
    parent_account = models.ForeignKey(
        to='self',
        on_delete=models.PROTECT,
        null=True,
        blank=True)
    number_suffix = models.PositiveIntegerField()
    number_as_char = models.CharField(
        max_length=100,
        default='0',
        unique=True)

    def save(self, *args, **kwargs):
        if self.parent_account is not None:
            self.number_as_char = '{}-{}'.format(
                self.parent_account.number_as_char,
                self.number_suffix)
        else:
            self.number_as_char = str(self.number_suffix)
        super().save(*args, **kwargs)

    def clean(self):
        qs = self._meta.model.objects.exclude(pk=self.pk)
        qs = qs.filter(
            parent_account=self.parent_account,
            number_suffix=self.number_suffix)
        if qs.exists():
            raise ValidationError('... some message ...')

我的表单最终是这样的:

class AccountForm(forms.ModelForm):
    class Meta:
        model = Account
        fields = [
            'parent_account', 'number_suffix',
        ]

编辑

我会将自己的答案标记为已接受,因为没有一个建议完全适合我的需要。

但是,赏金转到 @samus 答案,因为使用 clean() 方法为我指明了正确的方向。

【讨论】:

  • 谢谢,恕我直言,这是更简单、更好的解决方案。对于这个用例来说,对模型本身进行清理完全有意义,这与将我带到这个 SO 问题的问题非常相似。
【解决方案4】:

另一种方法——虽然可能不太好——是使用Django signals。您可以发出一个pre_save 信号,该信号将为即将保存的实例上的number_as_char 字段设置正确的值。

这样您就不必在模型的 save() 方法或 ModelFormclean() 方法中完成它。

使用信号应确保任何使用 ORM 来操作您的数据的操作(这也应该意味着所有 ModelForms)都会触发您的信号。

这种方法的缺点是无法直接从代码中明确该属性是如何生成的。人们必须偶然发现信号定义才能发现它甚至在那里。如果你能忍受它,我会选择信号。

【讨论】:

  • 嗯...我不确定信号。据我了解,表单验证不会捕获我从信号中引发的任何ValidationError
  • 对,我忘了验证错误。您能否在原始问题中提供验证码?
  • 到目前为止,除了我的问题中显示的内容之外,我没有其他验证码;我感兴趣的验证是 number_as_char 字段是唯一的。
  • 我在您在原始问题中发布的代码中看不到任何验证。我假设您在表单中设置此字段,因为您的表单不会忽略用户在该字段上的输入。尝试对 ModelForm 上的 number_as_char 字段使用 disabled (docs.djangoproject.com/en/2.0/ref/forms/fields/#disabled) 属性。如果您的save() 方法已经进行了计算,那么通过设置它,您应该能够将计算该字段的责任完全推给模型。看看是否有帮助。如果是这样,我可以将其重新发布到单独的答案中。
  • TL;DR:如果您在 ModelForm 类上设置 disabled,则根本不必在表单类中设置 number_as_char 值。
猜你喜欢
  • 2019-12-24
  • 2016-10-22
  • 2010-12-10
  • 2014-05-12
  • 1970-01-01
  • 2014-12-28
  • 1970-01-01
  • 2013-08-27
相关资源
最近更新 更多