#34791: Issue when using Prefetch objects in prefetch_related
-------------------------------------+-------------------------------------
     Reporter:  Maxime Toussaint     |                    Owner:  nobody
         Type:  Bug                  |                   Status:  new
    Component:  Database layer       |                  Version:  4.2
  (models, ORM)                      |
     Severity:  Normal               |               Resolution:
     Keywords:  prefetch,            |             Triage Stage:
  prefetch_related                   |  Unreviewed
    Has patch:  0                    |      Needs documentation:  0
  Needs tests:  0                    |  Patch needs improvement:  0
Easy pickings:  0                    |                    UI/UX:  0
-------------------------------------+-------------------------------------
Description changed by Maxime Toussaint:

Old description:

> There seems to be an issue when using a Prefetch object to fetch
> something that has already been fetched. Here is an example: so let's say
> I have a Pizza and some Toppings. Now I want to get all the toppings, but
> for some reason I also want to separately fetch only the toppings that
> are out of stock. I could do something like:
>

> {{{
> queryset = Pizza.objects.all().prefetch_related(
>     'toppings',
>     Prefetch('toppings',
> queryset=Topping.objects.filter(is_in_stock=False),
> to_attr='out_of_stock_toppings'),
> )
> }}}
>
> This looks good, but if we run it, it will fail, saying
> out_of_stock_toppings is not an attribute of Pizza. However, if I were to
> do it like this instead:
> {{{
> queryset = Pizza.objects.all().prefetch_related(
>     Prefetch('toppings',
> queryset=Topping.objects.filter(is_in_stock=False),
> to_attr='out_of_stock_toppings'),
>     'toppings',
> )
> }}}
>
> then all works fine. Looking at the code, this seems to be because the
> name used by a field to validate the cache is not the name used to store
> the data on the model, but rather simply the name of the field, so it
> collides. I have not tested it, but I think in the second example, the
> data returned will actually be the filtered data, not the full expected
> queryset. Note that this is my first time reading that part of the code,
> so there could be things I missed.
>
> Now this is a bit of a nonsensical example, but when using rest_framework
> with serializers, this type of situation could come up, where one
> serializer needs it formatted a certain way and this issue could arise
> (it has for me).
>
> I am not sure what the best way to fix this would be, but I feel like
> setting a to_attr should make the cache take that new field name into
> account instead of the field name.

New description:

 Note: Edited the description following the discussion

 There seems to be an issue when using a Prefetch object to fetch something
 that has already been fetched. The issue only seems to happen when there
 is depth in the prefetch. Here is an example I made this morning that
 fails:
 {{{
         pizzas = Pizza.objects.all().prefetch_related(
             "toppings__origin",
             Prefetch(
                 "toppings__origin",
                 queryset=Country.objects.filter(label="China"),
                 to_attr="china",
             ),
         )

         china = pizzas[0].toppings.all()[0].china
 }}}
 Here, when trying to get china, I would assume it to either be a Country
 object or None. However, I get the message: AttributeError: 'Topping'
 object has no attribute 'china'

 Here are the models I set up for my test:
 {{{
 class Country(models.Model):
     label = models.CharField(max_length=50)


 class Pizza(models.Model):
     label = models.CharField(max_length=50)


 class Topping(models.Model):
     pizza = models.ForeignKey(Pizza, on_delete=models.CASCADE,
 related_name="toppings")
     label = models.CharField(max_length=50)
     origin = models.ForeignKey(
         Country, on_delete=models.CASCADE, related_name="toppings"
     )
 }}}
 And here are the queries made when calling the queryset:
 {{{
 1. SELECT "tests_pizza"."id", "tests_pizza"."label",
 "tests_pizza"."provenance_id" FROM "tests_pizza"
 2. SELECT "tests_topping"."id", "tests_topping"."pizza_id",
 "tests_topping"."label", "tests_topping"."origin_id" FROM "tests_topping"
 WHERE "tests_topping"."pizza_id" IN (1, 2)
 3. SELECT "tests_country"."id", "tests_country"."label",
 "tests_country"."continent_id" FROM "tests_country" WHERE
 "tests_country"."id" IN (2, 3, 4)
 }}}
 Note that the filter by label='china' has completely disappeared.

 Now, if I switch the prefetches around like so:
 {{{
         pizzas = Pizza.objects.all().prefetch_related(
             Prefetch(
                 "toppings__origin",
                 queryset=Country.objects.filter(label="China"),
                 to_attr="china",
             ),
             "toppings__origin",
         )
 }}}
 Fetching china now works, and here are the queries being made:
 {{{
 1. SELECT "tests_pizza"."id", "tests_pizza"."label",
 "tests_pizza"."provenance_id" FROM "tests_pizza"
 2. SELECT "tests_topping"."id", "tests_topping"."pizza_id",
 "tests_topping"."label", "tests_topping"."origin_id" FROM "tests_topping"
 WHERE "tests_topping"."pizza_id" IN (1, 2)
 3. SELECT "tests_country"."id", "tests_country"."label",
 "tests_country"."continent_id" FROM "tests_country" WHERE
 ("tests_country"."label" = 'China' AND "tests_country"."id" IN (2, 3, 4))
 4. SELECT "tests_country"."id", "tests_country"."label",
 "tests_country"."continent_id" FROM "tests_country" WHERE
 "tests_country"."id" IN (2, 3, 4)
 }}}
 This time, both calls to Country were made.

 I did follow the code a bit yesterday, and I believe it comes from the
 fact that the ForeignKey field defines the cache name as being simply the
 name of the field. I am not certain though, and it would likely require
 someone with more knowledge than me to look into it.

--

-- 
Ticket URL: <https://code.djangoproject.com/ticket/34791#comment:5>
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/0107018a23f6151e-a5c5038b-b8ca-4e6d-b799-1c71264eaa5b-000000%40eu-central-1.amazonses.com.

Reply via email to