tfoxy / graphene-django-optimizer

Optimize database access inside graphene queries
MIT License
427 stars 86 forks source link

prefetch_related to_attr not working #54

Closed lcasassa closed 4 years ago

lcasassa commented 4 years ago

How can I get the to_attr to work? It does not set the new attribute. I tried several ways including the one on the main README (docs).

models.py:

from django.db import models

class Part(models.Model):
    name = models.CharField(max_length=50, unique=True)

    def __str__(self):
        return self.name

class PartMapping(models.Model):
    name = models.CharField(max_length=50, unique=True)
    part = models.ForeignKey(Part, related_name='part_mappings', on_delete=models.PROTECT, default=None, null=True, blank=True)

    def __str__(self):
        if self.part:
            return self.part.name
        return f"NM-{self.name}"

class Supplier(models.Model):
    name = models.CharField(max_length=255, unique=True)

    def __str__(self):
        return self.name

class SupplierMapping(models.Model):
    name = models.CharField(max_length=255, unique=True)
    supplier = models.ForeignKey(Supplier, related_name='supplier_mappings', on_delete=models.PROTECT, default=None, null=True, blank=True)

    def __str__(self):
        if self.supplier:
            return self.supplier.name
        return f"NM-{self.name}"

class PartSupplierLink(models.Model):
    part_mapping = models.ForeignKey(PartMapping, related_name='part_supplier_links', on_delete=models.PROTECT)
    supplier_mapping = models.ForeignKey(SupplierMapping, related_name='part_supplier_links', on_delete=models.PROTECT)

    def __str__(self):
        return f"{str(self.part_mapping)} - {str(self.supplier_mapping)}"

shema.py

import graphene
from django.db.models import Prefetch
from graphene import ObjectType, ConnectionField
from graphene_django import DjangoObjectType
import graphene_django_optimizer as gql_optimizer

from supplychain.models import Part, PartMapping, Supplier, SupplierMapping, PartSupplierLink

def prefetch_suppliers(info, *_args, **_kwargs):
    return Prefetch(
        "part_mappings__part_supplier_links__supplier_mapping__supplier", to_attr="prefetched_suppliers"
    )

class PartType(gql_optimizer.OptimizedDjangoObjectType):
    class Meta:
        model = Part
        fields = ("id", "name")

    suppliers = gql_optimizer.field(
        graphene.List(lambda: SupplierType), prefetch_related=prefetch_suppliers
    )

    def resolve_suppliers(root, _info, **_kwargs):
        if hasattr(root, "prefetched_suppliers"):           # This is false when it should be true because of to_attr in the prefech_related
            return root.prefetched_suppliers
        return None #Supplier.objects.filter(supplier_mappings__part_supplier_links__part_mapping__part=root)

class SupplierType(gql_optimizer.OptimizedDjangoObjectType):
    class Meta:
        model = Supplier
        fields = ("id", "name")

class Query(graphene.ObjectType):
    all_parts = graphene.List(PartType)

    def resolve_all_parts(root, info):
        query = gql_optimizer.query(Part.objects.all(), info)
        return query

schema = graphene.Schema(query=Query)

resolve_suppliers on shema.py is not finding the attribute prefetched_suppliers set by the to_attr on the prefetch.

Versions used:

What am I doing wrong? Is this a bug?

Thanks

lcasassa commented 4 years ago

Maybe the multi-table-hop is not supported? if so, any recommendations on how to implement this? I could give it a try and do a PR.

lcasassa commented 4 years ago

This has something to do with the prefetch in django.

>>> q=Part.objects.prefetch_related(Prefetch('part_mappings', to_attr='pms'))
>>> q[0].pms
[<PartMapping: KH1234>]
>>> q=Part.objects.prefetch_related(Prefetch('part_mappings__part_supplier_links', to_attr='pms_psls'))
>>> q[0].pms_psls
Traceback (most recent call last):
  File "<console>", line 1, in <module>
AttributeError: 'Part' object has no attribute 'pms_psls'
>>> 

It is not able to do double reverse hops.

lcasassa commented 4 years ago

I will continue debugging here as this could be helpful to someone else. Hope you don't mind.

>>> import logging
>>> l = logging.getLogger('django.db.backends')
>>> l.setLevel(logging.DEBUG)
>>> l.addHandler(logging.StreamHandler())

