Reputation: 82
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
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