仅当外键存在时才保存的标准方法是啥?

Posted

技术标签:

【中文标题】仅当外键存在时才保存的标准方法是啥?【英文标题】:What's the standard way of saving something only if its foreign key exists?仅当外键存在时才保存的标准方法是什么? 【发布时间】:2019-07-07 23:50:49 【问题描述】:

我正在使用 Python 3.7 和 Django。我有以下模型,带有另一个模型的外键...

class ArticleStat(models.Model):
    objects = ArticleStatManager()
    article = models.ForeignKey(Article, on_delete=models.CASCADE, related_name='articlestats')
    ...

    def save(self, *args, **kwargs):
        if self.article.exists():
            try:
                article_stat = ArticleStat.objects.get(article=self.article, elapsed_time_in_seconds=self.elapsed_time_in_seconds)
                self.id = article_stat.id
                super().save(*args, **kwargs, update_fields=["hits"])
            except ObjectDoesNotExist:
                super().save(*args, **kwargs)

如果相关的外键存在,我只想保存它,否则,我注意到错误结果。做这样的事情的标准 Django/Python 方式是什么?我以为我读过我可以使用“.exists()”(Check if an object exists),但我得到了一个错误

AttributeError: 'Article' object has no attribute 'exists'

编辑:这是我必须检查的单元测试...

    id = 1
    article = Article.objects.get(pk=id)
    self.assertTrue(article, "A pre-condition of this test is that an article exist with id=" + str(id))
    articlestat = ArticleStat(article=article, elapsed_time_in_seconds=250, hits=25)
    # Delete the article
    article.delete()
    # Attempt to save ArticleStat
    articlestat.save()

【问题讨论】:

这里的问题是您在实例而不是查询集上调用exists()。请查看文档和示例docs.djangoproject.com/en/2.1/ref/models/querysets/… 【参考方案1】:

如果您想确定 ArticleStatsave 方法中存在 Article,您可以尝试从您的数据库中获取它,而不仅仅是测试 self.article

引用Alex Martelli:

" ... Grace Murray Hopper 的著名格言“请求宽恕比请求许可更容易”,有许多有用的应用——在 Python 中,..."

我认为使用 try .. except .. else 更符合 Python 风格,我会这样做:

from django.db import models

class ArticleStat(models.Model):
    ...
    article = models.ForeignKey(
        Article, on_delete=models.CASCADE, related_name='articlestats'
    )

    def save(self, *args, **kwargs):
        try:
            article = Article.objects.get(pk=self.article_id)
        except Article.DoesNotExist:
            pass
        else:
            try:
                article_stat = ArticleStat.objects.get(
                    article=article,
                    elapsed_time_in_seconds=self.elapsed_time_in_seconds
                )
                self.id = article_stat.id
                super().save(*args, **kwargs, update_fields=["hits"])
            except ArticleStat.DoesNotExist:
                super().save(*args, **kwargs)

【讨论】:

谢谢。这是处理此类事情的典型方式吗?我问是因为在极少数情况下,文章对象在您的“article = Article.objects.get(pk=self.ar” 之后和“save”行之前被删除,这种方法会失败,不是吗? 我试图根据您的问题写一个答案,但我没有足够的数据来说明您的解决方案是否是在您的特定项目中处理此行为的最佳解决方案。对于“罕见事件”,您可以使用事务。 对于事务方法,您有示例说明它的外观吗? 我看到其他人指出,链接往往会随着时间的推移而中断,因此在问题中嵌入答案更可取。您不必在答案中添加任何内容,但某种高效、万无一失的代码肯定是我正在寻找的。​​span> 我再次阅读了您的评论,并确认您在我的回答中提出的代码。 “在极少数情况下,文章对象被删除,ArticleStat 的 get 方法将失败,但 except 将处理失败。所以你不需要我在 cmets 中写的任何事务。【参考方案2】:

如果您使用的是关系数据库,外键约束将在迁移后自动添加。 save 方法可能不需要任何自定义。

class ArticleStat(models.Model):
    objects = ArticleStatManager()
    article = models.ForeignKey(
        Article, on_delete=models.CASCADE, related_name='articlestats'
    )

使用以下代码创建 ArticleStats

from django.db import IntegrityError
try:
  ArticleStats.objects.create(article=article, ...)
except IntegrityError:
  pass

如果 article_id 有效,则创建 ArticleStats 对象,否则引发 IntegrityError。

article = Article.objects.get(id=1)
article.delete()
try:
  ArticleStats.objects.create(article=article, ...)
  print("article stats is created")
except IntegrityError:
  print("article stats is not created")


# Output
article stats is not created

注意:在 mysql v5.7、Django 1.11 上测试

【讨论】:

我写了一个单元测试来检查这个(现在包含在问题中),但是使用你的代码仍然会导致错误,“ValueError: save() disabled to prevent data loss due to unsaved related object '文章'。”也许是因为单元测试使用了 SqlLite? sqlite 中默认禁用外键约束,但这不是此错误的原因。无论数据库后端如何,您都会收到此错误。检查this链接。 如果你使用 .create() 而不是使用 .save(),那么它永远不会引发这个错误。建议:articlestat = ArticleStat.objects.create(article=article, elapsed_time_in_seconds=250, hits=25) (.create() 内部调用 .save()) 对象的更新怎么样?我尝试用“更新”替换“保存”,但出现错误,“AttributeError:'super'对象没有属性'update'” .update() 在 QuerySet 中定义,仅适用于 queryset。例如,.update() 适用于 ArticleStat.objects.al().update(sometime) 或 ArticleStat.objects.filter().update() 但不适用于 ArticleStat.objects.get(id=id).update () 【参考方案3】: ArticleStat 模型上的

article 字段不是可选的。如果没有文章的 ForeignKey,您将无法保存您的 ArticleStat 对象

这里是类似的代码,item是Item模型的ForeignKey,是必需的。

class Interaction(TimeStampedModel, models.Model):
    ...
    item = models.ForeignKey(Item, on_delete=models.CASCADE, related_name='interactions')
    type = models.IntegerField('Type', choices=TYPE_CHOICES)
    ...

如果我尝试从 shell 中保存一个 Interaction 对象而不选择该项目的 ForeignKey,我会收到一个 IntegrityError。

~ interaction = Interaction()
~ interaction.save()
~ IntegrityError: null value in column "item_id" violates not-null constraint

您不需要支票self.article.exists()。 Django 和数据库将需要该字段,并且不会让您在没有它的情况下保存对象。

您应该阅读 Django Docs 中的 ForeignKey 字段

【讨论】:

嗨,我意识到如果 hte 文章不存在,Django 不会让我保存该字段——抛出一个错误,这会停止我的程序。我不希望我的程序停止。处理这种事情的典型 Django 方式是什么?看起来这对程序员来说一定是一件很常见的事情,而 Django 有一些巧妙的方法来处理它。【参考方案4】:

您可以只测试article 字段的值。如果没有设置,我相信它默认为None

if self.article:  # Value is set

如果您希望此 ForeignKey 字段是可选的(听起来像您这样做),您需要在该字段上设置 blank=Truenull=True。这将允许该字段为空白(验证中),并在该字段不存在时在该字段上设置null

正如下面的 cmets 中所述,您的数据库可能会强制执行该字段是必需的这一事实,并拒绝删除 article 实例。

【讨论】:

如果您不需要文章实例而只想检查是否存在,我会将其细化为if self.article_id:。这样可以避免在之前未获取 article 的情况下执行数据库查询。 @JonahBishop,“如果 self.article:”不起作用。即使文章已经从数据库中删除,“if self.article”仍然评估为非空实体。 @lukewarm, "if self.article_id:" 即使文章已从数据库中删除,也似乎评估为 true。 如果您更改模型以包含null=True 选项,是否会改变删除模型实例的效果? @dave 你确定 Article 实例已被删除吗?大多数数据库会在一定程度上强制引用完整性,如果您只是使用常规的ForeignKey 字段,那么这种情况不太可能发生。您对@JonahBishop 的回复似乎也表明文章行实际上并没有被删除..【参考方案5】:

正如其他答案所指出的,ArticleStat 模型需要 Article ForeignKey,如果没有有效的 Article 实例,保存将自动失败。使用无效输入优雅地失败的最佳方法是使用 Form 验证和 Django 的 Forms API。或者,如果使用 Django Rest Framework 处理序列化数据,则使用 Serializer,这是 JSON 数据的类似 Form 的等价物。这样,除非您有特定要求,否则您不需要覆盖 save 方法。

到目前为止,没有人提到.exists(). 的正确用法,它是 queryset 的方法,而不是 model 实例,这就是为什么您会收到您提到的错误上面尝试将其应用于具有self.article.exists() 的单个模型实例时。要检查对象是否存在,只需使用.filter 而不是.get。如果您的文章 (pk=1) 存在,则:

Article.objects.filter(pk=1)

将返回一个包含一篇文章的查询集:

<Queryset: [Article: 1]>

Article.objects.filter(pk=1).exists()

将返回True。而如果项目不存在,查询将返回一个空查询集,.exists() 将返回False,而不是引发异常(就像尝试.get() 一个不存在的对象那样)。如果 pk 以前存在并已被删除,这仍然适用。

编辑:刚刚注意到您的ArticleStaton_delete 行为当前设置为CASCADE。这意味着当一篇文章被删除时,相关的 ArticleStat 也会被删除。所以我认为您在尝试if self.article: 时一定是误解了您在回复@jonah-bishop 的回答时提到的错误/困难。对于最小的修复,如果您在删除文章后仍想保留 ArticleStat,请将 on_delete 关键字更改为 models.SET_NULL,并根据 Jonah 的回答,添加额外的关键字 null=True, blank=True

article = models.ForeignKey(Article, on_delete=models.SET_NULL, related_name='articlestats', null=True, blank=True)

那么简单地做if self.article:来检查一个有效的ForeignKey对象关系应该没有问题。但是使用 Forms/Serializers 仍然是更好的做法。

【讨论】:

那么关于编写我的“保存”方法,我将“Article.objects.filter(pk=1).exists()”这一行放在哪里? 您可以将其用于您的单元测试检查,但最好不要在您的模型上编写自定义保存方法来检查有效的文章。在 Django 中,强烈建议在保存之前通过表单(或序列化程序)处理所有用户输入,因为它不仅验证数据,而且提供针对恶意意图的内置保护。顺便说一下,“表单”这个词不一定需要手动用户输入——Django 表单只是一个验证内容的对象。 ModelForm 只需几句代码即可验证所有模型字段。 您将输入数据传递到表单中,然后调用form.is_valid(),它将捕获异常并返回一个False 值(如果有),因此只有在True 时才保存它。如果你使用 Django 的基于类的视图,你不需要写任何东西,除了表单并将它分配给你的视图。查看文档;这也是good Form walkthrough。 谢谢,但我没有使用表格。对象是通过作为 Python 命令运行的服务创建的(有时多个命令同时运行,导致对象可能不存在的奇怪情况)。 它们也适用于该用例。 Django 表单/序列化器只是一个在保存之前完成验证数据和处理异常的艰苦工作的对象。否则,您将不得不自己捕获异常,因为您要么选择不存在的内容,要么将所需的数据库字段留空。 (除非您可以从流程中的其他地方访问文章的预期 pk,在这种情况下,您可以像上面那样运行单个项目过滤器。)【参考方案6】:

如果article 实例已被删除,最后一行articlestat.save() 的代码将失败。如果您使用 mysql 或 sqlite3 等关系数据库,Django 和数据库会自动为您检查文章。

在迁移期间,将创建一个约束。例如:

shell>>> python manage.py sqlmigrate <appname> 0001
CREATE TABLE impress_impress ...
...

ALTER TABLE `impress_impress` ADD CONSTRAINT 
    `impress_impress_target_id_73acd523_fk_account_myuser_id` FOREIGN KEY (`target_id`) 
    REFERENCES `account_myuser` (`id`);

...

所以如果你想在没有article的情况下保存articlestat,将会引发错误。

【讨论】:

嗨,是的,我知道如果文章不存在会引发错误——这就是我提出问题的原因。如果文章不存在或至少使一切正常失败,我如何防止调用保存?我只是不希望该方法引发任何异常。【参考方案7】:

您可以在.save()之前拨打.full_clean()

from django.core.exceptions import ValidationError

class ArticleStat(models.Model):
    #...
    def save(self, *args, **kwargs):
        try:
            self.full_clean()
        except ValidationError as e:
            # dont save
            # Do something based on the errors contained in e.message_dict.
            # Display them to a user, or handle them programmatically.
            pass
        else:
            super().save(*args, **kwargs)

【讨论】:

嗨,这不起作用。当我为了更新现有对象而调用“保存”方法时,会调用 ValidationError 并出现错误,'['Article stat with this Article and Elapsed time in seconds already exists.']' 在你的代码中搜索错误,full_clean 知道如何处理更新操作。 我对现有对象代码的更新工作正常,但是当我添加“self.full_clean()”内容时,我在之前的评论中遇到了错误。我对错误中提到的两个字段有唯一的约束。也许 full_clean 不适用于更新的唯一约束? 阅读docs,检查source代码,也许你会看到它支持update操作,并会在你的代码中找到错误。

以上是关于仅当外键存在时才保存的标准方法是啥?的主要内容,如果未能解决你的问题,请参考以下文章

Restkit:当外键引用Core Data中缺少的本地对象时获取远程链接对象

Restkit:当外键设置为 null 时,Core Data 中的关系不会重置

当外键也是主键时,在 MySQL 上出现外键错误? [复制]

仅当尚未设置时才设置原子

仅当 Validation 为 True 时才启用 Jbutton

仅当存在集合类而不是其他类时才应用 CSS 规则 [重复]