Git Product home page Git Product logo

graphene-django-optimizer's Introduction

graphene-django-optimizer

build status coverage PyPI version python version django version

Optimize queries executed by graphene-django automatically, using select_related, prefetch_related and only methods of Django QuerySet.

Install

pip install graphene-django-optimizer

Note: If you are using Graphene V2, please install version 0.8. v0.9 and forward will support only Graphene V3

Usage

Having the following schema based on the tutorial of graphene-django (notice the use of gql_optimizer)

# cookbook/ingredients/schema.py
import graphene

from graphene_django.types import DjangoObjectType
import graphene_django_optimizer as gql_optimizer

from cookbook.ingredients.models import Category, Ingredient


class CategoryType(DjangoObjectType):
    class Meta:
        model = Category


class IngredientType(DjangoObjectType):
    class Meta:
        model = Ingredient


class Query(graphene.ObjectType):
    all_categories = graphene.List(CategoryType)
    all_ingredients = graphene.List(IngredientType)

    def resolve_all_categories(root, info):
        return gql_optimizer.query(Category.objects.all(), info)

    def resolve_all_ingredients(root, info):
        return gql_optimizer.query(Ingredient.objects.all(), info)

We will show some graphql queries and the queryset that will be executed.

Fetching all the ingredients with the related category:

{
  allIngredients {
    id
    name
    category {
      id
      name
    }
  }
}
# optimized queryset:
ingredients = (
    Ingredient.objects
    .select_related('category')
    .only('id', 'name', 'category__id', 'category__name')
)

Fetching all the categories with the related ingredients:

{
  allCategories {
    id
    name
    ingredients {
      id
      name
    }
  }
}
# optimized queryset:
categories = (
    Category.objects
    .only('id', 'name')
    .prefetch_related(Prefetch(
        'ingredients',
        queryset=Ingredient.objects.only('id', 'name'),
    ))
)

Advanced usage

Sometimes we need to have a custom resolver function. In those cases, the field can't be auto optimized. So we need to use gql_optimizer.resolver_hints decorator to indicate the optimizations.

If the resolver returns a model field, we can use the model_field argument:

import graphene
import graphene_django_optimizer as gql_optimizer


class ItemType(gql_optimizer.OptimizedDjangoObjectType):
    product = graphene.Field('ProductType')

    @gql_optimizer.resolver_hints(
        model_field='product',
    )
    def resolve_product(root, info):
        # check if user have permission for seeing the product
        if info.context.user.is_anonymous():
            return None
        return root.product

This will automatically optimize any subfield of product.

Now, if the resolver uses related fields, you can use the select_related argument:

import graphene
import graphene_django_optimizer as gql_optimizer


class ItemType(gql_optimizer.OptimizedDjangoObjectType):
    name = graphene.String()

    @gql_optimizer.resolver_hints(
        select_related=('product', 'shipping'),
        only=('product__name', 'shipping__name'),
    )
    def resolve_name(root, info):
        return '{} {}'.format(root.product.name, root.shipping.name)

Notice the usage of the type OptimizedDjangoObjectType, which enables optimization of any single node queries.

Finally, if your field has an argument for filtering results, you can use the prefetch_related argument with a function that returns a Prefetch instance as the value.

from django.db.models import Prefetch
import graphene
import graphene_django_optimizer as gql_optimizer


class CartType(gql_optimizer.OptimizedDjangoObjectType):
    items = graphene.List(
        'ItemType',
        product_id=graphene.ID(),
    )

    @gql_optimizer.resolver_hints(
        prefetch_related=lambda info, product_id: Prefetch(
            'items',
            queryset=gql_optimizer.query(Item.objects.filter(product_id=product_id), info),
            to_attr='gql_product_id_' + product_id,
        ),
    )
    def resolve_items(root, info, product_id):
        return getattr(root, 'gql_product_id_' + product_id)

With these hints, any field can be optimized.

Optimize with non model fields

Sometimes we need to have a custom non model fields. In those cases, the optimizer would not optimize with the Django .only() method. So if we still want to optimize with the .only() method, we need to use disable_abort_only option:

class IngredientType(gql_optimizer.OptimizedDjangoObjectType):
    calculated_calories = graphene.String()

    class Meta:
        model = Ingredient

    def resolve_calculated_calories(root, info):
        return get_calories_for_ingredient(root.id)


class Query(object):
    all_ingredients = graphene.List(IngredientType)

    def resolve_all_ingredients(root, info):
        return gql_optimizer.query(Ingredient.objects.all(), info, disable_abort_only=True)

Contributing

See CONTRIBUTING.md

graphene-django-optimizer's People

Contributors

bellini666 avatar cramshaw avatar dependabot[bot] avatar dex4er avatar felixmeziere avatar iorlandini avatar jackton1 avatar juyrjola avatar maarcingebala avatar maxpeterson avatar mekhami avatar nikolaik avatar omegadroid avatar pacu2 avatar tfoxy avatar ulgens avatar yardensachs avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar

graphene-django-optimizer's Issues

Optimizing queries which are part of standard graphene.ObjectType

Hi! First off thanks for this module, it's really great, and for the release yesterday, I'd just come across the prefetch/select related bug and it was fixed before I could report it!

So I have been playing extensively and want to use this alongside some sort of pagination. Currently I have a query that looks like:

  wi4(page: 2) {
    totalCount
    page
    results {
      idInstance
      image {
         idImage
      }
    }
  }

Where results is a Django queryset.

To make this work I have a resolver which does:

class Query:
    wi_4 = graphene.Field(
        PaginatedInstanceType, page=graphene.Int())

    def resolve_wi_1(self, info, page=1, **kwargs):
        qs = gql_optimizer.query(
            Instance.objects.all(), info)
        return get_paginator(qs, PaginatedInstanceType, page=page)

and the type:

class PaginatedInterface(graphene.Interface):
    page = graphene.Int()
    pages = graphene.Int()
    has_next = graphene.Boolean()
    has_prev = graphene.Boolean()
    total_count = graphene.Int()
    results = None

class PaginatedInstanceType(graphene.ObjectType):
    results = graphene.List(InstanceType)

    class Meta:
        interfaces = (PaginatedInterface,)

Where InstanceType is a standard django-graphene DjangoObjectType.

Unfortunately this seems to result in the optimizer being disregarded and it runs n+1 queries. I've tried the recommended advanced usages but they all seem to rely on the ObjectType being an actual DjangoObjectType, which doesn't work here as I don't have a model at the top level.

