-
Notifications
You must be signed in to change notification settings - Fork 826
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Consider supporting promise-based dataloaders in v3 #1391
Comments
Graphene's own docs still talk about using promises for DataLoaders. How does one actually implement DataLoaders in v3? |
Probably no longer per Promise. One possibility should be aiodataloader. There is an entry about it here: Here you can see the modified documentation: If you can get a working solution, I would be very interested :) p.s.: Your linked manual should still be for Graphene 2! |
tested with I get:
|
I run into the same error as @DenisseRR, @AlexCLeduc did you manage to make If yes, could you share an example of doing so? |
@superlevure Below, an explicit async loop is created within a synchronous view. The important part is that you make sure you're executing the resolvers in "async mode" and make sure you're using the same dataloader instances across resolver calls. There's some extra stuff you'll almost certainly want, like logging and attaching the user to the context: # app_name/my_graphql_view.py
import asyncio
from graphene_django.views import GraphQLView
from graphql.execution.executors.asyncio import AsyncioExecutor
from .loaders import UserLoader
class GraphQLContext:
def __init__(self, dataloaders, user):
self.dataloaders = dataloaders
self.user = user
class MyGraphQLView(GraphQLView):
def __init__(self, *args, executor=None, **kwargs):
self.loop = asyncio.new_event_loop()
asyncio.set_event_loop(self.loop)
executor = AsyncioExecutor()
super().__init__(*args, **kwargs, executor=executor)
def execute_graphql_request(self, *args, **kwargs):
result = super().execute_graphql_request(*args, **kwargs)
self.loop.close()
if result.errors:
self.log_errors(result.errors)
return result
def log_errors(self, errors):
# your logging logic
def get_dataloaders(self):
return {
"user_loader": UserLoader()
}
def get_context(self, request):
dataloaders = self.get_dataloaders()
return GraphQLContext(
user=self.request.user
dataloaders=dataloaders
) Now your resolvers can access the user dataloader through context, e.g. class Person(graphene.ObjectType)
# ...
@staticmethod
def resolve_parent(parent,info):
return info.context.dataloaders['user_loader'].load(parent.id)
# or, using async resolver
@staticmethod
async def resolve_parent_name(parent,info):
parent = await info.context.dataloaders['user_loader'].load(parent.id)
return parent.name
By the way, it would be nice if graphene provided an |
@AlexCLeduc w.r.t. the AsyncGraphqlView, I think this is a |
Thank you for the explanation and the snippet @AlexCLeduc. |
@superlevure, after playing around a bit with v3, getting async resolvers/dataloaders working correctly is quite easy when calling the schema directly (e.g. There is an open PR adding an class MyGraphQLView(GraphQLView):
def execute_graphql_request(
self, request, data, query, variables, operation_name, show_graphiql=False
):
"""
copied from GraphQLView, but swapping self.schema.execute for self.execute_graphql_as_sync
"""
if not query:
if show_graphiql:
return None
raise HttpError(HttpResponseBadRequest("Must provide query string."))
try:
document = parse(query)
except Exception as e:
return ExecutionResult(errors=[e])
if request.method.lower() == "get":
operation_ast = get_operation_ast(document, operation_name)
if operation_ast and operation_ast.operation != OperationType.QUERY:
if show_graphiql:
return None
raise HttpError(
HttpResponseNotAllowed(
["POST"],
"Can only perform a {} operation from a POST request.".format(
operation_ast.operation.value
),
)
)
validation_errors = validate(self.schema.graphql_schema, document)
if validation_errors:
return ExecutionResult(data=None, errors=validation_errors)
try:
extra_options = {}
if self.execution_context_class:
extra_options["execution_context_class"] = self.execution_context_class
options = {
"source": query,
"root_value": self.get_root_value(request),
"variable_values": variables,
"operation_name": operation_name,
"context_value": self.get_context(request),
"middleware": self.get_middleware(request),
}
options.update(extra_options)
operation_ast = get_operation_ast(document, operation_name)
if (
operation_ast
and operation_ast.operation == OperationType.MUTATION
and (
graphene_settings.ATOMIC_MUTATIONS is True
or connection.settings_dict.get("ATOMIC_MUTATIONS", False) is True
)
):
with transaction.atomic():
result = self.execute_graphql_as_sync(**options) # alex changed this line
if getattr(request, MUTATION_ERRORS_FLAG, False) is True:
transaction.set_rollback(True)
return result
return self.execute_graphql_as_sync(**options) # alex changed this line
except Exception as e:
return ExecutionResult(errors=[e])
def execute_graphql_as_sync(self, **options):
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
options["context_value"] = GraphQLContext(
user=self.request.user,
dataloaders=self.get_dataloaders() # dataloaders must be created in the loop in which they're used
)
result = loop.run_until_complete(self.schema.execute_async(**options))
if result.errors:
self.log_errors(result.errors)
loop.close()
return result
def log_errors(self, errors):
# your logging logic
def get_dataloaders(self):
return {
"user_loader": UserLoader()
} It seems to work, and I validated that the batching is working properly, but it would not surprise me if mutations were broken, they sometimes behave strangely when executed asynchronously |
Thank you @AlexCLeduc ! In the mean time I had found about graphql-python/graphene-django#1256 too and especially this repo https://github.com/fabienheureux/graphene-async (thanks to @fabienheureux) that provides a working example of I could make both of your examples work (the gain in performances is quite significant, I went from 1102 SQL queries to.. only 2 in my specific use case) but the introduction of I'm considering going back to |
This working would be life-changing for us! |
So after investigating lots of other options for supporting DataLoaders with Django I've come to the conclusion that the best approach is by implementing a custom execution context class in graphql-core to handle "deferred/future" values. I've proposed this change here graphql-python/graphql-core#155 but until we can get it merged into graphql-core I've published a library to let you integrate it with Graphene (v3) now: https://github.com/jkimbo/graphql-sync-dataloaders @superlevure @felixmeziere @AlexCLeduc I hope this is able to help you. |
Thanks @jkimbo! Will this work in a nested context, e.g. dataloaders that compose one another, or a resolver that calls multiple dataloaders? It's not obvious to me how to chain futures |
@AlexCLeduc can you give an example of what you mean? |
@jkimbo Promises were chainable using the If a resolver requires 2 dataloader calls, e.g. a grandparent field that calls a parent_loader twice, using the old promise API would look something like this, def resolve_grandparent(person,info):
return parent_loader.load(person.id).then(lambda parent: parent_loader.load(parent.id) ) |
@AlexCLeduc ah yeah that's not possible yet but I'll have a look at adding it. |
Thanks @jkimbo for graphql-sync-dataloader. |
Glad you find it useful! That was basically a re-write of execution context, I'm glad graphql-core provides an API to override it. By the way, are you in Ottawa Canada? |
Thanks a lot @jkimbo !!! |
@ericls yes I'm in that Ottawa haha. Glad to hear fellowapp is still using promises, makes me feel less crazy 😄 If anyone is interested in the generator syntactic sugar I described in the OP, |
When I opened this issue, I was unaware the execution context was capable of adding promise support. I'm happy enough with @ericls's solution, we can close this |
Closing this as this, as further work on this is currently not planned and there is no available capacity to bring this to the main library. |
I know graphql-core dropped support for promises, but the author seems to think promise-support can be added via hooks like middleware and execution context (see response to my identical issue in graphql-python/graphql-core#148).
Since most people using syrusakbary's promise library are probably already graphene users, if anyone is going to help make graphql-core 3 and promises play together, it makes sense for that to be done in graphene.
Why not just use async?
I think I have a decent use-case for non-async, promise-based resolution. Async is nice and all, and having a standard is great, but many of us are just using dataloaders as an execution pattern, not because we actually have async data-sources. Moving everything to run in async environment can have consequences.
We are calling the django ORM from our dataloaders. Because django 3.0 forces us to isolate ORM calls and wrap them in
sync_to_async
, we stuck with promise based dataloaders for syntactic reasons. Examples below:What we'd like to do, but django doesn't allow
What django would like us to do
What we settled on instead (use generator-syntax around promises)
A simple
generator_function_to_promise
is used as part of dataloaders, as well as a middleware that converts generators returned from resolvers into promises. I have hundreds of dataloaders following this pattern. I don't want to be stuck isolating all the ORM calls as per django's recommendations. That would be noisy and decrease legibility.It seems there may be other issues around using async dataloaders with connections. Admittedly that problem sounds more easily surmountable and wouldn't require supporting promises.
The text was updated successfully, but these errors were encountered: