【问题标题】:Is Django post_save triggered before/after saving instance to database?Django post_save 是否在将实例保存到数据库之前/之后触发?
【发布时间】:2014-03-13 04:15:51
【问题描述】:

我有一个使用 Django 的网站。每个帖子都是一个称为文章的对象。我想在保存后检索帖子的 HTML,所以我编写了以下 post_save 钩子:

@receiver(models.signals.post_save, sender=Article)
def _send_article_mentions(sender, instance, **kwargs):
    import requests
    from django.contrib.sites.models import Site
    from urlparse import urljoin
    from ParallelTransport.settings import ARTICLES_URL
    SITE_URL = 'http://'+Site.objects.get_current().domain
    article_url = urljoin( SITE_URL, instance.get_absolute_url() )
    import time
    time.sleep(20)
    r = requests.get(article_url)
    error_file = open(ARTICLES_URL+'/'+'error.txt','w')
    error_file.write('file started1\n')

    m = r.status_code
    error_file.write(str(m))
    error_file.close()

它基本上会等待 20 秒(作为测试添加),然后尝试使用其 URL 检索帖子的 HTML,并将请求状态代码写入文件以进行调试。

问题是我在第一次保存时总是得到状态 = 404,它在第二次和后续保存时确实有效。我认为 Django 的工作方式是按顺序排列的:

  1. 使用 save() 将实例保存到数据库。此时帖子将获得一个 URL
  2. 发送 post_save 信号

但是我应该能够在 post_save 中检索 HTML。我对 post_save 的理解有误吗?

添加注释:

  1. 将此代码放在 save() 方法中不起作用。也不应该。帖子在 save() 方法的末尾添加到数据库中,因此在 save() 结束之前不应有任何 URL。
  2. 这是在生产站点上,而不是在开发服务器上。
  3. 我想使用 HTML 中的链接发送“pingbacks”或实际上是webmention。但是我所有的 pingbacks 都被拒绝了,因为该帖子还没有 URL。这是不起作用的最低限度代码。

【问题讨论】:

  • 您是否考虑过另一种方法:不使用 HTTP 检索文章,而是直接将文章模板呈现为字符串。见docs.djangoproject.com/en/1.6/ref/templates/api/…
  • 我想使用 HTML 中的链接发送“pingbacks”或实际上是webmention。但是我所有的 pingbacks 都被拒绝了,因为该帖子还没有 URL。这是不起作用的最低限度代码。

标签: python django post save


【解决方案1】:

虽然这是一个完全错误的方法(*),但问题可能出在数据库事务中。当前线程保存文章,但在这个未提交的事务中,您试图通过另一个线程(通过 Web 服务器)获取这些数据。在这种情况下,这种行为是完全正确的。要么您需要在通过另一个线程检索之前提交,要么通过其他方式获取 HTML。

(*) 应该在后台异步完成(Celery 或其他更轻量级的异步队列应用程序),或者如果您想获取 HTML,您可以直接调用视图(取决于您的视图,您可能必须伪造请求; 如果太复杂,您可以创建一个辅助函数来挑选最少的代码来呈现模板)。如果您只需要在保存某些内容后调用 3rd 方 API,则您希望异步执行此操作。如果您不这样做,“save() 代码”的成功将取决于您的连接或第 3 方服务的可用性,您将需要在您不会处理交易的地方处理交易;)

【讨论】:

  • 你似乎对线程的事情是正确的,虽然我一点也不明白。我在 post_save 中设置了 60 秒的睡眠时间,当它运行时尝试通过浏览器访问该帖子,它给出了 404。
  • 在事务型数据库中,如果您执行插入/更新查询,修改后的数据并没有真正保存,它们是临时的。这与打开一个 txt 文件,进行更改但不保存文件的原理相同。您会在屏幕上看到更改(当前线程),但是当其他人打开同一个文件(另一个 Web 服务器线程)时,会出现旧内容。文件打开多长时间(你的睡眠时间)无关紧要,你必须先按Ctrl+S(提交),然后才能对其他人可见。
  • 是的,现在说得通了!谢谢。
  • 这是一个很好的解释。当我使用 post_save 传递对象 id 和 celery 任务来检索相同的对象时,我遇到了这个问题
【解决方案2】:

您是否尝试过覆盖对象的保存方法、调用 super、等待然后尝试检索 HTML? 你也在使用开发服务器吗?在第一个请求仍在进行时,处理第二个请求可能会出现问题。也许在合适的服务器上试试?

【讨论】:

  • 是的。除非完全执行了看起来正确的保存,否则该帖子没有 URL。我在一个实际的服务器上做所有这些。
【解决方案3】:

