Django CASCADE and post_delete interaction - python

I have the following model:
class A():
foriegn_id1 = models.CharField # ref to a database not managed by django
foriegn_id2 = models.CharField
class B():
a = models.OneToOneField(A, on_delete=models.CASCADE)
So I want A to be deleted as well when B is deleted:
#receiver(post_delete, sender=B)
def post_delete_b(sender, instance, *args, **kwargs):
if instance.a:
instance.a.delete()
And on the deletion of A, I want to delete the objects from the unmanaged databases:
#receiver(post_delete, sender=A)
def post_delete_b(sender, instance, *args, **kwargs):
if instance.foriegn_id1:
delete_foriegn_obj_1(instance.foriegn_id1)
if instance.foriegn_id2:
delete_foriegn_obj_2(instance.foriegn_id2)
Now, if I delete object B, it works fine. But if I delete obj A, then obj B is deleted by cascade, and then it emits a post_delete signal, which triggers the deletion of A again. Django knows how to manage that on his end, so it works fine until it reaches delete_foriegn_obj, which is then called twice and returns a failure on the second attempt.
I thought about validating that the object exists in delete_foriegn_obj, but it adds 3 more calls to the DB.
So the question is: is there a way to know during post_delete_b that object a has been deleted?
Both instance.a and A.objects.get(id=instance.a.id) return the object (I guess Django caches the DB update until it finishes all of the deletions are done).

The problem is that the cascaded deletions are performed before the requested object is deleted, hence when you queried the DB (A.objects.get(id=instance.a.id)) the related a instance is present there. instance.a can even show a cached result so there's no way it would show otherwise.
So while deleting a B model instance, the related A instance will always be existent (if actually there's one). Hence, from the B model post_delete signal receiver, you can get the related A instance and check if the related B actually exists from DB (there's no way to avoid the DB here to get the actual picture underneath):
#receiver(post_delete, sender=B)
def post_delete_b(sender, instance, *args, **kwargs):
try:
a = instance.a
except AttributeError:
return
try:
a._state.fields_cache = {}
except AttributeError:
pass
try:
a.b # one extra query
except AttributeError:
# This is cascaded delete
return
a.delete()
We also need to make sure we're not getting any cached result by making a._state.fields_cache empty. The fields_cache (which is actually a descriptor that returns a dict upon first access) is used by the ReverseOneToOneDescriptor (accessor to the related object on the opposite side of a one-to-one) to cache the related field name-value. FWIW, the same is done on the forward side of the relationship by the ForwardOneToOneDescriptor accessor.
Edit based on comment:
If you're using this function for multiple senders' post_delete, you can dynamically get the related attribute via getattr:
getattr(a, sender.a.field.related_query_name())
this does the same as a.b above but allows us to get attribute dynamically via name, so this would result in exactly similar query as you can imagine.

Related

Django - How to dynamically create signals inside model Mixin

I'm working on a model Mixin which needs to dynamically set signals based on one attribute.
It's more complicated but for simplicity, let's say the Mixin has this attribute:
models = ['app.model1','app.model2']
This attribute is defined in model which extends this mixin.
How can I register signals dynamically?
I tried to create a classmethod:
#classmethod
def set_signals(cls):
def status_sig(sender, instance, created, *args, **kwargs):
print('SIGNAL')
... do som things
for m in cls.get_target_models():
post_save.connect(status_sig,m)
My idea was to call this method somewhere in class automatically (for example __call__ method) but for now, I just tried to call it and then save the model to see if it works but it didn't.
from django.db.models.signals import post_save
print(post_save.receivers)
Realestate.set_signals()
print(post_save.receivers)
r = Realestate.objects.first()
r.status = 1
r.save()
output
[]
[((139967044372680, 46800232), <weakref at 0x7f4c9d702408; dead>), ((139967044372680, 46793464), <weakref at 0x7f4c9d702408; dead>)]
So you see that it registered those models but no signal has been triggered after saving the realestate.
Do you know how to make it work? Even better without having to call method explicitely?
EDIT:
I can't just put the signals creation inside mixin file because models depends on the string in child model.
If you haven't already solved this:
In the connect method, set weak=False. By default it's True so the locally-defined function reference will get lost if the object instance is garbage collected.
This is likely what's happening to your status_sig function; as you can see in the print out of the post_save receivers, the weakref's are dead so will always just return None
In the Django docs:
weak – Django stores signal handlers as weak references by default. Thus, if your receiver is a local function, it may be garbage collected. To prevent this, pass weak=False when you call the signal’s connect() method.
For more info on weakrefs, see Python docs

determine if Django model is marked for deletion

My example is very contrived, but hopefully it gets the point across.
Say I have two models like this:
class Group(models.Model):
name = models.CharField(max_length=50)
class Member(models.Model):
name = models.CharField(max_length=50)
group = models.ForeignKey(Group)
I want to add some code so that when a Member is deleted it gets recreated as a new entry (remember, very contrived!). So I do this:
#receiver(post_delete, sender=Member)
def member_delete(sender, instance, **kwargs):
instance.pk = None
instance.save()
This works perfectly fine for when a Member is deleted.
The issue, though, is if a Group is deleted this same handler is called. The Member is re-created with a reference to the Group and an IntegrityError is thrown when the final commit occurs.
Is there any way within the signal handler to determine that Group is being deleted?
What I've tried:
The sender seems to always be Member regardless.
I can't seem to find anything on instance.group to indicate a delete. Even trying to do a Group.objects.filter(id=instance.group_id).exists() returns true. It may be that the actual delete of the parent occurs after post_delete calls occur on the children, in which case what I'm trying to do is impossible.
Try doing your job by a classmethod inside Member class and forget about signals.
#classmethod
def reinit(cls, instance):
instance.delete()
instance.save()

