Skip to content

Commit d2d53cc

Browse files
johnrutherfordjoemcbride
authored andcommitted
Add documentation for the DataLoader implementation (graphql-dotnet#598)
1 parent ea0a324 commit d2d53cc

File tree

9 files changed

+329
-49
lines changed

9 files changed

+329
-49
lines changed

docs/src/dataloader.md

Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
<!--Title:DataLoader-->
2+
<!--Url:dataloader-->
3+
4+
GraphQL .NET includes an implementation of Facebook's [DataLoader](https://github.com/facebook/dataloader).
5+
6+
Consider a GraphQL query like this:
7+
8+
```
9+
{
10+
orders(date: "2017-01-01") {
11+
orderId
12+
date
13+
user {
14+
userId
15+
firstName
16+
lastName
17+
}
18+
}
19+
}
20+
```
21+
22+
When the query is executed, first a list of orders is fetched. Then for each order, the associated user must also be fetched. If each user is fetched one-by-one, this would get more inefficient as the number of orders (N) grows. This is known as the N+1 problem. If there are 50 orders (N = 50), 51 separate requests would be made to load this data.
23+
24+
A DataLoader helps in two ways:
25+
26+
1. Similar operations are batched together. This can make fetching data over a network much more efficient.
27+
2. Fetched values are cached so if they are requested again, the cached value is returned.
28+
29+
In the example above, a using a DataLoader will allow us to batch together all of the requests for the users. So there would be 1 request to retrieve the list of orders and 1 request to load all users associated with those orders. This would always be a total of 2 requests rather than N+1.
30+
31+
## Setup
32+
33+
1. Register `IDataLoaderContextAccessor` in your IoC container.
34+
2. Register `DataLoaderDocumentListener` in your IoC container.
35+
36+
``` csharp
37+
services.AddSingleton<IDataLoaderContextAccessor, DataLoaderContextAccessor>();
38+
services.AddSingleton<DataLoaderDocumentListener>();
39+
```
40+
41+
3. Add the `DataLoaderDocumentListener` to the `DocumentExecuter`.
42+
43+
``` csharp
44+
var listener = Services.GetRequiredService<DataLoaderDocumentListener>();
45+
46+
var executer = new DocumentExecuter();
47+
var result = executer.ExecuteAsync(opts => {
48+
49+
...
50+
51+
opts.Listeners.Add(listener);
52+
});
53+
```
54+
55+
## Usage
56+
57+
First, inject the `IDataLoaderContextAccessor` into your GraphQL type class.
58+
59+
Then use the the `Context` property on the accessor to get the current `DataLoaderContext`. Each request will have its own context instance.
60+
61+
Use one of the "GetOrAddLoader" methods on the `DataLoaderContext`. These methods all require a string key to uniquely identify each loader. They also require a delegate for fetching the data. Each method will get an existing loader or add a new one, identified by the string key. Each method has various overloads to support different ways to load and map data with the keys.
62+
63+
Call `LoadAsync()` on the data loader. This will queue the request and return a `Task<T>`. If the result has already been cached, the task returned will already be completed.
64+
65+
The `DataLoaderDocumentListener` configured in the set up steps above automatically handles dispatching all pending data loader operations at each step of the document execution.
66+
67+
## Examples
68+
69+
This is an example of using a DataLoader to batch requests for loading items by a key. `LoadAsync()` is called by the field resolver for each Order. `IUsersStore.GetUsersByIdAsync()` will be called with the batch of userIds that were requested.
70+
71+
``` csharp
72+
public class OrderType : ObjectGraphType<Order>
73+
{
74+
// Inject the IDataLoaderContextAccessor to access the current DataLoaderContext
75+
public OrderType(IDataLoaderContextAccessor accessor, IUsersStore users)
76+
{
77+
...
78+
79+
Field<UserType, User>()
80+
.Name("User")
81+
.ResolveAsync(context =>
82+
{
83+
// Get or add a batch loader with the key "GetUsersById"
84+
// The loader will call GetUsersByIdAsync for each batch of keys
85+
var loader = accessor.Context.GetOrAddBatchLoader<int, User>("GetUsersById", users.GetUsersByIdAsync);
86+
87+
// Add this UserId to the pending keys to fetch
88+
// The task will complete once the GetUsersByIdAsync() returns with the batched results
89+
return loader.LoadAsync(context.Source.UserId);
90+
});
91+
}
92+
}
93+
94+
public interface IUsersStore
95+
{
96+
// This will be called by the loader for all pending keys
97+
// Note that fetch delegates can accept a CancellationToken parameter or not
98+
Task<Dictionary<int, User>> GetUsersByIdAsync(IEnumerable<int> userIds, CancellationToken cancellationToken);
99+
}
100+
```
101+
102+
103+
This is an example of using a DataLoader to batch requests for loading a collection of items by a key. This is used when a key may be associated with more than one item. `LoadAsync()` is called by the field resolver for each User. A User can have zero to many Orders. `IOrdersStore.GetOrdersByUserIdAsync` will be called with a batch of userIds that have been requested.
104+
105+
``` csharp
106+
public class UserType : ObjectGraphType<User>
107+
{
108+
// Inject the IDataLoaderContextAccessor to access the current DataLoaderContext
109+
public UserType(IDataLoaderContextAccessor accessor, IOrdersStore orders)
110+
{
111+
...
112+
113+
Field<ListGraphType<OrderType>, IEnumerable<Order>>()
114+
.Name("Orders")
115+
.ResolveAsync(ctx =>
116+
{
117+
// Get or add a collection batch loader with the key "GetOrdersByUserId"
118+
// The loader will call GetOrdersByUserIdAsync with a batch of keys
119+
var ordersLoader = accessor.Context.GetOrAddCollectionBatchLoader<int, Order>("GetOrdersByUserId",
120+
orders.GetOrdersByUserIdAsync);
121+
122+
// Add this UserId to the pending keys to fetch data for
123+
// The task will complete with an IEnumberable<Order> once the fetch delegate has returned
124+
return ordersLoader.LoadAsync(ctx.Source.UserId);
125+
});
126+
}
127+
}
128+
129+
public class OrdersStore : IOrdersStore
130+
{
131+
public async Task<ILookup<int, Order>> GetOrdersByUserIdAsync(IEnumerable<int> userIds)
132+
{
133+
var orders = await ... // load data from database
134+
135+
return orders
136+
.ToLookup(x => x.UserId);
137+
}
138+
}
139+
140+
```
141+
142+
This is an example of using a DataLoader without batching. This could be useful if the data may be requested multiple times. The result will be cached the first time. Subsequent calls to `LoadAsync()` will return the cached result.
143+
144+
``` csharp
145+
public class QueryType : ObjectGraphType
146+
{
147+
// Inject the IDataLoaderContextAccessor to access the current DataLoaderContext
148+
public QueryType(IDataLoaderContextAccessor accessor, IUsersStore users)
149+
{
150+
Field<ListGraphType<UserType>, IEnumerable<User>>()
151+
.Name("Users")
152+
.Description("Get all Users")
153+
.ResolveAsync(ctx =>
154+
{
155+
// Get or add a loader with the key "GetAllUsers"
156+
var loader = accessor.Context.GetOrAddLoader("GetAllUsers",
157+
() => users.GetAllUsersAsync());
158+
159+
// Prepare the load operation
160+
// If the result is cached, a completed Task<IEnumerable<User>> will be returned
161+
return loader.LoadAsync();
162+
});
163+
}
164+
}
165+
166+
public interface IUsersStore
167+
{
168+
Task<IEnumerable<User>> GetAllUsersAsync();
169+
}
170+
```

docs/src/index.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,4 @@
22

33
* [Getting Started](getting-started)
44
* [Learn Advanced Topics](learn)
5+
* [DataLoader](dataloader)

docs/src/layout.htm

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,9 @@
2323
<li>
2424
<[linkto:learn]>
2525
</li>
26+
<li>
27+
<[linkto:dataloader]>
28+
</li>
2629
<[linkto:{previous};<li><a href="{href}" title="{title}">Previous</a></li>]>
2730
<[linkto:{next};<li><a href="{href}" title="{title}">Next</a></li>]>
2831
</ul>

docs/src/learn.md

Lines changed: 3 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,6 @@ Field<ListGraphType<DinnerType>>(
3434
GraphQL .NET supports dependency injection through a simple resolve function on the Schema class. Internally when trying to resolve a type the library will call this resolve function.
3535

3636

37-
3837
The default implementation uses `Activator.CreateInstance`.
3938

4039
```csharp
@@ -282,50 +281,11 @@ Or simply put on average we will have **2x Products** each will have 1 Title for
282281

283282
Now if we set the ```avgImpact``` to 2.0 and set the ```MaxComplexity``` to 23 (or higher) the query will execute correctly. If we change the ```MaxComplexity``` to something like 20 the DocumentExecutor will fail right after parsing the AST tree and will not attempt to resolve any of the fields (or talk to the database).
284283

285-
## Query Batching
286-
287-
Query batching allows you to make a single request to your data store instead of multiple requests. This can also often be referred to as the ["N+1"](http://stackoverflow.com/questions/97197/what-is-the-n1-selects-issue) problem. One technique of accomplishing this is to have all of your resolvers return a `Task`, then resolve those tasks when the batch is complete. Some projects provide features like [Marten Batched Queries](http://jasperfx.github.io/marten/documentation/documents/querying/batched_queries/) that support this pattern.
288-
289-
The trick is knowing when to execute the batched query. GraphQL .NET provides the ability to add listeners in the execution pipeline. Combined with a custom `UserContext` this makes executing the batch trivial.
290-
291-
```csharp
292-
public class GraphQLUserContext
293-
{
294-
// a Marten batched query
295-
public IBatchedQuery Batch { get; set; }
296-
}
297-
298-
var result = await executer.ExecuteAsync(_ =>
299-
{
300-
...
301-
_.UserContext = userContext;
302-
_.Listeners.Add(new ExecuteBatchListener());
303-
});
304-
305-
public class ExecuteBatchListener : DocumentExecutionListenerBase<GraphQLUserContext>
306-
{
307-
public override async Task BeforeExecutionAwaitedAsync(
308-
GraphQLUserContext userContext,
309-
CancellationToken token)
310-
{
311-
await userContext.Batch.Execute(token);
312-
}
313-
}
314-
315-
// using the Batched Query in the field resolver
316-
Field<ListGraphType<DinnerType>>(
317-
"popularDinners",
318-
resolve: context =>
319-
{
320-
var userContext = context.UserContext.As<GraphQLUserContext>();
321-
return userContext.Batch.Query(new FindPopularDinners());
322-
});
323-
```
284+
## DataLoader
324285

325-
## Projects attempting to solve N+1
286+
GraphQL .NET includes an implementation of Facebook's [DataLoader](https://github.com/facebook/dataloader).
326287

327-
* [Marten](http://jasperfx.github.io/marten/documentation/documents/querying/batched_queries/) - by Jeremy Miller, PostgreSQL
328-
* [GraphQL .NET DataLoader](https://github.com/dlukez/graphql-dotnet-dataloader) by [Daniel Zimmermann](https://github.com/dlukez)
288+
Documentation is here: <[linkto:dataloader]>
329289

330290
## Metrics
331291

src/GraphQL/DataLoader/DataLoaderContext.cs

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@
44

55
namespace GraphQL.DataLoader
66
{
7+
/// <summary>
8+
/// Provides a way to register DataLoader instances
9+
/// </summary>
710
public class DataLoaderContext
811
{
912
private readonly Dictionary<string, IDataLoader> _loaders = new Dictionary<string, IDataLoader>();
@@ -12,10 +15,10 @@ public class DataLoaderContext
1215
/// <summary>
1316
/// Add a new data loader if one does not already exist with the provided key
1417
/// </summary>
15-
/// <typeparam name="TDataLoader"></typeparam>
16-
/// <param name="loaderKey"></param>
17-
/// <param name="dataLoaderFactory"></param>
18-
/// <returns></returns>
18+
/// <typeparam name="TDataLoader">The type of <seealso cref="IDataLoader"/></typeparam>
19+
/// <param name="loaderKey">Unique string to identify the <seealso cref="IDataLoader"/> instance</param>
20+
/// <param name="dataLoaderFactory">Function to create the TDataLoader instance if it does not already exist</param>
21+
/// <returns>Returns an existing TDataLoader instance or a newly created instance if it did not exist already</returns>
1922
public TDataLoader GetOrAdd<TDataLoader>(string loaderKey, Func<TDataLoader> dataLoaderFactory)
2023
where TDataLoader : IDataLoader
2124
{
@@ -42,9 +45,9 @@ public TDataLoader GetOrAdd<TDataLoader>(string loaderKey, Func<TDataLoader> dat
4245
}
4346

4447
/// <summary>
45-
/// Dispatch all queued data loaders
48+
/// Dispatch all registered data loaders
4649
/// </summary>
47-
/// <param name="cancellationToken"></param>
50+
/// <param name="cancellationToken">Optional <seealso cref="CancellationToken"/> to pass to fetch delegate</param>
4851
public void DispatchAll(CancellationToken cancellationToken = default(CancellationToken))
4952
{
5053
lock (_loaders)

0 commit comments

Comments
 (0)