Skip to content

Commit 7fa8d1f

Browse files
committed
fix(converter.py): performance fix in convert_field_to_djangomodel.dynamic_type.CustomField. Get instance from the database only one time.
1 parent d18cab8 commit 7fa8d1f

File tree

1 file changed

+49
-5
lines changed

1 file changed

+49
-5
lines changed

Diff for: graphene_django/converter.py

+49-5
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import functools
2+
import inspect
13
from collections import OrderedDict
24
from functools import singledispatch, wraps
35

@@ -25,6 +27,7 @@
2527
)
2628
from graphene.types.json import JSONString
2729
from graphene.types.scalars import BigInt
30+
from graphene.types.resolver import get_default_resolver
2831
from graphene.utils.str_converters import to_camel_case
2932
from graphql import GraphQLError
3033

@@ -324,13 +327,54 @@ def wrap_resolve(self, parent_resolver):
324327
resolver = super().wrap_resolve(parent_resolver)
325328

326329
def custom_resolver(root, info, **args):
327-
fk_obj = resolver(root, info, **args)
328-
if not isinstance(fk_obj, model):
329-
# In case the resolver is a custom one that overwrites
330+
# Note: this function is used to resolve FK or 1:1 fields
331+
# it does not differentiate between custom-resolved fields
332+
# and default resolved fields.
333+
334+
# because this is a django foreign key or one-to-one field, the primary-key for
335+
# this node can be accessed from the root node.
336+
# ex: article.reporter_id
337+
from graphene.utils.str_converters import to_snake_case
338+
339+
# get the name of the id field from the root's model
340+
field_name = to_snake_case(info.field_name)
341+
db_field_key = root.__class__._meta.get_field(field_name).attname
342+
if hasattr(root, db_field_key):
343+
# get the object's primary-key from root
344+
object_pk = getattr(root, db_field_key)
345+
else:
346+
return None
347+
348+
is_resolver_awaitable = inspect.iscoroutinefunction(resolver)
349+
350+
if is_resolver_awaitable:
351+
fk_obj = resolver(root, info, **args)
352+
# In case the resolver is a custom awaitable resolver overwrites
330353
# the default Django resolver
331-
# This happens, for example, when using custom awaitable resolvers.
332354
return fk_obj
333-
return _type.get_node(info, fk_obj.pk)
355+
356+
instance_from_get_node = _type.get_node(info, object_pk)
357+
358+
if instance_from_get_node is None:
359+
# no instance to return
360+
return
361+
elif isinstance(resolver, functools.partial) and resolver.func is get_default_resolver():
362+
return instance_from_get_node
363+
elif resolver is not get_default_resolver():
364+
# Default resolver is overriden
365+
# For optimization, add the instance to the resolver
366+
setattr(root, field_name, instance_from_get_node)
367+
# Explanation:
368+
# previously, _type.get_node` is called which results in at least one hit to the database.
369+
# But, if we did not pass the instance to the root, calling the resolver will result in
370+
# another call to get the instance which results in at least two database queries in total
371+
# to resolve this node only.
372+
# That's why the value of the object is set in the root so when the object is accessed
373+
# in the resolver (root.field_name) it does not access the database unless queried explicitly.
374+
fk_obj = resolver(root, info, **args)
375+
return fk_obj
376+
else:
377+
return instance_from_get_node
334378

335379
return custom_resolver
336380

0 commit comments

Comments
 (0)