我有类似的问题可能是由同一个问题引起的(问的不同,https://plus.google.com/u/0/106729891586898564412/posts/Aoq3X1g4MvX)。 我没有以正确的方式解决它,但是您可以尝试使用数据库缓存,或者(在另一个 django 数据库问题中看到它)关闭所有数据库连接并重新查询。

【讨论】:

    【解决方案4】:

    编辑 2:
    我创建了一个简单的示例(使用 Django 1.5.5)来测试它是否按预期工作。据我所知,确实如此。 pre_save 在数据库提交之前触发,而 post_save 在之后触发。
    详细示例:

    两个示例模型。
    文章用于触发信号。
    ArticleUrl 用于记录来自 Article.get_absolute_url() 的响应。

    # models.py
    from django.db import models
    from django.core.urlresolvers import reverse
    
    class Article(models.Model):
      name = models.CharField(max_length=150)
    
      def get_absolute_url(self):
      """
      Either return the actual url or a string containing '404' if reverse
       fails (Article is not saved).
      """
        try:
          return reverse('article:article-detail', kwargs={'pk': self.pk})
        except:
          return '404'
    
    class ArticleUrl(models.Model):
      article_name = models.CharField(max_length=150)
      url = models.CharField(max_length=300)
    
      def __unicode__(self):
        return self.article_name + ': ' + self.url
    

    示例views.py 和urls.py 被省略,因为它们很简单。如果需要,我可以添加它们。

    # views.py, url.py
    

    为文章创建 pre_save 和 post_save 信号:

    # signals.py
    from django.db.models import signals
    from django.dispatch import receiver
    from article.models import Article, ArticleUrl
    
    
    @receiver(signals.pre_save, sender=Article)
    def _log_url_pre(sender, instance, **kwargs):
      article = instance
      article_url = ArticleUrl(
        article_name = 'pre ' + article.name,
        url = article.get_absolute_url()
      )
      article_url.save()
    
    
    @receiver(signals.post_save, sender=Article)
    def _log_url_post(sender, instance, **kwargs):
      article = instance
      article_url = ArticleUrl(
        article_name = 'post ' + article.name,
        url = article.get_absolute_url()
      )
      article_url.save()
    

    导入我的 signals.py 以便 Django 可以使用它:

    # __init__.py
    import signals
    

    定义上述内容后,我继续在 Django shell (python.exe manage.py shell) 中创建了一个新文章。

    >>> from article.models import *
    >>> a = Article(name='abcdd')
    >>> a.save()
    >>> ArticleUrl.objects.all()
    [<ArticleUrl: pre abcdd: 404>, <ArticleUrl: post abcdd: /article/article/8>]
    

    上面的例子似乎确实表明 pre_save 确实没有返回 url,但 post_save 确实返回了。两者似乎都按预期行事。
    您应该检查您的代码是否偏离上述示例或以某种方式干扰程序执行。

    编辑 1:
    话虽如此(如下),根据What Happens When You Save,post_save 信号应该在数据库保存之后运行。
    您的应用/网站的其他部分是否会以某种方式干扰此操作?

    原帖:
    根据Django documentation,post_save 信号在 save() 结束时发送,而不是在它之后。
    据我了解,Django 信号是同步的(进程内),因此它们会停止实际的 save()。在信号完成之前,它不会完全完成。

    这并不总是适用,但您是否考虑过可以在 save() 之后调用的自定义信号?

    【讨论】:

    • 我不知道怎么做。例如,如果我第二次和每次都保存帖子,上面的代码就可以工作。它在第一次保存时不起作用。
    • 我添加了 pre_save 和 post_save 按预期工作的详细示例。你能把它和你自己的代码比较一下吗?
    • 您的示例代码有效。似乎帖子确实有一个可通过反向检索的 URL,但其他代理无法访问,例如 post_save 结束之前的浏览器。我通过在 post_save 中仅睡眠 60 秒并通过浏览器访问帖子来测试这一点,但它不起作用。看来@Bruce 是对的,甚至以为我听不懂他说的话。
    • @Bruce 指出您可能遇到了 SQL 事务提交问题。您的代码基本上在 2 个单独的线程中运行。第一个是 save() 本身,第二个是对 url 内容的请求。看来 Django 只是在 save() 完全完成之前不会将更改(和新数据)从第一个提交到数据库。这只发生在 post_save 之后。您是否考虑过以不同的方式做您需要的事情?也许通过模型而不是 url 访问内容?
    猜你喜欢
    • 2011-08-18
    • 2019-08-23
    • 1970-01-01
    • 2011-12-21
    • 1970-01-01
    • 1970-01-01
    • 2018-04-03
    • 2017-12-26
    • 1970-01-01
    相关资源
    最近更新 更多