Django - How to properly override model's delete() method to update (soft delete) instead of delete

My database design requires to set "deleted" flag on table row when a row is deleted, instead of actually deleting a row. I am using formsets that call model's delete() function when an object is marked for deletion. This means I need to override model's delete function. Using signals is not an option.
My question is how do I properly override it to do update instead of delete. I looked at models.Model.delete function and it does some complex stuff using django.db.models.deletion.Collector class. The Collector can delete and also update values. I was able to override delete method to update only as following:
class CustomerInfo(models.Model):
...
def delete(self, using=None):
collector = Collector(using=using)
collector.add_field_update(self._meta.get_field('deleted'), True, [self])
collector.delete()
But I am not sure if this is the proper way to do it. Alternatively I can override it as following.
def delete(self, using=None):
self.deleted = True
self.save()
Both approaches work, but I am not sure which is the correct one.

When is the "post_save" signal in django activated/called?

I've to update some database tables after saving a particular model. I've used the #receiver(post_save decorator for this. But when in this decorator function, the values are still not saved in the database. I've one to many relation but when I get the current instance that is being saved using kwargs['instance'], it doesn't have child objects. But after saving when I check from shell, it does have child objects. Following is the code that I'm using:
#receiver(post_save, sender=Test)
def do_something(sender, **kwargs):
test = kwargs['instance']
users = User.objects.filter(tags__in=test.tags.values_list('id',flat=True))
for user in users:
other_model = OtherModel(user=user, test=test, is_new=True)
other_model.save()
post_save is sent at the end of Model.save_base(), which is itself called by Model.save((). This means that if you override your model's save() method, post_save is sent when you call on super(YourModel, self).save(*args, **kw).
If Tag has a ForeignKey on Test and the Test instance was just created, you can't expect to have any Tag instance related to your Test instance at this stage, since the Tag instances obviously need to know the Test instance's pk first so they can be saved too.
The post_save for the parent instance is called when the parent instance is saved. If the children are added after that, then they won't exist at the time the parent post_save is called.

Garbage collecting objects in Django

I have a one-to-many relationship, and I would like to automatically delete the one side after the last referencing object on the many side has been deleted. That is to say, I want to perform garbage collection, or do a kind of reverse cascade operation.
I have tried to solve this by using Django's post_delete signal. Here is a simplified example of what I'm trying to do:
models.py
class Bar(models.Model):
j = models.IntegerField()
# implicit foo_set
class Foo(models.Model):
i = models.IntegerField()
bar = models.ForeignKey(Bar)
def garbage_collect(sender, instance, **kwargs):
# Bar should be deleted after the last Foo.
if instance.bar.foo_set.count() == 0:
instance.bar.delete()
post_delete.connect(garbage_collect, Foo)
This works when using Model.delete, but with QuerySet.delete it breaks horribly.
tests.py
class TestGarbageCollect(TestCase):
# Bar(j=1)
# Foo(bar=bar, i=1)
# Foo(bar=bar, i=2)
# Foo(bar=bar, i=3)
fixtures = ['db.json']
def test_separate_post_delete(self):
for foo in Foo.objects.all():
foo.delete()
self.assertEqual(Foo.objects.count(), 0)
self.assertEqual(Bar.objects.count(), 0)
This works just fine.
tests.py continued
def test_queryset_post_delete(self):
Foo.objects.all().delete()
self.assertEqual(Foo.objects.count(), 0)
self.assertEqual(Bar.objects.count(), 0)
This breaks on the second time the signal is emitted, because as Django's documentation says, QuerySet.delete is applied instantly, and instance.bar.foo_set.count() == 0 is true already on the first time the signal is emitted. Still reading from the docs, QuerySet.delete will emit post_delete signal for every deleted object, and garbage_collect gets called after Bar has been deleted.
To the questions then:
Is there a better way of garbage collecting the one side of a one-to-many relationship?
If not, what should I change to be able to use QuerySet.delete?
By checking code in delete() inside django/db/models/deletion.py, I found the QuerySet.delete deletes collected instances in batch and THEN trigger post_delete for those deleted instances. If you delete Bar() in the first post_delete calling for the first deleted Foo() instance, later post_delete of Foo() instances will be failed because the Bar() which they point to has already been deleted.
The key here is that Foo()s having same bar does not point to the same Bar() instance, and the bar gets deleted too early. Then we could
straightly try...except the lookup of instance.bar
def garbage_collect(sender, instance, **kwargs):
try:
if instance.bar.foo_set.exists():
instance.bar.delete()
except Bar.DoesNotExist:
pass
preload Bar() for each instances to avoid the above exception
def test_queryset_post_delete(self):
Foo.objects.select_related('bar').delete()
def garbage_collect(sender, instance, **kwargs):
if instance.bar.foo_set.exists():
instance.bar.delete()
Both of above solutions do extra SELECT queries. The more graceful ways could be
Do the deletion of Bar always in garbage_collect or manually later, if you can:
Bar.objects.filter(foo__isnull=True).delete()
In garbage_collect, record the deletion plan for Bar() instead of deleting, to some ref-count flag or queued tasks.
I ques you can override the model's method delete, find the related objects and delete them too.

Categories