Django在批量插入/更新/删除时“模拟”数据库触发行为

Posted

技术标签:

【中文标题】Django在批量插入/更新/删除时“模拟”数据库触发行为【英文标题】:Django "emulate" database trigger behavior on bulk insert/update/delete 【发布时间】:2014-07-07 19:54:22 【问题描述】:

这是一个自我解释的问题,但我们开始吧。 我正在 Django 中创建一个业务应用程序,我不想在应用程序和数据库中“传播”所有逻辑,但另一方面,我不想让数据库处理这个任务(它可能通过使用Triggers)。

所以我想“重现”数据库触发器的行为,但在 Django 的模型类中(目前使用 Django 1.4)。

经过一些研究,我发现对于单个对象,我可以覆盖“models.Model”类的“save”和“delete”方法,插入“before”和“after”钩子以便它们可以被执行在父母的保存/删除之前和之后。像这样:

     class MyModel(models.Model):

         def __before(self):
             pass

         def __after(self):
            pass

         @commit_on_success #the decorator is only to ensure that everything occurs inside the same transaction
         def save(self, *args, *kwargs):
             self.__before()
             super(MyModel,self).save(args, kwargs)
             self.__after()

大问题在于批量操作。从 QuerySet 运行“update()”/“delete()”时,Django 不会触发模型的保存/删除。 Insted,它使用 QuerySet 自己的方法。更糟糕的是,它也不会触发任何信号。

编辑: 更具体一点:视图内的模型加载是动态的,因此不可能定义“特定于模型”的方式。在这种情况下,我应该创建一个抽象类并在那里处理它。

我最后一次尝试是创建一个自定义管理器,在这个自定义管理器中,覆盖更新方法,循环查询集中的模型,并触发每个模型的“save()”(考虑上面的实现,或“信号”系统)。它可以工作,但会导致数据库“过载”(想象一个 10k 行的查询集正在更新)。

【问题讨论】:

您到底想在保存前/保存后触发器中做什么?根据您的需要,有些方法可能有效,有些则无效。 很难说,因为每个模型可能有不同的行为。 “SaleItems”可以更新其“Sale”记录的值,而正在更新的“Sale”记录可以及时更新“BillingStatus”,等等。同样,每个模型都可能有前/后行为。 【参考方案1】:

有一些注意事项,您可以覆盖查询集的 update 方法来触发信号,同时仍然使用 SQL UPDATE 语句:

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

def CustomQuerySet(QuerySet):
    @commit_on_success
    def update(self, **kwargs):
        for instance in self:
            pre_save.send(sender=instance.__class__, instance=instance, raw=False, 
                          using=self.db, update_fields=kwargs.keys())
        # use self instead of self.all() if you want to reload all data 
        # from the db for the post_save signal
        result = super(CustomQuerySet, self.all()).update(**kwargs)
        for instance in self:
            post_save.send(sender=instance.__class__, instance=instance, created=False,
                           raw=False, using=self.db, update_fields=kwargs.keys())
        return result

    update.alters_data = True

我克隆当前查询集(使用self.all()),因为update方法会清除查询集对象的缓存。

有一些问题可能会或可能不会破坏您的代码。首先,它将引入竞争条件。您根据更新数据库时可能不再准确的数据在pre_save 信号的接收器中执行某些操作。

大型查询集也可能存在一些严重的性能问题。与update 方法不同,所有模型都必须加载到内存中,然后仍然需要执行信号。特别是如果信号本身必须与数据库交互,性能可能会慢得令人无法接受。与常规的 pre_save 信号不同,更改模型实例不会自动导致数据库更新,因为模型实例不用于保存新数据。

可能还有一些问题会在少数极端情况下导致问题。

无论如何,如果您可以在不出现严重问题的情况下处理这些问题,我认为这是最好的方法。它产生尽可能少的开销,同时仍将模型加载到内存中,这是正确执行各种信号所必需的。

【讨论】:

我不认为它会比这更接近。我看到的唯一真正的问题是,这还不是每条记录“pre/save/post”之间的错误链。按照您描述的方式,它会触发所有的 pres,然后保存,然后是所有的帖子。 update 方法执行单个 sql UPDATE 语句。如果不单独保存每个实例,就无法为每行实现同步的 pre/save/post 链。无论如何,不​​是在 Python 中。【参考方案2】:

首先,您可以使用内置的 pre_savepost_save,pre_delete,post_delete 信号,而不是覆盖 save 以添加 __before__after 方法。 https://docs.djangoproject.com/en/1.4/topics/signals/

from django.db.models.signals import post_save

class YourModel(models.Model):
    pass

def after_save_your_model(sender, instance, **kwargs):
     pass

# register the signal
post_save.connect(after_save_your_model, sender=YourModel, dispatch_uid=__file__)

pre_deletepost_delete 在查询集上调用 delete() 时会被触发。

但是,对于批量更新,您必须手动调用要自己触发的函数。您也可以将其全部放入事务中。

如果您使用的是动态模型,要调用正确的触发函数,您可以检查模型的 ContentType。例如:

from django.contrib.contenttypes.models import ContentType

def view(request, app, model_name, method):
    ...
    model = get_model(app, model_name)
    content_type = ContentType.objects.get_for_model(model)
    if content_type == ContenType.objects.get_for_model(YourModel):
        after_save_your_model(model)
    elif content_type == Contentype.objects.get_for_model(AnotherModel):
        another_trigger_function(model)

【讨论】:

“手动调用”的问题是我永远不知道我正在处理哪个模型,因为视图中的模型处理是动态的。同样,批量更新/插入不会触发信号 您能澄清一下“视图中的模型处理是动态的”是什么意思吗?这会起作用还是更新:要手动调用您的信号,您必须这样做:queryset.update(field=value); for model in queryset:after_save_your_model(model) 您可以为 post_save 的批量插入执行类似的操作。 基本上,对模式 'r'^(?P(.*?))/(?P(.*?))/(?P(.*?))/'' 转到相同的视图方法,在视图内部,我使用“get_model(app,model)”加载模型。而且您的建议并不像 DB 触发器,因为如果“id=2”行的数据依赖于“id=1”行的数据,它就无法工作。 不需要对内容类型进行额外的数据库查询。相反,您可以使用指定的不同发送者将不同的函数连接到信号,或者如果您有多个信号发送者,请在 connect 中传递 sender=None 并检查实际模型类。内容类型仅对数据库级别的通用关系是必需的。

以上是关于Django在批量插入/更新/删除时“模拟”数据库触发行为的主要内容,如果未能解决你的问题,请参考以下文章

jdbc-批量插入批量删除批量更新

Django 批量插入数据自定义分页器多表关系的建立及Form组件(待更新。。。)

Java--MyBatis批量插入批量更新和批量删除

如何强制 django 立即保存而不是在循环后进行批量更新

Spring Boot Elasticsearch7.6.2实现创建索引删除索引判断索引是否存在获取/添加/删除/更新索引别名单条/批量插入单条/批量更新删除数据递归统计ES聚合的数据

在批量更新删除时禁用 NSFetchedResultsController 生成的动画