I have found that if I 'trick' it by settings the results selection as the main selection, the field_name to results and the parent type to the GraphQLObject of the PaginatedInstanceType (passed in here as object_type) then it all works beautifully.

       results = filter(
            lambda x: x.name.value == 'results', info.field_asts[0].selection_set.selections)
        info.field_asts = list(results)

        info.parent_type = info.schema.get_type(
            str(object_type))

        info.field_name = 'results'

Before I start trying to make any changes, I wanted to check that this isn't currently supported and I haven't just missed how to go about it?

Thanks!

django filter

How to use this package beside django filter can you give an example?

prefetch_related to_attr not working

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:

  • Django==3.1
  • graphene-djang==2.12.1
  • graphene-django-optimizer==0.6.2

What am I doing wrong? Is this a bug?

Thanks

How to update info.field_nodes[0].selection_set?

Context

have a GraphQL query that looks like this.

query One {
  library(id: "library_1") {
    name
    book {
      title
      __typename
    }
    __typename
  }
}

When I investigate info object info.field_nodes[0].selection_set.selections I got selection set of FieldNodes that looks like that:

<class 'graphql.language.ast.SelectionSetNode'>
<class 'graphql.pyutils.frozen_list.FrozenList'> [FieldNode at 45:49, FieldNode at 54:95, FieldNode at 100:110]


# FieldNode at 45:49
name

# FieldNode at 54:95
book {
  title
  __typename
}

# FieldNode at 100:110
__typename

Problem

In my second query set I added results field to wrap my data.

query All {
  allLibrary {
    results {
      name
      book {
        title
        __typename
      }
      __typename
    }
    __typename
  }
}

Unfortunately, this causes a problem because query set is resolved from level of allLibrary not library as in the first example. The consequences of this structure is that info.field_nodes[0].selection_set.selections resolves fields incorrectly. Skiping, all the fields that I have interest in (name , book, __typename).

<class 'graphql.language.ast.SelectionSetNode'>
<class 'graphql.pyutils.frozen_list.FrozenList'> [FieldNode at 14:147, FieldNode at 133:143] 

# FieldNode at 31:128
results {
  name
  book {
    title
    __typename
  }
  __typename
}

# FieldNode at 133:143
__typename

Question

How I can fix my info.field_nodes[0].selection_set so it fetches the FieldNode correctly ?

Only not work with relay node

First, thanks for this library๐Ÿ˜.

When used with relay node, I found only optimize not worked. And the reason maybe this:

def _is_resolver_for_id_field(self, resolver):
resolve_id = DjangoObjectType.resolve_id
# For python 2 unbound method:
if hasattr(resolve_id, 'im_func'):
resolve_id = resolve_id.im_func
return resolver == resolve_id

resolver is functools.partial and it's func is GlobalID.id_resolver, it's args[0] is DjangoObjectType.resolve_id.

Environment:
Python: 3.7.3
Django: 2.2.1

Using the optimizer, I get the error "must be str, not Prefetch"

I have some django models:

class UserWorkspace(SafeDeleteModel):
    name = models.CharField(max_length=50)
    user = models.ForeignKey(User, on_delete=models.CASCADE)

    instance_groups = models.ManyToManyField(InstanceGroup)


class InstanceGroup(SafeDeleteModel):
    instances = models.ManyToManyField(project)


class project(SafeDeleteModel):
    """
    Projects
    """
    uploaded_file = models.ForeignKey('primavera.UploadedFile', on_delete=models.CASCADE)
    proj_id = models.IntegerField(null=True, help_text="FK to PROJECT table - identifies the project")
    # ...........................


class task(SafeDeleteModel):
    """
    Activities
    """
    uploaded_file = models.ForeignKey('primavera.UploadedFile', on_delete=models.CASCADE)
    task_id = models.IntegerField(null=True, help_text="FK to TASK table - identifies the task")
    proj_id = models.ForeignKey(null=True, help_text="FK to PROJECT table - identifies the project", to="project", on_delete=models.CASCADE, related_name="tasks")
    # ............................

The query looks like this:

def resolver_for_model(model):
    return lambda root, info, **kwargs: gql_optimizer.query(model.objects.all(), info)

class BaseQuery():
    all_user_workspace = graphene.List(type_for_model(UserWorkspace), resolver=resolver_for_model(UserWorkspace))

When I run this query:

{
  allUserWorkspace {
    id
    name
    instanceGroups {
      instances {
        lastRecalcDate
        id
        tasks {
          id
        }
      }
    }
  }
}

I get this error:

graphql.error.located_error.GraphQLLocatedError: must be str, not Prefetch

I looked at that code and tried to replicate the problem by running something like this:

Instance.objects.all().prefetch_related(Prefetch('tasks'))

This works just fine though. I'm not sure what could be causing this error.

Optimize queries for types with no base model

I'm currently using gql_optimizer.query in my resolvers to query for my objects of type OptimizedDjangoObjectType. However, I'm refactoring now so that the resolvers will derive from graphene.Union types so that I can return either the found object or an error in my queries.
The query looks like something along these lines:

query user {
  user(id: 1) {
    __typename
    ... on User {
      name
    }
    ... on Error {
      message
    }
  }
}

Right now, objects are:

class Error(OptimizedDjangoObjectType):
    message = graphene.NonNull(graphene.String)


class User(OptimizedDjangoObjectType):
    class Meta:
        model = account_models.User


class UserType(graphene.Union):
    class Meta:
        types = (User, Error)

And my query/resolver is:

class Query:
    user = graphene.Field(UserType, id=graphene.ID())

    def resolve_user(self, info, id):
        return gql_optimizer.query(User.objects.filter(pk=id), info)[0]

With the current functionality, my query breaks in QueryOptimizer._get_base_model because one of the Union possible_types (the Error OptimizedDjangoObjectType) Meta class does not declare a model.

Of course works if I add an Abstract Error model to my app, but that feels a little hacky since I don't actually need an Error model.

Would it be possible to add support for queries where the graphene_type does not have a base model?

Is Django 4.1.5 supported?

Hi, I was trying out the query optimizer for my POC but even a simple query returned an error.

class ProductType(DjangoObjectType):
    class Meta:
        model = Product
        fields = ALL_FIELDS


class Query(graphene.ObjectType):
    products = graphene.List(ProductType)

    def resolve_products(root, info):
        return gql_optimizer.query(Product.objects.all(), info)
{
  "errors": [
    {
      "message": "There is no current event loop in thread 'Thread-1 (process_request_thread)'.",
      "locations": [
        {
          "line": 2,
          "column": 3
        }
      ],
      "path": [
        "products"
      ]
    }
  ],
  "data": {
    "products": null
  }
}