>>> from supplychain.models import Part, Supplier
>>> from django.db.models import Prefetch
>>> q=Part.objects.prefetch_related(Prefetch('part_mappings__part_supplier_links__supplier_mapping__supplier', to_attr='s'))
>>> def get_data(q):
...   for part in q:
...     print("get another part")
...     for part_mapping in part.part_mappings.all():
...       print("get another part mapping")
...       for part_supplier_link in part_mapping.part_supplier_links.all():
...         print("get another part supplier link")
...         print(part_supplier_link.supplier_mapping.supplier.name)
>>> get_data(q)
(0.000) SELECT "supplychain_part"."id", "supplychain_part"."name" FROM "supplychain_part"; args=()
(0.000) SELECT "supplychain_partmapping"."id", "supplychain_partmapping"."name", "supplychain_partmapping"."part_id" FROM "supplychain_partmapping" WHERE "supplychain_partmapping"."part_id" IN (1, 2); args=(1, 2)
(0.000) SELECT "supplychain_partsupplierlink"."id", "supplychain_partsupplierlink"."part_mapping_id", "supplychain_partsupplierlink"."supplier_mapping_id" FROM "supplychain_partsupplierlink" WHERE "supplychain_partsupplierlink"."part_mapping_id" IN (1, 2); args=(1, 2)
(0.000) SELECT "supplychain_suppliermapping"."id", "supplychain_suppliermapping"."name", "supplychain_suppliermapping"."supplier_id" FROM "supplychain_suppliermapping" WHERE "supplychain_suppliermapping"."id" IN (1); args=(1,)
(0.000) SELECT "supplychain_supplier"."id", "supplychain_supplier"."name" FROM "supplychain_supplier" WHERE "supplychain_supplier"."id" IN (1); args=(1,)
get another part
get another part mapping
get another part supplier link
(0.000) SELECT "supplychain_supplier"."id", "supplychain_supplier"."name" FROM "supplychain_supplier" WHERE "supplychain_supplier"."id" = 1 LIMIT 21; args=(1,)
Super
get another part
get another part mapping
get another part supplier link
Super
>>> 

You can see that everything is prefetched except the name of the supplier. yahoo. let's see how to prefetch it.

lcasassa commented 4 years ago

Testing the same with more data:

>>> from django.db import reset_queries
>>> reset_queries()
>>> q=Part.objects.prefetch_related(Prefetch('part_mappings__part_supplier_links__supplier_mapping__supplier', to_attr='s'))
>>> get_data(q)
(0.000) SELECT "supplychain_part"."id", "supplychain_part"."name" FROM "supplychain_part"; args=()
(0.000) SELECT "supplychain_partmapping"."id", "supplychain_partmapping"."name", "supplychain_partmapping"."part_id" FROM "supplychain_partmapping" WHERE "supplychain_partmapping"."part_id" IN (1, 2); args=(1, 2)
(0.000) SELECT "supplychain_partsupplierlink"."id", "supplychain_partsupplierlink"."part_mapping_id", "supplychain_partsupplierlink"."supplier_mapping_id" FROM "supplychain_partsupplierlink" WHERE "supplychain_partsupplierlink"."part_mapping_id" IN (1, 2, 3); args=(1, 2, 3)
(0.000) SELECT "supplychain_suppliermapping"."id", "supplychain_suppliermapping"."name", "supplychain_suppliermapping"."supplier_id" FROM "supplychain_suppliermapping" WHERE "supplychain_suppliermapping"."id" IN (1, 2, 3); args=(1, 2, 3)
(0.000) SELECT "supplychain_supplier"."id", "supplychain_supplier"."name" FROM "supplychain_supplier" WHERE "supplychain_supplier"."id" IN (1, 2, 3); args=(1, 2, 3)
get another part
get another part mapping
get another part supplier link
(0.000) SELECT "supplychain_supplier"."id", "supplychain_supplier"."name" FROM "supplychain_supplier" WHERE "supplychain_supplier"."id" = 1 LIMIT 21; args=(1,)
Super
get another part
get another part mapping
get another part supplier link
Super
get another part supplier link
(0.000) SELECT "supplychain_supplier"."id", "supplychain_supplier"."name" FROM "supplychain_supplier" WHERE "supplychain_supplier"."id" = 2 LIMIT 21; args=(2,)
Duper
get another part mapping
get another part supplier link
(0.000) SELECT "supplychain_supplier"."id", "supplychain_supplier"."name" FROM "supplychain_supplier" WHERE "supplychain_supplier"."id" = 3 LIMIT 21; args=(3,)
Triple
>>> 

I can confirm: You can see that everything is prefetched except the name of the supplier.

lcasassa commented 4 years ago

It's working!!

The solution is "Prefetch" in cascade :)


def prefetch_suppliers(info, *_args, **_kwargs):
    return Prefetch(
        "part_mappings", queryset=PartMapping.objects.all().prefetch_related(
            Prefetch(
                "part_supplier_links", queryset=PartSupplierLink.objects.all().prefetch_related(
                    Prefetch(
                        "supplier_mapping", queryset=SupplierMapping.objects.all().prefetch_related(
                            Prefetch(
                                "supplier", queryset=Supplier.objects.all()
                            )
                        )
                    )
                )
            )
        )
    )

class PartType(gql_optimizer.OptimizedDjangoObjectType):
    class Meta:
        model = Part
        fields = ("id", "name")

    suppliers = gql_optimizer.field(
        graphene.List(lambda: SupplierType), prefetch_related=prefetch_suppliers
    )

    def resolve_suppliers(part, _info, **_kwargs):
        suppliers = []
        for part_mapping in part.part_mappings.all():
            for part_supplier_link in part_mapping.part_supplier_links.all():
                suppliers.append(part_supplier_link.supplier_mapping.supplier)

        return suppliers

image

image

lcasassa commented 4 years ago

Source of the solution was found on the last comment of: https://www.reddit.com/r/django/comments/evjmx2/filtering_prefetch_object_using_outerref_and/