Aruj
Aruj

Reputation: 82

Django CASCADE and post_delete interaction

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

Upvotes: 1

Views: 2653

Answers (1)

heemayl
heemayl

Reputation: 42007

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.

Upvotes: 2

Related Questions