And can you also please add an example of how to use it with the relay interface? When I tried it, it returned an error.

class ProductNode(DjangoObjectType):
    class Meta:
        model = Product
        filter_fields = "__all__"
        interfaces = (relay.Node,)


class Query(graphene.ObjectType):
    products = DjangoFilterConnectionField(ProductNode)

    def resolve_products(root, info):
        return gql_optimizer.query(Product.objects.all(), info)


{
  "errors": [
    {
      "message": "Cannot return null for non-nullable field BrandNodeConnection.edges.",
      "locations": [
        {
          "line": 3,
          "column": 5
        }
      ],
      "path": [
        "brands",
        "edges"
      ]
    }
  ],
  "data": {
    "brands": null
  }
}

python==3.10.8

Django==4.1.5
graphene==3.2.1
graphene-django==3.0.0
graphene-django-optimizer==0.9.1

Resolving variables in nested queries fails

I'm trying to run the query with a variable in a subquery (products(first: $first)):

query Test($id: ID, $first: Int) {
  category(id: $id) {
    products(first: $first) {
      edges {
        node {
          id
        }
      }
    }
  }
}

The above would fail with an error: 'Variable' object has no attribute 'value'. I did some debugging and it seems that the problem happens in this line.

It is assumed that arg.value has a value field, but instead it is of type Variable. The value would have to be looked up in info.variable_values.

If two different resolvers of a DjangoObjectType have hints with the same value in to_attr arg of Prefetch, Django raises an exception

If two different resolvers of a DjangoObjectType have hints with the same value in to_attr of Prefetch, then Django will raise an exception ("Lookup was already seen with a different queryset" from django.db.models.query.prefetch_related_objects).

What we would like instead is that graphene-django-optimizer lets us add those two hints but makes sure that, if both resolvers are needed in a query, it only adds the "common hint" once (therefore not leading to a duplicate prefetch that will raise an error).

The benefit of this is that we would be able to add duplicate hints where we want, to make sure the hints are triggered even when one of the fields is queried and not the other, while also allowing for both fields being queried together without duplicate prefetches nor exceptions being risen.

This PR solves the problem #76

Version 0.9.0 raises ImportError

When using these packages:

Package                        Version
------------------------------ ----------
graphene                       2.1.9
graphene-django                2.15.0
graphene-django-optimizer      0.9.0
graphql-core                   2.3.2
graphql-relay                  2.0.1

the following exception gets raised when running tests or accessing the GraphiQL interface:

Internal Server Error: /graphql/
Traceback (most recent call last):
  File "/home/andi/virtualenvs/smelt/lib/python3.8/site-packages/graphene_django/settings.py", line 80, in import_from_string
    module = importlib.import_module(module_path)
  File "/usr/lib/python3.8/importlib/__init__.py", line 127, in import_module
    return _bootstrap._gcd_import(name[level:], package, level)
  File "<frozen importlib._bootstrap>", line 1014, in _gcd_import
  File "<frozen importlib._bootstrap>", line 991, in _find_and_load
  File "<frozen importlib._bootstrap>", line 975, in _find_and_load_unlocked
  File "<frozen importlib._bootstrap>", line 671, in _load_unlocked
  File "<frozen importlib._bootstrap_external>", line 848, in exec_module
  File "<frozen importlib._bootstrap>", line 219, in _call_with_frames_removed
  File "/home/andi/workspace/smelt/smelt/smelt/schema.py", line 7, in <module>
    from smeltapp import schema
  File "/home/andi/workspace/smelt/smelt/smeltapp/schema.py", line 16, in <module>
    from graphene_django_optimizer import query, OptimizedDjangoObjectType, resolver_hints
  File "/home/andi/virtualenvs/smelt/lib/python3.8/site-packages/graphene_django_optimizer/__init__.py", line 2, in <module>
    from .query import query  # noqa: F401
  File "/home/andi/virtualenvs/smelt/lib/python3.8/site-packages/graphene_django_optimizer/query.py", line 11, in <module>
    from graphql import GraphQLResolveInfo, GraphQLSchema
ImportError: cannot import name 'GraphQLResolveInfo' from 'graphql' (/home/andi/virtualenvs/smelt/lib/python3.8/site-packages/graphql/__init__.py)

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/home/andi/virtualenvs/smelt/lib/python3.8/site-packages/django/core/handlers/exception.py", line 34, in inner
    response = get_response(request)
  File "/home/andi/virtualenvs/smelt/lib/python3.8/site-packages/django/core/handlers/base.py", line 115, in _get_response
    response = self.process_exception_by_middleware(e, request)
  File "/home/andi/virtualenvs/smelt/lib/python3.8/site-packages/django/core/handlers/base.py", line 113, in _get_response
    response = wrapped_callback(request, *callback_args, **callback_kwargs)
  File "/home/andi/virtualenvs/smelt/lib/python3.8/site-packages/django/views/decorators/csrf.py", line 54, in wrapped_view
    return view_func(*args, **kwargs)
  File "/home/andi/virtualenvs/smelt/lib/python3.8/site-packages/django/views/generic/base.py", line 62, in view
    self = cls(**initkwargs)
  File "/home/andi/virtualenvs/smelt/lib/python3.8/site-packages/graphene_django/views.py", line 104, in __init__
    schema = graphene_settings.SCHEMA
  File "/home/andi/virtualenvs/smelt/lib/python3.8/site-packages/graphene_django/settings.py", line 127, in __getattr__
    val = perform_import(val, attr)
  File "/home/andi/virtualenvs/smelt/lib/python3.8/site-packages/graphene_django/settings.py", line 66, in perform_import
    return import_from_string(val, setting_name)
  File "/home/andi/virtualenvs/smelt/lib/python3.8/site-packages/graphene_django/settings.py", line 89, in import_from_string
    raise ImportError(msg)
ImportError: Could not import 'smelt.schema.schema' for Graphene setting 'SCHEMA'. ImportError: cannot import name 'GraphQLResolveInfo' from 'graphql' (/home/andi/virtualenvs/smelt/lib/python3.8/site-packages/graphql/__init__.py).

With version 0.8.0 this exception does not get raised.

Support for DjangoConnectionField

I have a query and resolver using DjangoConnectionField as follows

  items = DjangoConnectionField(ItemType)

  def resolve_items(root, info, **kwargs):
       return gql_optimizer.query(Item.objects.all(), info)

The SQL not optimized, as I can see all fields are queried when inspecting the raw sql output via GraphQL.

Prefetch related in custom resolver

Hi, there is a way to improve the performance of a custom resolver like this one? I could not find it.

