为啥 django 的 model.save() 不调用 full_clean()?

Posted

技术标签:

【中文标题】为啥 django 的 model.save() 不调用 full_clean()?【英文标题】:Why doesn't django's model.save() call full_clean()?为什么 django 的 model.save() 不调用 full_clean()? 【发布时间】:2011-05-25 09:37:37 【问题描述】:

我只是好奇是否有人知道 django 的 orm 是否有充分的理由不对模型调用“full_clean”,除非它被保存为模型表单的一部分。

请注意,当您调用模型的 save() 方法时,不会自动调用 full_clean()。当您想为自己手动创建的模型运行一步模型验证时,您需要手动调用它。 django's full clean doc

(注意:为 Django 1.6 更新了引用...以前的 django 文档也对 ModelForms 提出了警告。)

人们是否有充分的理由不想要这种行为?我认为如果您花时间向模型添加验证,您会希望每次保存模型时都运行验证。

我知道如何让一切正常工作,我只是在寻找解释。

【问题讨论】:

非常感谢您提出这个问题,它阻止了我更多时间用头撞墙。我创建了一个可以帮助其他人的 mixin。查看要点:gist.github.com/glarrain/5448253 我最后使用信号来捕获pre_save 钩子并在所有捕获的模型上执行full_clean 【参考方案1】:

如果您想始终确保模型验证,全局pre_save 信号可以很好地工作。但是,它会在当前版本(3.1.x)中遇到 Django 的身份验证问题,并可能导致您正在使用的其他应用程序的模型出现问题。

详细说明@Peter Shannon 的答案,此版本将仅验证您执行它的模块内的模型,跳过使用"raw" saves 的验证并将dispatch_uid 添加到avoid duplicate signals。

from django.db.models.signals import pre_save
import inspect
import sys

MODELS = [obj for name, obj in
    inspect.getmembers(sys.modules[__name__], inspect.isclass)]

def validate_model(sender, instance, **kwargs):
    if 'raw' in kwargs and not kwargs['raw']:
        if type(instance) in MODELS:
            instance.full_clean()

pre_save.connect(validate_model, dispatch_uid='validate_models')

【讨论】:

【参考方案2】:

调用full_clean 方法的最简单方法就是在你的模型中重写save 方法:

class YourModel(models.Model):
    ...  
    
    def save(self, *args, **kwargs):
        self.full_clean()
        return super(YourModel, self).save(*args, **kwargs)

【讨论】:

为什么这比使用信号更好(或更差)? 我发现这种方法有两个问题 1) 如果 ModelForm 的 full_clean() 将被调用两次:通过表单和保存 2) 如果表单排除某些字段,它们仍将被验证通过保存。 最好定义一个新的 clean_save() 方法,该方法调用 full_clean() 然后 save() 并在手动保存自定义模型时显式使用它?【参考方案3】:

评论@Alfred Huang 的回答和评论。可以通过在当前模块 (models.py) 中定义一个类列表并在 pre_save 钩子中检查它来将 pre_save 钩子锁定到应用程序:

CUSTOM_CLASSES = [obj for name, obj in
        inspect.getmembers(sys.modules[__name__])
        if inspect.isclass(obj)]

@receiver(pre_save)
def pre_save_handler(sender, instance, **kwargs):
    if type(instance) in CUSTOM_CLASSES:
        instance.full_clean()

【讨论】:

【参考方案4】:

我们可以将应用程序用作settings.py 中的INSTALLED_APPS 部分,而不是插入一段声明接收器的代码

INSTALLED_APPS = [
    # ...
    'django_fullclean',
    # your apps here,
]

在此之前,您可能需要使用 PyPI 安装 django-fullclean

pip install django-fullclean

【讨论】:

你为什么要pip install 一些包含 4 行代码的应用程序(检查 source code)而不是自己编写这些行? 另一个我自己没试过的库:github.com/danielgatis/django-smart-save【参考方案5】:

