#34634: Creating objects with nested MTI crashes.
-------------------------------------+-------------------------------------
     Reporter:  Mariusz Felisiak     |                    Owner:  Akash
                                     |  Kumar Sen
         Type:  Bug                  |                   Status:  assigned
    Component:  Database layer       |                  Version:  4.2
  (models, ORM)                      |
     Severity:  Normal               |               Resolution:
     Keywords:                       |             Triage Stage:  Accepted
    Has patch:  1                    |      Needs documentation:  0
  Needs tests:  0                    |  Patch needs improvement:  0
Easy pickings:  0                    |                    UI/UX:  0
-------------------------------------+-------------------------------------

Comment (by Simon Charette):

 The empty string insertions are due to a bug in `Options._get_fields` due
 to how its caching strategy doesn't take `seen_models` into accounts even
 if it can greatly influence the value stashed in the cache.

 Because of that fields inherited from `Place` are present twice in
 `ItalianRestaurantManyParents.meta.fields` which breaks `Model.__init__`

 {{{#!python
 class ItalianRestaurantManyParents(ItalianRestaurant, Place):
     place_two_ptr = models.OneToOneField(
         Place, on_delete=models.CASCADE, parent_link=True
     )

 >>> ItalianRestaurantManyParents._meta.fields
 (<django.db.models.fields.AutoField: id>,
  <django.db.models.fields.CharField: name>,
  <django.db.models.fields.CharField: address>,
  <django.db.models.fields.related.OneToOneField: place_ptr>,
  <django.db.models.fields.IntegerField: rating>,
  <django.db.models.fields.BooleanField: serves_hot_dogs>,
  <django.db.models.fields.BooleanField: serves_pizza>,
  <django.db.models.fields.related.ForeignKey: chef>,
  <django.db.models.fields.related.OneToOneField: restaurant_ptr>,
  <django.db.models.fields.BooleanField: serves_gnocchi>,
  <django.db.models.fields.AutoField: id>,  # dupe, already inherited from
 Place
  <django.db.models.fields.CharField: name>,   # dupe, already inherited
 from Place
  <django.db.models.fields.CharField: address>,   # dupe, already inherited
 from Place
  <django.db.models.fields.related.OneToOneField: italianrestaurant_ptr>,
  <django.db.models.fields.related.OneToOneField: place_two_ptr>)
 }}}

 But if you clear the options cache

 {{{#!python
 >>> ItalianRestaurantManyParents._meta._expire_cache()
 >>> ItalianRestaurant._meta._expire_cache()
 >>> Restaurant._meta._expire_cache()
 >>> Rating._meta._expire_cache()
 >>> Place._meta._expire_cache()
 >>> ItalianRestaurantManyParents._meta.fields
 (<django.db.models.fields.AutoField: id>,
  <django.db.models.fields.CharField: name>,
  <django.db.models.fields.CharField: address>,
  <django.db.models.fields.related.OneToOneField: place_ptr>,
  <django.db.models.fields.IntegerField: rating>,
  <django.db.models.fields.BooleanField: serves_hot_dogs>,
  <django.db.models.fields.BooleanField: serves_pizza>,
  <django.db.models.fields.related.ForeignKey: chef>,
  <django.db.models.fields.related.OneToOneField: restaurant_ptr>,
  <django.db.models.fields.BooleanField: serves_gnocchi>,
  <django.db.models.fields.related.OneToOneField: italianrestaurant_ptr>,
  <django.db.models.fields.related.OneToOneField: place_two_ptr>)
 }}}

 Things are right again!

 My initial attempt at solving the issue


 {{{#!diff
 diff --git a/django/db/models/options.py b/django/db/models/options.py
 index 00735e0de1..2f46df992e 100644
 --- a/django/db/models/options.py
 +++ b/django/db/models/options.py
 @@ -864,7 +864,7 @@ def _get_fields(
          reverse=True,
          include_parents=True,
          include_hidden=False,
 -        seen_models=None,
 +        topmost_call=True,
      ):
          """
          Internal helper function to return fields of the model.
 @@ -885,13 +885,6 @@ def _get_fields(
          # implementation and to provide a fast way for Django's internals
 to
          # access specific subsets of fields.

 -        # We must keep track of which models we have already seen.
 Otherwise we
 -        # could include the same field multiple times from different
 models.
 -        topmost_call = seen_models is None
 -        if topmost_call:
 -            seen_models = set()
 -        seen_models.add(self.model)
 -
          # Creates a cache key composed of all arguments
          cache_key = (forward, reverse, include_parents, include_hidden,
 topmost_call)

 @@ -906,12 +899,11 @@ def _get_fields(
          # Recursively call _get_fields() on each parent, with the same
          # options provided in this call.
          if include_parents is not False:
 +            # In diamond inheritance it is possible that we see the same
 +            # field from two different routes. In that case, avoid adding
 +            # fields from the same parent again.
 +            parent_fields = set()
              for parent in self.parents:
 -                # In diamond inheritance it is possible that we see the
 same
 -                # model from two different routes. In that case, avoid
 adding
 -                # fields from the same parent again.
 -                if parent in seen_models:
 -                    continue
                  if (
                      parent._meta.concrete_model != self.concrete_model
                      and include_parents == PROXY_PARENTS
 @@ -922,13 +914,14 @@ def _get_fields(
                      reverse=reverse,
                      include_parents=include_parents,
                      include_hidden=include_hidden,
 -                    seen_models=seen_models,
 +                    topmost_call=False,
                  ):
                      if (
                          not getattr(obj, "parent_link", False)
                          or obj.model == self.concrete_model
 -                    ):
 +                    ) and obj not in parent_fields:
                          fields.append(obj)
 +                        parent_fields.add(obj)
          if reverse and not self.proxy:
              # Tree is computed once and cached until the app cache is
 expired.
              # It is composed of a list of fields pointing to the current
 model
 }}}

 So in order to address I issue I think a plan would be
 1. Wait for #33414 to be merged as it affects the number of queries
 performed on creation
 2. Adjust the system check that detects field collisions to catch the case
 in the initial report
 3. Merge the changes to `Options._get_fields` with the test added by
 Mariusz with the small adjustment to the model mentioned in comment:10 to
 make sure it passes the adjusted test added in 2.

 Does that make sense to you?

-- 
Ticket URL: <https://code.djangoproject.com/ticket/34634#comment:15>
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/01070188bd8dd80d-21fd9f99-3366-43b4-aeb1-ab2b14073653-000000%40eu-central-1.amazonses.com.

Reply via email to