`

class MatchType(DjangoObjectType):
    accepted_attendances = graphene.Int()


class Meta:
    name = 'match'
    model = Match
    interfaces = (CustomNode,)
    connection_class = CountableConnectionBase
    only_fields = [
        'attendances',
    ]

    filter_fields = {
        'datetime': ['gte'],
    }

def resolve_accepted_attendances(self, info):
    return self.attendances.filter(status=AttendanceStatusConstants.ACCEPTED).count()

Prefetching for generic relations does not seem to work

Hi there,

first of all, thanks for the project, and I'd like to point out that prefetching works for ForeignKey and ManyToMany relations. Somehow only GenericForeignKey is not recognized as optimizable.

Versions

Package Version
Python 3.8
Django 2.2
graphene 2.1.9
graphene-django 2.15.0
graphene-django-optimizer 0.8.0
graphql-core 2.3.2
graphql-relay 2.0.1

Models

class Comment(models.Model):
    text = models.TextField(null=True, blank=True)
    when = models.DateTimeField(null=True)
    who = models.ForeignKey(User, null=True, on_delete=models.SET_NULL)
    remote_id = models.PositiveIntegerField(null=True)
    content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
    object_id = models.PositiveIntegerField()
    content_object = GenericForeignKey('content_type', 'object_id')


class CommentMixin(models.Model):
    comments = GenericRelation(Comment)

    class Meta:
        abstract = True


class Incident(CommentMixin, models.Model):
    incident_id = models.IntegerField(unique=True)

Schema

class CommentType(OptimizedDjangoObjectType):
    class Meta:
        model = models.Comment
        interfaces = (graphene.relay.Node,)


class IncidentType(OptimizedDjangoObjectType):
    comments = DjangoConnectionField(CommentType)

    @resolver_hints(model_field=models.Comment)
    def resolve_comments(self, info, **kwargs):
        return self.comments.all()

Query

{
  incidents(first: 20) {
    edges {
      node {
        id
        comments {
          edges {
            node {
              text
              when
              who {
                username
              }
            }
          }
        }
      }
    }
  }
}

Problem

For non-generic relations the optimization works as expected, but with a generic relation (in the above example: comments) it does not. Based on the raw SQL queries executed, it looks like for every incident the comments are queried and for each comment the related User model is queried.

Expected

Generic relations are tough. Django allows prefetching of the model that is a member of the generic relation, but nothing further, e.g.:

Incident.objects.all().prefetch_related("comments")  # works
Incident.objects.all().prefetch_related("comments", "comments__who")  # does not work

It would be unfair to expect that the optimizer overcomes this limitation of Django. It would be really great, if the optimizer could prefetch the comments in this example.

Adds more queries instead of optmizing

I have been trying to use this optimizer to optimize queries, but I am confused now as the number of queries increases instead.
Anyone with the same issue?

Django==3.2.16
Python==3.9.16

Handling of relay connections

Hello! First let me say, kudos for this library, it's a lifesaver :)

I have a question regarding Relay connections.

When I try to optimize queries involving relay connections, I get the following error:

graphql.error.located_error.GraphQLLocatedError: 'ConnectionOptions' object has no attribute 'model'

But AFAIK, I cannot specify a model on relay connections subclasses.

class TeamConnection(relay.Connection):
    class Meta(object):
        node = TeamNode
        model = TeamMembership  # this wont work

Do you have any idea about how I could make this work? I'd be glad to help with a PR if you give me some directions.

Resolver_Hints root None

Hey, i am trying to retreive a list of categories and her translations with a custom filter.
I tried to use a resolver_hint from the examples:
image

My models looks like that:
image

My Query is the following:
image

The response tells the following:
"NoneType' object has no attribute 'gql_language_code_de'"

As you see, the root variable is None:
image

Can you tell me whats wrong with my code?

Doesn't work with filterset_class

I'm using filterset_class with an OrderingFilter for my types and after adding gql_optimizer to my queries it adds one query more instead of reducing the amount.

Note: I'll try to figure out the source of the problem in the next days, maybe someone already knows something about this issue?

Permission to publish a port that is based on graphene-django-optimizer

Hello, i made a port of graphene-django-optimizer that is intended for use with strawberry-graphql:
https://github.com/energy-efficiency/strawberry-django-optimizer

I would like to ask for permission to make it available under MIT license. I also would like to puplish it on PyPi.
I already added a link to graphene-django-optimizer in the documentation. Is there anything else i should do to accredit the authors of graphene-django-optimizer?

Optimize pagination of relay connection fields

Copied from #1 (comment).

@tfoxy Do you use optimizers with any pagination? We're also using Apollo as the client and we want to provide cursor-based pagination. But we also have queries with nested connections e.g.:

{
  products(first: 10) {
    edges {
      node {
        name
        variants(first: 5) {
          edges {
            node {
              name
            }
          }
        }
      }
    }
  }
}

Using just the ORM I'm able to fetch the data only with two DB queries:

Product.objects.prefetch_related('variants')

I'm trying to achieve the same in the API but unfortunately any pagination seems to break the prefetches as they do slicing on querysets internally which results in refetching objects that were already cached by prefetch_related.

Including `id` field in query aborts "only" optimization

In my API I have a type which is based on Django's model using DjangoObjectType and relay.Node interface. Using the latter automatically adds the id (graphene.ID) field in the type.

class Product(DjangoObjectType):
    name = graphene.String()

    class Meta:
        interfaces = [relay.Node]
        model = models.Product

Now, when I query the API (with optimizer enabled) and include the id field in the query, the "only optimization" is aborted as a result of this check - my automatically added id field has no name. As a result, this function is called which sets the only_list to None and disables any future "only optimizations" I'd have in my case.

@tfoxy Could you validate the issue? Maybe I broke something on my side, but maybe it is a bug in the library.

Also, I don't fully understand why do you abort the optimization and set None to the only_list. I experimented with the code and assigned [] instead of None in that function and it solved the issue.

Drop support of django 1.10 and 1.9 and Python 3.4

Graphene no longer supports those versions and django doesn't either, 1.9 and 1.10 reached EOL in 2017.

We shouldn't support them anymore, anyone still using one of these version should upgrade to the latest LTS which still provide security and bug fixes.

Supporting them requires us more work as some behavior are different between versions and we can't be sure it will actually work as expected in production as graphene doesn't support them.

Same thing with Python 3.4, we can't say for sure it will work, graphene doesn't support it anymore and neither Python does (since March 2019).

TypeError("Cannot call select_related() after .values() or .values_list()")

Python version: 3.8
Django version: 3.1.14

I am a maintainer of Nautobot and we recently implemented the optimizer in our v1.2.0 release. We just got this bug report related to trying to query a related object and filtering only the id field for it, which results in this error:

{
  "errors": [
    {
      "message": "Cannot call only() after .values() or .values_list()",
      "locations": [
        {
          "line": 5,
          "column": 3
        }
      ],
      "path": [
        "device",
        "rel_cluster_to_device"
      ]
    }
  ],
  "data": {
    "device": {
      "name": "R10-S3",
      "rel_cluster_to_device": null
    }
  }
}

And this server-side traceback:

Jan 04 10:38:42 dh01-a-06-18 nautobot-server[2465943]: An error occurred while resolving field DeviceType.rel_device_to_vm
Jan 04 10:38:42 dh01-a-06-18 nautobot-server[2465943]: Traceback (most recent call last):
Jan 04 10:38:42 dh01-a-06-18 nautobot-server[2465943]:   File "/opt/nautobot/lib/python3.9/site-packages/graphql/execution/executor.py", line 452, in resolve_or_error
Jan 04 10:38:42 dh01-a-06-18 nautobot-server[2465943]:     return executor.execute(resolve_fn, source, info, **args)
Jan 04 10:38:42 dh01-a-06-18 nautobot-server[2465943]:   File "/opt/nautobot/lib/python3.9/site-packages/graphql/execution/executors/sync.py", line 16, in execute
Jan 04 10:38:42 dh01-a-06-18 nautobot-server[2465943]:     return fn(*args, **kwargs)
Jan 04 10:38:42 dh01-a-06-18 nautobot-server[2465943]:   File "/opt/nautobot/lib/python3.9/site-packages/nautobot/core/graphql/generators.py", line 139, in resolve_relationship
Jan 04 10:38:42 dh01-a-06-18 nautobot-server[2465943]:     queryset_ids = gql_optimizer.query(
Jan 04 10:38:42 dh01-a-06-18 nautobot-server[2465943]:   File "/opt/nautobot/lib/python3.9/site-packages/graphene_django_optimizer/query.py", line 40, in query
Jan 04 10:38:42 dh01-a-06-18 nautobot-server[2465943]:     return QueryOptimizer(info, **options).optimize(queryset)
Jan 04 10:38:42 dh01-a-06-18 nautobot-server[2465943]:   File "/opt/nautobot/lib/python3.9/site-packages/graphene_django_optimizer/query.py", line 60, in optimize
Jan 04 10:38:42 dh01-a-06-18 nautobot-server[2465943]:     return store.optimize_queryset(queryset)
Jan 04 10:38:42 dh01-a-06-18 nautobot-server[2465943]:   File "/opt/nautobot/lib/python3.9/site-packages/graphene_django_optimizer/query.py", line 378, in optimize_queryset
Jan 04 10:38:42 dh01-a-06-18 nautobot-server[2465943]:     queryset = queryset.select_related(*self.select_list)
Jan 04 10:38:42 dh01-a-06-18 nautobot-server[2465943]:   File "/opt/nautobot/lib/python3.9/site-packages/django/db/models/query.py", line 1047, in select_related
Jan 04 10:38:42 dh01-a-06-18 nautobot-server[2465943]:     raise TypeError("Cannot call select_related() after .values() or .values_list()")
Jan 04 10:38:42 dh01-a-06-18 nautobot-server[2465943]: TypeError: Cannot call select_related() after .values() or .values_list()
Jan 04 10:38:42 dh01-a-06-18 nautobot-server[2465943]: Traceback (most recent call last):
Jan 04 10:38:42 dh01-a-06-18 nautobot-server[2465943]:   File "/opt/nautobot/lib/python3.9/site-packages/graphql/execution/executor.py", line 452, in resolve_or_error
Jan 04 10:38:42 dh01-a-06-18 nautobot-server[2465943]:     return executor.execute(resolve_fn, source, info, **args)
Jan 04 10:38:42 dh01-a-06-18 nautobot-server[2465943]:   File "/opt/nautobot/lib/python3.9/site-packages/graphql/execution/executors/sync.py", line 16, in execute
Jan 04 10:38:42 dh01-a-06-18 nautobot-server[2465943]:     return fn(*args, **kwargs)
Jan 04 10:38:42 dh01-a-06-18 nautobot-server[2465943]:   File "/opt/nautobot/lib/python3.9/site-packages/nautobot/core/graphql/generators.py", line 139, in resolve_relationship
Jan 04 10:38:42 dh01-a-06-18 nautobot-server[2465943]:     queryset_ids = gql_optimizer.query(
Jan 04 10:38:42 dh01-a-06-18 nautobot-server[2465943]:   File "/opt/nautobot/lib/python3.9/site-packages/graphene_django_optimizer/query.py", line 40, in query
Jan 04 10:38:42 dh01-a-06-18 nautobot-server[2465943]:     return QueryOptimizer(info, **options).optimize(queryset)
Jan 04 10:38:42 dh01-a-06-18 nautobot-server[2465943]:   File "/opt/nautobot/lib/python3.9/site-packages/graphene_django_optimizer/query.py", line 60, in optimize
Jan 04 10:38:42 dh01-a-06-18 nautobot-server[2465943]:     return store.optimize_queryset(queryset)
Jan 04 10:38:42 dh01-a-06-18 nautobot-server[2465943]:   File "/opt/nautobot/lib/python3.9/site-packages/graphene_django_optimizer/query.py", line 378, in optimize_queryset
Jan 04 10:38:42 dh01-a-06-18 nautobot-server[2465943]:     queryset = queryset.select_related(*self.select_list)
Jan 04 10:38:42 dh01-a-06-18 nautobot-server[2465943]:   File "/opt/nautobot/lib/python3.9/site-packages/django/db/models/query.py", line 1047, in select_related
Jan 04 10:38:42 dh01-a-06-18 nautobot-server[2465943]:     raise TypeError("Cannot call select_related() after .values() or .values_list()")

This query reproduces the error:

query {
  device(id: "a7c1dcc8-80eb-4754-9981-e2f63a9ab11e") {
    id
    name
    rel_bgp_device_router_id {
      id
    }
  }
}

But the following variant query works just fine:

query {
  device(id: "a7c1dcc8-80eb-4754-9981-e2f63a9ab11e") {
    id
    name
    rel_bgp_device_router_id {
      id
      address
    }
  }
}

Unfortunately it's non-trivial to try it for yourself. We have a temporary workaround that doesn't fix anything in graphene-django-optimizer but just wraps the calls in a try...except block. See: nautobot/nautobot#1230

We'd be happy to try to help fix this bug, but just in case you might be able to fix it quicker, I wanted to report it here. Thanks for this great project!

Summary

ManyToOneRel reverse fields are causing extra queries

When querying for a reverse foreign key field (ManyToOneRel), the prefetch does not add the original pointer field. This causes Django to query that field for each iteration.

Here is an example of the issue:

{
  allClients {
    products {
      id
    }
  }
}

Issue/Actual issue:

SELECT "client"."id",
       "client"."deleted",
FROM "client"

SELECT "product"."id"
FROM "product"
WHERE "product"."client_id" IN ('d4b3aab3-aac2-44c3-aed3-504d315d52c7'::uuid,
                                '1fc41289-cefd-4fc0-9fa1-e6247d84f4ee'::uuid)

SELECT "product"."id",
       "product"."client_id"
FROM "product"
WHERE "product"."id" = '92391767-9c60-4d0b-9b01-1b050b0e9573'::uuid

SELECT "product"."id",
       "product"."client_id"
FROM "product"
WHERE "product"."id" = '1002b6ab-741a-4f5a-8bd4-f9d1b218d0c0'::uuid

SELECT "product"."id",
       "product"."client_id"
FROM "product"
WHERE "product"."id" = '1810afee-e931-45b8-a1e0-9be95a39abb8'::uuid

Expected queries:

SELECT "client"."id",
       "client"."deleted",
FROM "client"

SELECT "product"."id"
       "product"."client_id"
FROM "product"
WHERE "product"."client_id" IN ('d4b3aab3-aac2-44c3-aed3-504d315d52c7'::uuid,
                                '1fc41289-cefd-4fc0-9fa1-e6247d84f4ee'::uuid)

Here is some code to show the issue with query counting:

# Models

class RelatedOneToManyItem(models.Model):
    name = models.CharField(max_length=100, blank=True)
    item = models.ForeignKey(Item, on_delete=models.PROTECT, related_name='otm_items')

# Schema

class RelatedOneToManyItemType(DjangoObjectType):
    class Meta:
        model = RelatedOneToManyItem

# Test

from django.test.utils import CaptureQueriesContext
from django.db import connection

@pytest.mark.django_db
def test_should_check_reverse_relations_add_foreign_key():
    info = create_resolve_info(schema, '''
        query {
            items {
                otmItems {
                    id
                }
            }
        }
    ''')
    qs = Item.objects.all()
    items = gql_optimizer.query(qs, info)
    optimized_items = qs.prefetch_related(
        Prefetch(
            'otm_items',
            queryset=RelatedOneToManyItem.objects.only('id', 'item_id'),
        ),
    )
    assert_query_equality(items, optimized_items)

    for i in range(10):
        the_item = Item.objects.create(name='foo')
        for k in range(10):
            related_item = RelatedOneToManyItem.objects.create(name='bar{}{}'.format(i,k), item=the_item)

    with CaptureQueriesContext(connection) as expected_query_capture:
        for i in items:
            for k in i.otm_items.all():
                pass

    with CaptureQueriesContext(connection) as optimized_query_capture:
        for i in optimized_items:
            for k in i.otm_items.all():
                pass

    assert len(optimized_query_capture.captured_queries) == 2
    assert len(expected_query_capture) == len(optimized_query_capture)

I was able to resolve this by adding this:

        if model_field.one_to_many or model_field.many_to_many:
            field_store = self._optimize_gql_selections(
                self._get_type(field_def),
                selection,
                # parent_type,
            )

+            if isinstance(model_field, ManyToOneRel):
+                field_store.only(model_field.field.name)

            related_queryset = model_field.related_model.objects.all()
            store.prefetch_related(name, field_store, related_queryset)
            return True

Under here:

I was unable to have the other tests pass though. not sure what would be the issue.

Optimizing model_fields across relationship boundaries

It doesn't seem possible from a cursory glance of the code, and I haven't spent enough time to see if it's an easy patch so I figured I'd ask the experts here first.

Is it possible to optimize across model relationship boundaries, for instance let's presume we wanted to make some things available at the top level resolver to avoid lots of nesting (the pseudo example below is not a particularly good one, but the question still stands):

class OrganizationType:
    def resolve_parent_organization(root, info):
        return root.parent_organization
    def resolve_accounts(root, info):
        return root.accounts.all()

class UserType:
@gql_optimizer.resolver_hints(model_field="organization__parent_organization")
    def resolve_parent_organization(root, info):
        return root.organization.parent_organization

and then we'd like to be able to optimize queries according to a pattern such as this:

user {
    parentOrganization {
        accounts
    }
}

The hacky approach we're taking at the moment is to prefetch downstream data when we access the parentOrganization resolver because this is ultimately better than N+1 queries but it still results in lots of overfetching. For instance, let's say that instead of accounts we only want the parentOrganization name.

graphene 3.1 use 'FieldNode' instead 'str'

Hello everyone,
I got some errors when update graphene from 3.0 to 3.1.

field_def = get_field_def(info.schema, info.parent_type, info.field_name)

In this line we use get_field_def from
https://github.com/graphql-python/graphql-core/blob/c214c1d93a78d7d1a1a0643b1f92a8bf29f4ad14/src/graphql/execution/execute.py#L1133
and now they using FieldNode instead of str.

So I did "monkey patch" for this package and replaced info.field_name to info.field_nodes[0] like this:

class MyQueryOptimizer(QueryOptimizer):

    def optimize(self, queryset):
        info = self.root_info
        field_def = get_field_def(
            info.schema,
            info.parent_type,
            info.field_nodes[0])
        store = self._optimize_gql_selections(
            self._get_type(field_def),
            info.field_nodes[0],
            # info.parent_type,
        )
        return store.optimize_queryset(queryset)


def query_opt(queryset, info, **options):
    return MyQueryOptimizer(info, **options).optimize(queryset)


graphene_django_optimizer.query = query_opt

Add support for model_field also a callable

Hi There!

Would it be possible to have model_field also use _normalize_hint_value so we could use a lambda function?

We have the problem that we need to define the hints at runtime since they are different based on language. Using the annotation does only work with lambdas as they get executed during the request.

Thanks for your help!

Select related is called without a value, creating n joins

According to Django docs:

There may be some situations where you wish to call select_related() with a lot of related objects, or where you donโ€™t know all of the relations. In these cases it is possible to call select_related() with no arguments. This will follow all non-null foreign keys it can find - nullable foreign keys must be specified. This is not recommended in most cases as it is likely to make the underlying query more complex, and return more data, than is actually needed.

https://docs.djangoproject.com/en/2.1/ref/models/querysets/#select-related

And an issue was found here (master):

queryset = queryset.select_related(*self.select_list)

There is a call to select related with an empty list. this triggers % joins.

I first saw this when I debugged this, and so many fields were added, and I could not understand why.
The tests in this repo did not catch this because they are all null=True.

To reproduce:

# Models
class SomeOtherItem(models.Model):
    name = models.CharField(max_length=100, blank=True)


class OtherItem(models.Model):
    name = models.CharField(max_length=100, blank=True)
    some_other_item = models.ForeignKey('SomeOtherItem', on_delete=models.PROTECT, null=False)

# Schema

class SomeOtherItemType(DjangoObjectType):
    class Meta:
        model = SomeOtherItem


class OtherItemType(DjangoObjectType):
    class Meta:
        model = OtherItem

class Query(graphene.ObjectType):
    other_items = graphene.List(OtherItemType)

    def resolve_other_items(root, info):
        return gql_optimizer.query(OtherItemType.objects.all(), info)

# Test
def test_should_only_use_the_only_and_not_select_related():
    info = create_resolve_info(schema, '''
        query {
            otherItems {
                id
                name
            }
        }
    ''')
    qs = OtherItem.objects.all()
    items = gql_optimizer.query(qs, info)
    optimized_items = qs.only('id', 'name')
    assert_query_equality(items, optimized_items)

Django 3.2 ?

Hi,

Is there a reason why Django 3.2 (only supported version tree before Django 4) is not officially supported? I successfully ran the tests on v0.8.0 against it.

I'd be happy to submit a PR

Best,

Support for mutations

Hi,
we're successfully using the optimizer for queries and it works perfectly.

But now I've noticed that mutation result is not optimized, which generates tons of sql queries.

I've tried tricking the resolver by calling MyNode.get_optimized_node directly, but without success:

class MyMutation:
    #...

    my_node = graphene.Field(nodes.MyNode)
    
    @classmethod
    def mutate_and_get_payload(cls, root, info, **kwargs):
        # ...
        return MyMutation(my_node=nodes.MyNode.get_optimized_node(info, nodes.MyNode._meta.model.objects, my_id))

Result is returned properly, but without optimization.

I've noticed a mention about mutations here: #18, but it didn't lead me anywhere.

Is there some workaround?

Thank you!

Nested queries are not properly optimized in graphene-django v3.0.0 (but are in v3.0.0b7)

graphene-django just released a non-beta version of v3 last month (https://github.com/graphql-python/graphene-django/releases/tag/v3.0.0), and graphene-django-optimizer does not appear to optimize queries properly with the new graphene-django release unfortunately.

The latest version of graphene-django-optimizer (0.9.1) appears to work properly with graphene-django v3.0.0b7 (a beta version released a couple years ago, which is listed in the dev-env-requirements.txt for this repo). But when bumping that, SQL-optimization no longer works in many cases, such as with nested fields requiring prefetch_related.

See branch here with version bumps and failing tests: #85

Support for Django 3.x

Would it be possible to update to support Django 3.x? This is one of the last deps we have that prevent us from upgrading.

Love the library. Thank you

prefetch relay children - duplicate issue of #1, #4, etc.

Hey, I guess something is wrong with my setup, perhaps not even related to graphene-django-optimizer.

I have exactly the same issue with #1

I have used all solutions I can see from Graphene and here, plus that I am even manually calling prefetch_related('children_set'), queries will still be N+1.

Here's my model.

class Province(Model):
    name                        = CharField(max_length=10)

class City(Model):
    province                    = ForeignKey(Province, on_delete=CASCADE)
    name                        = CharField(max_length=15)

Overridding DjangoConnectionField to not use merge_querysets, like suggested:
#429

class PrefetchingConnectionField(DjangoFilterConnectionField): 
# or just subclass DjangoConnectionField, doesn't make a difference for N+1

    @classmethod
    def merge_querysets(cls, default_queryset, queryset):
        return queryset

    @classmethod
    def resolve_connection(cls, connection, default_manager, args, iterable):
        if iterable is None:
            iterable = default_manager

        if isinstance(iterable, QuerySet):
            _len = iterable.count()
        else:
            _len = len(iterable)

        connection = connection_from_list_slice(
            iterable,
            args,
            slice_start=0,
            list_length=_len,
            list_slice_length=_len,
            connection_type=connection,
            edge_type=connection.Edge,
            pageinfo_type=PageInfo,
        )
        connection.iterable = iterable
        connection.length = _len
        return connection

Schema

class Province(DjObj):
    class Meta(NodeI):
        model = models.Province
        filter_fields = {   # for DjangoFilterConnectionField
            'id': ['exact'],
        }

class City(DjObj):
    class Meta(NodeI):
        model = models.City
        filter_fields = {
            'id': ['exact'],
        }


class GeoQueryCodex:
    provinces                      = PrefetchingConnectionField(Province)
    province                       = Node.Field(Province)

    def resolve_provinces(self, info, *args, **kw):
        # use the plugin
        qs = gql_optimizer.query(models.Province.objects.all(), info)
        # or just manually prefetch
        qs = models.Province.objects.all().prefetch_related('city_set')
        return qs

    cities                         = PrefetchingConnectionField(City)
    city                           = Node.Field(City)

And the query

query Provinces {
  provinces(first:10) {
    edges {
      node {
        id
        name
        citySet {  # default naming of the children, since a related_name is not supplied
          edges {
            node {
              name
            }
          }
        }
      }
    }
  }
}

just for sanity check, python query does work:

@timeit
def d():
    result = []
    for x in Province.objects.all().prefetch_related('city_set'):
        result.append((x.id, x.name, [area.name for cityin x.city_set.all()]))
    print(result)

# this does not result in N+1, as it would be much faster than without 'prefetch_related'

Now, this result in N+1... Driving me crazy :(

Hope you don't mind me calling for help here @maarcingebala :) Your Saleor repo was the original inspiration that I used the Graphene stack.

please help me fellow python gods

Prefetch related with named arguments and objects

Hi @tfoxy, following #22, we are unable to actually know what when we are getting called from:
prefetch_related(info, *args[, **kwargs?])

args = tuple(args)
self._add_optimization_hints(
optimization_hints.select_related(info, *args),
store.select_list,
)
self._add_optimization_hints(
optimization_hints.prefetch_related(info, *args),
store.prefetch_list,
)

We could whether have as first or second argument first, last, filter, foo, etc.

We would like to get a **kwargs instead. But we can't both pass *args and **kwargs to prevent any breaking change for any of the optimizer users.

I believe users using the arguments from *args should be still safe if they are properly using the field name... Otherwise, it will break for sure.

What do you think? Could we get such change in and release it as a major change?

Optimization hints

@tfoxy Could you explain a little bit how the concept of OptimizationHints works and how to use it? I'm having problems with resolving prefetches by name and I see in the code that another method is to lookup them by hints. Thanks!

Add support for skip and include directives

What is the possibility of adding support for skip and include directives? I found when including a field in a query but then excluding it with @include(if: false), Graphene Django Optimizer still includes it in the SQL Query:

An anonymized example:

query Thing {
  things: Things(startDate: $startDate, first: 10) {
    totalCount
    edges {
      node {
        excludedThing @include(if: false)
        }
      }
    }
  }
}

This would still put excludedThing is the SQL query

Resolvers that uses `.get` instead of `.filter` (i.e. returning `Model`, rather than `QuerySet` instance) on `Query`.

We are trying to get graphene-django-optimizer to work with our setup. Most of our queries start from a query { me { ... } } root. As such the implementation of the Query is:

class Query(graphene.ObjectType):
    me = graphene.Field(UserNode)

    @classmethod
    def resolve_me(cls, root, info):
        user = info.context.user
        if not user.is_authenticated:
            raise Exception("Authentication credentials were not provided")
        return user

or another example:

class Query(graphene.ObjectType):
    category = graphene.Field(CategoryNode, slug=graphene.String(required=True))

    @classmethod
    def resolve_category(cls, root, info, slug):
        return CategoryDefinition.objects.get(slug=slug)

We are not able to figure out how to get the subtree of such an entry point to optimize. Simply extending UserNode from OptimizedDjangoObjectType does not make any difference in the queries being run. Any idea on how we could utilize graphene-django-optimizer?

Optimizer error 'str' object has no attribute 'name' on `get_field_def`

This is the info context

GraphQLResolveInfo(field_name='childOrganizations', field_nodes=[FieldNode at 39:70], return_type=<GraphQLNonNull <GraphQLList <GraphQLNonNull <GrapheneObjectType 'OrganizationType'>>>>, parent_type=<GrapheneObjectType 'Query'>, path=Path(prev=None, key='childOrganizations', typename='Query'), schema=<graphql.type.schema.GraphQLSchema object at 0x7ff2eebc0650>, fragments={}, root_value=None, operation=OperationDefinitionNode at 0:72, variable_values={}, context=<WSGIRequest: POST '/graphql/'>, is_awaitable=<function assume_not_awaitable at 0x7ff2f18f22a0>)
Using selector: EpollSelector

And i'm getting the error when the optimizer tries to get the field def

field_def = get_field_def(info.schema, info.parent_type, info.field_name)

and it gets the error in the first linne of the function due to field_node is now a string

def get_field_def(
        schema: GraphQLSchema, parent_type: GraphQLObjectType, field_node: FieldNode
) -> GraphQLField:
        """Get field definition.

    This method looks up the field on the given type definition. It has special casing
    for the three introspection fields, ``__schema``, ``__type`, and ``__typename``.
    ``__typename`` is special because it can always be queried as a field, even in
    situations where no other fields are allowed, like on a Union. ``__schema`` and
    ``__type`` could get automatically added to the query type, but that would require
    mutating type definitions, which would cause issues.

    For internal use only.
    """
    field_name = field_node.name.value

    if field_name == "__schema" and schema.query_type == parent_type:
            return SchemaMetaFieldDef
    elif field_name == "__type" and schema.query_type == parent_type:
            return TypeMetaFieldDef
    elif field_name == "__typename":
            return TypeNameMetaFieldDef
    return parent_type.fields.get(field_name)
ipdb> info.field_name.name.value
*** AttributeError: 'str' object has no attribute 'name'
Using selector: EpollSelector
[tool.poetry]
version = "0.1.0"
description = ""
authors = []

[tool.poetry.dependencies]
python = "^3.11"
django = "^4.2.3"
graphene = "^3.2.2"
graphene-django = "^3.1.2"
graphene-file-upload = "^1.3.0"
pillow = "^10.0.0"
sentry-sdk = "^1.27.0"
rq = "^1.15.1"
django-admin-interface = "^0.26.0"
django-modeladmin-reorder = "^0.3.1"
django-anymail = "^10.0"
django-extensions = "^3.2.3"
django-rq = "^2.8.1"
django-import-export = "^3.2.0"
django-mptt = "^0.14.0"
django-solo = "^2.1.0"
django-simple-history = "^3.3.0"
django-tinymce = "^3.6.1"
psycopg2-binary = "^2.9.6"
django-model-utils = "^4.3.1"
django-redis = "^5.3.0"
hiredis = "^2.2.3"
graphene-django-optimizer = "^0.9.1"

[tool.poetry.dev-dependencies]
ipython = "^8.14.0"
ipdb = "^0.13.13"


[build-system]
requires = ["poetry>=1.5.1"]
build-backend = "poetry.masonry.api"

Multiple fragments specifying the same relation field

When multiple fragments specify the same relation field it currently is optimized multiple times. In the example below prefetch_related will be executed twice. I think there should only be one call to select_related/prefetch_related for one output field.

Failing test case:

@pytest.mark.django_db
def test_should_optimize_when_using_overlapping_fragments():
    info = create_resolve_info(schema, '''
        query {
            items(name: "bar") {
            ...ItemFragment_1
            ...ItemFragment_2
            }
        }
        fragment ItemFragment_1 on ItemType {
            itemSet {
                id
            }
        }
        fragment ItemFragment_2 on ItemType {
            itemSet {
                id
            }
        }
    ''')
    qs = Item.objects.filter(name='bar')
    items = gql_optimizer.query(qs, info)
    optimized_items = qs.prefetch_related(
        Prefetch('item_set', queryset=Item.objects.only('id', 'item_id'))
    )
    assert_query_equality(items, optimized_items)

I've looked at the code, but I can't see a simple way to fix this. Maybe the query should be flattened before optimization?

Optimize retrieving a single object

I'm following the graphene-django tutorial and got up to retrieving a single object. The README here does not mention how optimization would work here. gql_optimizer.query expects a queryset. As a workaround, I'm using:

      def resolve_category(self, info, **kwargs):
          id = kwargs.get('id')
          name = kwargs.get('name')

          if id is not None:
              return gql_optimizer.query(Category.objects.filter(pk=id), info).get()

          if name is not None:
              return gql_optimizer.query(Category.objects.filter(name=name), info).get()

          return None

Is this the recommended approach?

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.