如果您有一个模型要确保至少有一个 FK 关系,并且您不想使用 null=False,因为这需要设置默认 FK(这将是垃圾数据),我最好的方法是想出的是添加自定义.clean().save() 方法。 .clean() 引发验证错误,.save() 调用 clean。这样,表单和其他调用代码、命令行和测试都可以强制执行完整性。没有这个,(AFAICT)就无法编写测试来确保模型与特定选择(非默认)的其他模型具有 FK 关系。

class Payer(models.Model):

    name = models.CharField(blank=True, max_length=100)
    # Nullable, but will enforce FK in clean/save:
    payer_group = models.ForeignKey(PayerGroup, null=True, blank=True,)

    def clean(self):
        # Ensure every Payer is in a PayerGroup (but only via forms)
        if not self.payer_group:
            raise ValidationError(
                'payer_group': 'Each Payer must belong to a PayerGroup.')

    def save(self, *args, **kwargs):
        self.full_clean()
        return super().save(*args, **kwargs)

    def __str__(self):
        return self.name

【讨论】:

【参考方案6】:

出于兼容性考虑,django内核中没有启用保存时自动清理。

如果我们正在开始一个新项目,并且希望模型上的默认 save 方法可以自动清理,我们可以在保存每个模型之前使用以下信号进行清理。

from django.dispatch import receiver
from django.db.models.signals import pre_save, post_save

@receiver(pre_save)
def pre_save_handler(sender, instance, *args, **kwargs):
    instance.full_clean()

【讨论】:

为什么这比覆盖某些 BaseModel(所有其他人将继承自)上的 save 方法先调用 full_clean,然后调用 super() 更好(或更差)? 我发现这种方法有两个问题 1) 如果 ModelForm 的 full_clean() 会被调用两次:通过表单和信号 2) 如果表单排除了某些字段,它们仍然会被验证通过信号。 @mehmet 所以你可以在pre_save_handler 中添加这些if send == somemodel, then exclude some fields 对于那些正在使用或考虑使用这种方法的人:请记住,这种方法不受 Django 官方支持,并且在可预见的将来也不会得到支持(请参阅 Django 错误跟踪器中的此评论: code.djangoproject.com/ticket/29655#comment:3),因此如果您为所有模型启用验证,您可能会偶然发现一些缺陷,例如身份验证停止工作 (code.djangoproject.com/ticket/29655)。您将不得不自己处理这些问题。但是,没有更好的方法 atm。 从 Django 2.2.3 开始,这会导致基本身份验证系统出现问题。你会得到一个ValidationError: Session with this Session key already exists。为避免这种情况,您需要为 sender in list_of_model_classes 添加一个 if 语句,以防止信号覆盖 Django 的默认身份验证模型。定义list_of_model_classes,任你选择【参考方案7】:

AFAIK,这是因为向后兼容。带有排除字段的 ModelForms、带有默认值的模型、pre_save() 信号等也存在问题。

您可能感兴趣的来源:

http://code.djangoproject.com/ticket/13100 http://groups.google.com/group/django-developers/browse_frm/thread/b888734b05878f87

【讨论】:

第二个参考文献中最有用的摘录(恕我直言):“开发一个“自动”验证选项,它既简单到实际有用,又足够健壮以处理所有边缘情况——如果这甚至是可能的——远远超过在 1.2 时间框架上可以完成的事情。因此,目前,Django 没有任何这样的东西,并且不会在 1.2 中拥有它。如果你认为你可以让它在 1.3 中工作,最好的办法是提出一个建议,至少包括一些示例代码,并说明如何保持它既简单又健壮。” 这个已经过时了,不知道现在还这样吗?

以上是关于为啥 django 的 model.save() 不调用 full_clean()?的主要内容,如果未能解决你的问题,请参考以下文章

Django model.save() 不写入数据库

Keras model.save() 和 model.save_weights() 的区别?

tf.saved_model.save(model, path_to_dir) 和 tf.keras.model.save 的区别

在 model.save 或 model.save 中捕获数据库异常!方法?

在 model.save() 中处理竞争条件

model.save没有更新模型值