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_save
、post_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_delete
和 post_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(.*?))/(?Pconnect
中传递 sender=None
并检查实际模型类。内容类型仅对数据库级别的通用关系是必需的。以上是关于Django在批量插入/更新/删除时“模拟”数据库触发行为的主要内容,如果未能解决你的问题,请参考以下文章
Django 批量插入数据自定义分页器多表关系的建立及Form组件(待更新。。。)
Spring Boot Elasticsearch7.6.2实现创建索引删除索引判断索引是否存在获取/添加/删除/更新索引别名单条/批量插入单条/批量更新删除数据递归统计ES聚合的数据