Image

Patrick Altman: Reusing GraphQL Queries within Django

Reusing GraphQL Queries within Django
Reusing GraphQL Queries within Django

We like to use GraphQL as the API layer for building SPAs (Single Page App) written with a VueJS frontend and Django backend. Types tend to map pretty nicely to models and frontend development can reuse types and queries for efficient querying to support different UI.

A great library for making this work so nicely for us isStrawberryand is a big reason we have sponsored the project for some time now.

One downside though, especially in Django, is potential for duplication of query logic. Do you extract it all into custom managers and querysets and keep resolvers super lean? In order to keep queries efficient though, you&aposll probably still need to inject some queryset optimizations in your types.

We do a bit of both. We reduce duplication of annotations and subqueries by creating reusable units inannotations.pyandsubqueries.pyfor instance. Then we optimize our GraphQL layer. Overridingget_queryseton types and other tricks that go beyond the scope of this post (we&aposll have to write one soon of all how to get the most out of Strawberry).

We have a number of processes that execute within Django background tasks that need to query some of the same data and at first we were taking care to recreate the same queryset logic. That wasn&apost going to last very long. Whenever you duplicate complex logic, code drift happens, and before you know it you are generating reports that don&apost reconcile in subtle and weird ways.

Our solution was to just query through the same GraphQL layer from Python but without going through the overhead of the request/response cycle. To do this we needed to generate a machine readable schema and then load that up in an object that would allow us to execute queries just like we were doing from the frontend.

The star of the show is this object that we tuck away in agraphql.pymodule:

import os import json from django.conf import settings from django.http import HttpRequest from graphql import parse from graphql.language.ast import OperationDefinitionNode from strawberry.types.execution import ExecutionResult from .api.schema import private class GraphQLSchema: def __init__(self): # The path to the JSON file containing the GraphQL queries generate from yarn generate self._path = os.path.join(settings.PROJECT_ROOT, "static/src/gql/persisted-documents.json") self._ops_by_name = {} self._create_named_op_map() def _create_named_op_map(self): assert os.path.exists(self._path), f"GraphQL file not found at {self._path}" ops_by_name = {} with open(self._path, encoding="utf-8") as file: documents = json.load(file).values() for doc in documents: ast = parse(doc) for d in ast.definitions: if isinstance(d, OperationDefinitionNode) and d.name: ops_by_name[d.name.value] = doc self._ops_by_name = ops_by_name def execute(self, query_name: str, context: HttpRequest = None, **variables) -> ExecutionResult: assert query_name in self._ops_by_name, f"Query {query_name} not found" query = self._ops_by_name[query_name] return private.execute_sync(query, variable_values=variables, context_value=context) schema = GraphQLSchema()

We use@graphql-codegen/cliand this config to create assets for our frontend to use as well as a version of the schema for ourGraphQLSchemato consume:

This is ourcodegen.ts

import type { CodegenConfig } from &apos@graphql-codegen/cli' const config: CodegenConfig = { schema: &aposhttp://localhost:8000/local-graphql/&apos, ignoreNoDocuments: true, // for better experience with the watcher generates: { &apos./static/src/gql/types.ts&apos: { plugins: [&apostypescript&apos], config: { useTypeImports: true, }, }, &apos./static/src/gql/&apos: { preset: &aposclient&apos, config: { useTypeImports: true, }, presetConfig: { fragmentMasking: false, persistedDocuments: { hashAlgorithm: &apossha256&apos // optional; defaults to sha1 } }, documents: [ &aposstatic/src/compositions/data/**/*.ts&apos, &aposstatic/src/compositions/data/gql/**/*.gql&apos ], }, &apos./static/src/compositions/data/&apos: { preset: &aposnear-operation-file&apos, presetConfig: { folder: &apos__generated__&apos, extension: &apos.ts&apos, baseTypesPath: &apos../../gql/types.ts&apos // GENERATES &apos@/gql/types&apos as &apos./@/gql/types&apos... }, config: { useTypeImports: true, preResolveTypes: false, }, plugins: [ &apostypescript-operations&apos, &apostyped-document-node&apos ], documents: [&aposstatic/src/compositions/data/gql/**/*.gql&apos], }, }, }; export default config;

Now in our Django/Python code we can execute GraphQL operations just like our frontend code does:

from .graphql import schema data = schema.execute("ShipmentsReport", id="12345")

This has been working really well. Not only is DRYing up code like this great for maintenance it a real reduction of cognitive burden.

https://wedgworth.dev/reusing-graphql-queries-within-django/