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.