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

1 投票
2 回答
3183 浏览
提问于 2025-04-18 07:01

这个问题自我解释得很清楚,但我还是来详细说一下。 我正在用Django创建一个商业应用,我不想把所有的逻辑分散在应用和数据库之间,但另一方面,我也不想让数据库来处理这些任务(其实可以通过使用触发器来实现)。

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

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

     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并不会触发模型的保存或删除。相反,它使用的是QuerySet自己的方法。而且更糟糕的是,它也不会触发任何信号。

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

我最后的尝试是创建一个自定义管理器,在这个自定义管理器中重写更新方法,循环遍历QuerySet中的模型,并触发每个模型的“save()”(考虑到上面的实现或“信号”系统)。这样做是可行的,但会导致数据库“过载”(想象一下更新一个有1万行的QuerySet)。

2 个回答

1

首先,不用重写保存方法来添加 __before__after 方法,你可以使用内置的 pre_savepost_savepre_deletepost_delete 信号。详细信息可以查看这个链接: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__)

当你在一个查询集上调用 delete() 时,pre_deletepost_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)
0

有一些注意事项,你可以重写查询集的 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 信号不同,改变模型实例不会自动导致数据库更新,因为模型实例并没有用来保存新数据。

可能还有其他一些问题会在某些特殊情况下导致麻烦。

总之,如果你能处理这些问题而不出现严重的麻烦,我认为这是最好的做法。这样做的开销尽可能小,同时又能将模型加载到内存中,这对于正确执行各种信号是非常必要的。

撰写回答