1

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).

Aruj
  • 82
  • 1
  • 10
  • Does this answer your question? [Django cascade delete and post\_delete signal](https://stackoverflow.com/questions/27566614/django-cascade-delete-and-post-delete-signal) – J Eti Jan 29 '21 at 09:34

1 Answers1

2

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.

heemayl
  • 39,294
  • 7
  • 70
  • 76
  • That solved the problem, thanks. Now, how would you change the last try block if there would be more than one possible sender? The A model is sort of glue layer between different entities, so to not write the same `post_delete` for each of them, I want to have a single one with several senders. – Aruj Dec 01 '19 at 08:01
  • I settled for `a.__getattribute__(sender.a.field.related_query_name())`, is there a "prettier" way? – Aruj Dec 01 '19 at 08:31
  • 1
    @Aruj Please check my edits. Don't use `__getattribute__` directly, it is automatically consulted first when doing any object's attribute lookup (which actually follows a pre-defined chain). So the only thing you should be using is `getattr` like I've shown, it's there so that you don't have to manually call any dunder method yourself (and if you don't want you don't have to care about all the lower-level details as well). – heemayl Dec 01 '19 at 11:13