Optimize database access inside graphene queries.
Project description
graphene-django-optimizer
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)
Annotations
The queryset can be optimized with an annotation when and only if a field is requested. To do so, the annotate resolver hint can be used.
class RecipeType(gql_optimizer.OptimizedDjangoObjectType):
ingredient_count = graphene.Int()
class Meta:
model = Recipe
fields = ('id',)
@gql_optimizer.resolver_hints(
annotate={
'gql_ingredient_count': Count('ingredients')
}
)
def resolve_ingredient_count(root, info):
return getattr(root, 'gql_ingredient_count')
class Query(object):
all_recipes = graphene.List(RecipeType)
def resolve_all_recipes(root, info):
return gql_optimizer.query(Recipe.objects.all(), info)
When using annotations there are two caveats.
- The queryset will not be annotated if the optimization fails.
- If an annotation is used in a related query that will usually
result in a optimized
select_related
,prefetch_related
is used instead, adding one additional query. See example below.
class RecipeType(gql_optimizer.OptimizedDjangoObjectType):
ingredient_count = graphene.Int()
class Meta:
model = Recipe
fields = ('id',)
@gql_optimizer.resolver_hints(
annotate={
'gql_ingredient_count': Count('ingredients')
}
)
def resolve_ingredient_count(root, info):
return getattr(root, 'gql_ingredient_count')
class IngredientType(gql_optimizer.OptimizedDjangoObjectType):
recipe = gql_optimizer.field(
graphene.Field(RecipeType), model_field='recipe',
)
class Meta:
model = Ingredient
fields = ('id', name')
class Query(object):
all_ingredients = graphene.List(IngredientType)
def resolve_all_ingredients(root, info):
return gql_optimizer.query(Ingredient.objects.all(), info)
The GraphQL query.
query {
allIngredients {
id
name
recipe {
id
name
ingredientCount
}
}
}
Will resolve in two SQL queries. One to fetch all ingredients, one to prefetch all recipes for those ingredients.
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
File details
Details for the file graphene-django-optimizer-patch-0.9.1.0.tar.gz
.
File metadata
- Download URL: graphene-django-optimizer-patch-0.9.1.0.tar.gz
- Upload date:
- Size: 12.1 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/4.0.1 CPython/3.9.15
File hashes
Algorithm | Hash digest | |
---|---|---|
SHA256 | 2479ed9a51dfb09a01bc08485a8ff9b5aed66e71becc5bd1eb1b312748c50c75 |
|
MD5 | 177bdf5294216b0493014fb774e04543 |
|
BLAKE2b-256 | 3c1d9647c65210d2b36ffca5db04275cb470e2a1a5fd29e6e76aac86ea86fc17 |