Consider the following piece of code:
@receiver(pre_save, sender=MyModel)
def my_handler(sender, **kwargs):
instance = kwargs['instance']
if instance.verified:
do_something(instance)
else:
do_else(instance)
Its good, because it keeps `MyModel` decoupled from `do_something`
But there is one flaw. If we do:
MyModel.objects.filter(verified=False).update(verified=True)
we are screwed, `do_something` is not executed, and our models get out-of-sync.
If we try to get smart and manually fire the pre_save signal for each instance, we are gonna have a hard time.
Its gonna be slow.
And its gonna be memory inefficient.
We already experienced it in our app.
So our new approach is like this:
pre_bulk_update = Signal(providing_args=["queryset", "update_kwargs"])
post_bulk_update = Signal(providing_args=["update_kwargs",])
pre_bulk_create = Signal(providing_args=["objs", "batch_size"])
post_bulk_create = Signal(providing_args=["objs", "batch_size"])
class MyModelQuerySet(models.QuerySet):
def update(self, **kwargs):
pre_bulk_update.send(sender=self.model, queryset=self, update_kwargs=kwargs)
res = super(MyModelQuerySet, self).update(**kwargs)
# The queryset will be altered after the update call
# so no reason to send it.
post_bulk_update.send(sender=self.model, update_kwargs=kwargs)
return res
def bulk_create(self, objs, batch_size=None):
pre_bulk_create.send(sender=self.model, objs=objs, batch_size=batch_size)
res = super(MyModelQuerySet, self).bulk_create(objs, batch_size)
post_bulk_create.send(sender=self.model, objs=objs, batch_size=batch_size)
return res
class MyModel(models.Model):
#...
objects = MyModelQuerySet.as_manager()
This gives us a nice interface to handle all kind of changes regarding `MyModel`
Our example usage looks like this:
@receiver(pre_save, sender=MyModel)
def my_handler(sender, **kwargs):
instance = kwargs['instance']
if instance.verified:
do_something(instance)
else:
do_else(instance)
@receiver(pre_bulk_update, sender=MyModel)
def my_bulk_update_handler(sender, **kwargs):
update_kwargs = kwargs['update_kwargs']
if 'verified' not in update_kwargs:
# no change im interested in
# no need to take any action
return
queryset = kwargs['queryset']
pks_to_be_updated = queryset.values_list('pk', flat=True)
if update_kwargs['verified']:
do_something_bulk_update_implementation(pks_to_be_updated)
else:
bulk_update_do_else_implementation(pks_to_be_updated)
@receiver(pre_bulk_create, sender=MyModel)
def my_bulk_create_handler(sender, **kwargs):
objs = kwargs['objs']
group_1 = []
group_2 = []
for obj in objs:
if obj.verified:
group_1.append(obj)
else:
group_2.append(obj)
if group_1:
do_something_bulk_create_implementation(group_1)
if group_2:
bulk_create_do_else_implementation(group_2)
I think this turns out to be a very clean approach.
It help us use the most optimal strategy to handle the change.
So I'm sharing this with the community to check your feedback.
I believe if this gets into the Django Internals, it can be a very powerful tool.
It will lose power as a 3rd party app.