Skip to content

Commit

Permalink
Merge 3801669 into 12ec8dc
Browse files Browse the repository at this point in the history
  • Loading branch information
jkimbo committed Apr 26, 2020
2 parents 12ec8dc + 3801669 commit 9855a4e
Show file tree
Hide file tree
Showing 3 changed files with 97 additions and 18 deletions.
35 changes: 17 additions & 18 deletions docs/execution/dataloader.rst
Expand Up @@ -4,7 +4,7 @@ Dataloader
DataLoader is a generic utility to be used as part of your application's
data fetching layer to provide a simplified and consistent API over
various remote data sources such as databases or web services via batching
and caching.
and caching. It is provided by a seperate package `aiodataloader <https://pypi.org/project/aiodataloader/>`.


Batching
Expand All @@ -15,32 +15,31 @@ Create loaders by providing a batch loading function.

.. code:: python
from promise import Promise
from promise.dataloader import DataLoader
from aiodataloader import DataLoader
class UserLoader(DataLoader):
def batch_load_fn(self, keys):
# Here we return a promise that will result on the
# corresponding user for each key in keys
return Promise.resolve([get_user(id=key) for key in keys])
async def batch_load_fn(self, keys):
# Here we call a function to return a user for each key in keys
return [get_user(id=key) for key in keys]
A batch loading function accepts a list of keys, and returns a ``Promise``
which resolves to a list of ``values``.
A batch loading async function accepts a list of keys, and returns a list of ``values``.

Then load individual values from the loader. ``DataLoader`` will coalesce all
individual loads which occur within a single frame of execution (executed once
the wrapping promise is resolved) and then call your batch function with all
the wrapping event loop is resolved) and then call your batch function with all
requested keys.


.. code:: python
user_loader = UserLoader()
user_loader.load(1).then(lambda user: user_loader.load(user.best_friend_id))
user1 = await user_loader.load(1)
user1_best_friend = await user_loader.load(user1.best_friend_id))
user_loader.load(2).then(lambda user: user_loader.load(user.best_friend_id))
user2 = await user_loader.load(2)
user2_best_friend = await user_loader.load(user2.best_friend_id))
A naive application may have issued *four* round-trips to a backend for the
Expand All @@ -54,9 +53,9 @@ make sure that you then order the query result for the results to match the keys
.. code:: python
class UserLoader(DataLoader):
def batch_load_fn(self, keys):
async def batch_load_fn(self, keys):
users = {user.id: user for user in User.objects.filter(id__in=keys)}
return Promise.resolve([users.get(user_id) for user_id in keys])
return [users.get(user_id) for user_id in keys]
``DataLoader`` allows you to decouple unrelated parts of your application without
Expand Down Expand Up @@ -111,8 +110,8 @@ leaner code and at most 4 database requests, and possibly fewer if there are cac
best_friend = graphene.Field(lambda: User)
friends = graphene.List(lambda: User)
def resolve_best_friend(root, info):
return user_loader.load(root.best_friend_id)
async def resolve_best_friend(root, info):
return await user_loader.load(root.best_friend_id)
def resolve_friends(root, info):
return user_loader.load_many(root.friend_ids)
async def resolve_friends(root, info):
return await user_loader.load_many(root.friend_ids)
1 change: 1 addition & 0 deletions setup.py
Expand Up @@ -56,6 +56,7 @@ def run_tests(self):
"mock>=4.0,<5",
"pytz==2019.3",
"iso8601>=0.1,<2",
"aiodataloader<1",
]

dev_requires = ["black==19.10b0", "flake8>=3.7,<4"] + tests_require
Expand Down
79 changes: 79 additions & 0 deletions tests_asyncio/test_dataloader.py
@@ -0,0 +1,79 @@
from collections import namedtuple
from unittest.mock import Mock
from pytest import mark
from aiodataloader import DataLoader

from graphene import ObjectType, String, Schema, Field, List


CHARACTERS = {
"1": {"name": "Luke Skywalker", "sibling": "3"},
"2": {"name": "Darth Vader", "sibling": None},
"3": {"name": "Leia Organa", "sibling": "1"},
}


get_character = Mock(side_effect=lambda character_id: CHARACTERS[character_id])


class CharacterType(ObjectType):
name = String()
sibling = Field(lambda: CharacterType)

async def resolve_sibling(character, info):
if character["sibling"]:
return await info.context.character_loader.load(character["sibling"])
return None


class Query(ObjectType):
skywalker_family = List(CharacterType)

async def resolve_skywalker_family(_, info):
return await info.context.character_loader.load_many(["1", "2", "3"])


mock_batch_load_fn = Mock(
side_effect=lambda character_ids: [get_character(id) for id in character_ids]
)


class CharacterLoader(DataLoader):
async def batch_load_fn(self, character_ids):
return mock_batch_load_fn(character_ids)


Context = namedtuple("Context", "character_loader")


@mark.asyncio
async def test_basic_dataloader():
schema = Schema(query=Query)

character_loader = CharacterLoader()
context = Context(character_loader=character_loader)

query = """
{
skywalkerFamily {
name
sibling {
name
}
}
}
"""

result = await schema.execute_async(query, context=context)

assert not result.errors
assert result.data == {
"skywalkerFamily": [
{"name": "Luke Skywalker", "sibling": {"name": "Leia Organa"}},
{"name": "Darth Vader", "sibling": None},
{"name": "Leia Organa", "sibling": {"name": "Luke Skywalker"}},
]
}

assert mock_batch_load_fn.call_count == 1
assert get_character.call_count == 3

0 comments on commit 9855a4e

Please sign in to comment.