首页 > 解决方案 > Django(v2.0.7) Admin:数据库事务提交后回调

问题描述

我有一个巨大的模型,有很多属性,包括多个 ManyToManyMapping。应用程序中的大部分添加/更新都是通过 REST API 进行的,但为了进行较小的更正,我使用了 Django Admin Form。这个管理表单也有多个内联表单集。

publish_event在通过表单或 REST API 更新模型后,我想将一些事件发布到 Kafka( )。我希望在事务提交到数据库时发生这种情况,以便监听 Kafka 事件的服务最终不会从数据库中获取陈旧的数据。

我提到了这篇SO 帖子,但它似乎是在每笔交易上都这样做,而不是在每个模型的基础上,并且on_commit 会造成事情被调用两次的问题(更多下文)。

到目前为止我尝试过的事情:

  1. 信号:由于添加了 ManyToManyMapping 而被拒绝,model.save()需要调用两次,最终发布了 2 个事件。此外,它在模型保存上运行,而不是事务提交,所以在回滚的情况下,我仍然会发布一个事件。

  2. 覆盖模型的方法:由于与被调用两次save(self, *args, **kwargs):相同的原因而被拒绝。model.save()

  3. 覆盖 ModelAdmin's save_model:这是当我们在表单上点击 Save 时首先要调用的东西之一,所以覆盖它并没有帮助,因为 formset 还没有被处理。因此,包括 M2M 映射在内的完整状态不会在数据库中提交。

def save_model(self, request, obj, form, change): super().save_model(request, obj, form, change) publish_event()

  1. 覆盖 ModelAdmin 的save_related:起初这似乎是解决方案,但事务尚未提交给 DB。 def save_related(self, request, form, formsets, change): form.save_m2m() for formset in formsets: self.save_formset(request, form, formset, change=change) publish_event() 到目前为止,我还没有弄清楚任何回调触发的事务提交后。

标签: djangodjango-modelsdjango-formsdjango-admin

解决方案


TLDR:覆盖change_view


深入研究源代码文件django.contrib.admin.option.py后,似乎保存模型和相关的 M2M 正在由以下代码触发_changeform_view

if all_valid(formsets) and form_validated:
    self.save_model(request, new_object, form, not add)
    self.save_related(request, form, formsets, not add)
    change_message = self.construct_change_message(request, form, formsets, add)

which 被调用changeform_viewwhich 设置原子事务。这就是我想要覆盖的内容,以便publish_event一旦将事情提交给 DB 就可以执行:

@csrf_protect_m
def changeform_view(self, request, object_id=None, form_url='', extra_context=None):
    with transaction.atomic(using=router.db_for_write(self.model)):
        return self._changeform_view(request, object_id, form_url, extra_context)

该代码依次由change_viewand调用add_view

def change_view(self, request, object_id, form_url='', extra_context=None):
    return self.changeform_view(request, object_id, form_url, extra_context)

由于我只通过表单进行更新(而不是创建),因此我覆盖change_view了显式调用publish_event

def change_view(self, request, object_id, form_url='', extra_context=None):
    change_resp = super(MySampleModelAdmin, self).change_view(request, object_id, form_url, extra_context)
    if request.method != 'GET': # since GET also call this and we don't want event published on GET
        publish_event()
    return change_resp

一旦change_resp = super(MySampleModelAdmin, self).change_view(request, object_id, form_url, extra_context)执行完成,事务就被提交,所以publish_event在这一步调用是安全的。在此之后change_view只是期望得到回应。


编辑:试过on_commit,这似乎也有效。这是基于信号的。

from django.db import transaction

@receiver(post_save, sender='app.MySampleModel')
def send_model_save_event(sender, instance=None, created=False, **kwargs):
    if instance is None:
        log.info('Instance is Null')
        return
    transaction.on_commit(lambda: handle_model_after_save(instance.id))

推荐阅读