#31475: Recursion issue when deleting related objects
-------------------------------------+-------------------------------------
               Reporter:  Matt Drew  |          Owner:  nobody
                   Type:  Bug        |         Status:  new
              Component:  Database   |        Version:  3.0
  layer (models, ORM)                |
               Severity:  Normal     |       Keywords:
           Triage Stage:             |      Has patch:  0
  Unreviewed                         |
    Needs documentation:  0          |    Needs tests:  0
Patch needs improvement:  0          |  Easy pickings:  0
                  UI/UX:  0          |
-------------------------------------+-------------------------------------
 I ran into this situation yesterday... Basically, if you have a cascaded
 action such as a SET_NULL on a foreign key, you could end up with a
 RecursionError.

 In English: ClassA is a thing.  ClassB has a foreign key to ClassA with
 on_delete=SET_NULL.  ClassC has a foreign key to ClassB with
 on_delete=CASCADE. When a ClassA is deleted, a query to get all the ClassB
 objects that link to ClassA happens in order to update the foreign key to
 NULL.  This query is optimized to only fetch the id field.

 That works fine by itself, but when ClassB has an __init__() that
 references at least two non-id fields, you get into a loop. When the
 objects are constructed further queries will be required to fetch the
 fields referenced in __init__(), but each of those only tries to fetch a
 single field and instantiate a ClassB with that field, whereupon it finds
 that it also needs a second ClassB field, so it queries for that field
 (and ONLY that field), which tries to instantiate a ClassB object with
 just the second field, which in __init__() requires the first field so it
 starts another query for the first field, ...

 {{{
 class ClassA(models.Model):
     some_value = models.CharField(max_length=30)


 class ClassB(models.Model):
     foo = models.CharField(max_length=30)
     bar = models.CharField(max_length=30)
     my_a = models.ForeignKey(ClassA, null=True, on_delete=SET_NULL)

     def __init__(self, *args, **kwargs):
         super().__init__(*args, **kwargs)
         self._initial_foo = self.foo # Reason for needing this is not
 shown
         self._initial_bar = self.bar


 class ClassC(models.Model):
     baz = models.CharField(max_length=30)
     my_b = models.ForeignKey(ClassB, null=True, on_delete=CASCADE)

 a = ClassA(some_value='hi bob')
 a.save()
 b = ClassB(my_a=a, foo='foo', bar='bar')
 b.save()
 c = ClassC(my_b=b, baz='baz')
 c.save()
 ClassA.objects.all().delete()
 }}}
 Result: RecursionError

 If I add a bogus post-delete to ClassB, then django will fetch all the
 ClassB fields in the query to update the ClassB.my_a references to NULL
 when a ClassA is deleted, and the recursion doesn't happen.

 {{{
 @receiver(models.signals.post_delete, sender=ClassB)
 def do_nothing(sender, *args, **kwargs):
     pass
 }}}

 I'm not sure how best to address this.  Maybe some Meta field that lets
 you specify the minimum fields needed to instantiate an object? Maybe just
 don't try to only fetch the id column? I'll leave it to you.

-- 
Ticket URL: <https://code.djangoproject.com/ticket/31475>
Django <https://code.djangoproject.com/>
The Web framework for perfectionists with deadlines.

-- 
You received this message because you are subscribed to the Google Groups 
"Django updates" group.
To unsubscribe from this group and stop receiving emails from it, send an email 
to [email protected].
To view this discussion on the web visit 
https://groups.google.com/d/msgid/django-updates/050.a9fa761e3e85cec129db901e72cc3d01%40djangoproject.com.

Reply via email to