@@ -6,64 +6,92 @@ This is a Swift version of the Facebook [DataLoader](https://github.com/facebook
6
6
[ ![ Swift] [ swift-badge ]] [ swift-url ]
7
7
[ ![ License] [ mit-badge ]] [ mit-url ]
8
8
9
- ## Installation 💻
9
+ ## Gettings started 🚀
10
10
11
- Update your ` Package.swift ` file.
11
+ Include this repo in your ` Package.swift ` file.
12
12
13
13
``` swift
14
14
.Package (url : " https://github.com/GraphQLSwift/DataLoader.git" , .upToNextMajor (from : " 1.1.0" ))
15
15
```
16
16
17
- ## Gettings started 🚀
18
- ### Batching
19
- Batching is not an advanced feature, it's DataLoader's primary feature.
17
+ To get started, create a DataLoader. Each DataLoader instance represents a unique cache. Typically instances are created per request when used
18
+ within a web-server if different users can see different things.
20
19
21
- Create a DataLoader by providing a batch loading function
20
+ ## Batching 🍪
21
+ Batching is not an advanced feature, it's DataLoader's primary feature.
22
+
23
+ Create a DataLoader by providing a batch loading function:
22
24
``` swift
23
25
let userLoader = Dataloader< Int , User> (batchLoadFunction : { keys in
24
26
try User.query (on : req).filter (\User.id ~~ keys).all ().map { users in
25
- return users.map { DataLoaderFutureValue.success ($0 ) }
27
+ keys.map { key in
28
+ DataLoaderFutureValue.success (users.filter { $0 .id == key })
29
+ }
26
30
}
27
31
})
28
32
```
29
- #### Load single key
33
+
34
+ The order of the returned DataLoaderFutureValues must match the order of the keys.
35
+
36
+ ### Load individual keys
30
37
``` swift
31
38
let future1 = try userLoader.load (key : 1 , on : eventLoopGroup)
32
39
let future2 = try userLoader.load (key : 2 , on : eventLoopGroup)
33
40
let future3 = try userLoader.load (key : 1 , on : eventLoopGroup)
34
41
```
35
42
36
- Now there is only one thing left and that is to dispathc it ` try userLoader.dispatchQueue(on: req.eventLoop) `
37
-
38
43
The example above will only fetch two users, because the user with key ` 1 ` is present twice in the list.
39
44
40
- #### Load multiple keys
45
+ ### Load multiple keys
41
46
There is also a method to load multiple keys at once
42
47
``` swift
43
48
try userLoader.loadMany (keys : [1 , 2 , 3 ], on : eventLoopGroup)
44
49
```
45
50
46
- #### Disable batching
47
- It is possible to disable batching ` DataLoaderOptions(batchingEnabled: false) `
48
- It will invoke ` batchLoadFunction ` immediately whenever any key is loaded
51
+ ### Execution
52
+ By default, a DataLoader will wait for a short time from the moment ` load ` is called to collect keys prior
53
+ to running the ` batchLoadFunction ` and completing the ` load ` futures. This is to let keys accumulate and
54
+ batch into a smaller number of total requests. This amount of time is configurable using the ` executionPeriod `
55
+ option:
56
+
57
+ ``` swift
58
+ let myLoader = DataLoader< String , String > (
59
+ options : DataLoaderOptions (executionPeriod : .milliseconds (50 )),
60
+ batchLoadFunction : { keys in
61
+ self .someBatchLoader (keys : keys).map { DataLoaderFutureValue.success ($0 ) }
62
+ }
63
+ )
64
+ ```
65
+
66
+ Longer execution periods reduce the number of total data requests, but also reduce the responsiveness of the
67
+ ` load ` futures.
68
+
69
+ If desired, you can manually execute the ` batchLoadFunction ` and complete the futures at any time, using the
70
+ ` .execute() ` method.
71
+
72
+ Scheduled execution can be disabled by setting ` executionPeriod ` to ` nil ` , but be careful - you * must* call ` .execute() `
73
+ manually in this case. Otherwise, the futures will never complete!
74
+
75
+ ### Disable batching
76
+ It is possible to disable batching by setting the ` batchingEnabled ` option to ` false ` .
77
+ In this case, the ` batchLoadFunction ` will be invoked immediately when a key is loaded.
49
78
50
- ### Caching
51
79
52
- DataLoader provides a memoization cache for all loads which occur in a single
53
- request to your application . After ` .load() ` is called once with a given key,
54
- the resulting value is cached to eliminate redundant loads.
80
+ ## Caching 💰
81
+ DataLoader provides a memoization cache . After ` .load() ` is called with a key, the resulting value is cached
82
+ for the lifetime of the DataLoader object. This eliminates redundant loads.
55
83
56
- In addition to relieving pressure on your data storage, caching results per-request
57
- also creates fewer objects which may relieve memory pressure on your application:
84
+ In addition to relieving pressure on your data storage, caching results also creates fewer objects which may
85
+ relieve memory pressure on your application:
58
86
59
87
``` swift
60
88
let userLoader = DataLoader< Int , Int > (... )
61
89
let future1 = userLoader.load (key : 1 , on : eventLoopGroup)
62
90
let future2 = userLoader.load (key : 1 , on : eventLoopGroup)
63
- assert (future1 === future2)
91
+ print (future1 == future2) // true
64
92
```
65
93
66
- #### Caching per-Request
94
+ ### Caching per-Request
67
95
68
96
DataLoader caching * does not* replace Redis, Memcache, or any other shared
69
97
application-level cache. DataLoader is first and foremost a data loading mechanism,
@@ -76,7 +104,7 @@ could result in cached data incorrectly appearing in each request. Typically,
76
104
DataLoader instances are created when a Request begins, and are not used once the
77
105
Request ends.
78
106
79
- #### Clearing Cache
107
+ ### Clearing Cache
80
108
81
109
In certain uncommon cases, clearing the request cache may be necessary.
82
110
@@ -94,15 +122,15 @@ let userLoader = DataLoader<Int, Int>(...)
94
122
userLoader.load (key : 4 , on : eventLoopGroup)
95
123
96
124
// A mutation occurs, invalidating what might be in cache.
97
- sqlRun ('UPDATE users WHERE id= 4 SET username= " zuck" ').then { userLoader.clear (4 ) }
125
+ sqlRun ('UPDATE users WHERE id= 4 SET username= " zuck" ').whenComplete { userLoader.clear (key : 4 ) }
98
126
99
127
// Later the value load is loaded again so the mutated data appears.
100
128
userLoader.load (key : 4 , on : eventLoopGroup)
101
129
102
130
// Request completes.
103
131
```
104
132
105
- #### Caching Errors
133
+ ### Caching Errors
106
134
107
135
If a batch load fails (that is, a batch function throws or returns a DataLoaderFutureValue.failure(Error)),
108
136
then the requested values will not be cached. However if a batch
@@ -112,15 +140,15 @@ be cached to avoid frequently loading the same `Error`.
112
140
In some circumstances you may wish to clear the cache for these individual Errors:
113
141
114
142
``` swift
115
- userLoader.load (key : 1 , on : eventLoopGroup).catch { error in {
143
+ userLoader.load (key : 1 , on : eventLoopGroup).whenFailure { error in
116
144
if (/* determine if should clear error */ ) {
117
145
userLoader.clear (key : 1 );
118
146
}
119
147
throw error
120
148
}
121
149
```
122
150
123
- #### Disabling Cache
151
+ ### Disabling Cache
124
152
125
153
In certain uncommon cases, a DataLoader which * does not* cache may be desirable.
126
154
Calling ` DataLoader(options: DataLoaderOptions(cachingEnabled: false), batchLoadFunction: batchLoadFunction) ` will ensure that every
@@ -162,18 +190,112 @@ let myLoader = DataLoader<String, String>(batchLoadFunction: { keys in
162
190
})
163
191
```
164
192
193
+ ## Using with GraphQL 🎀
194
+
195
+ DataLoader pairs nicely well with [ GraphQL] ( https://github.com/GraphQLSwift/GraphQL ) and
196
+ [ Graphiti] ( https://github.com/GraphQLSwift/Graphiti ) . GraphQL fields are designed to be
197
+ stand-alone functions. Without a caching or batching mechanism,
198
+ it's easy for a naive GraphQL server to issue new database requests each time a
199
+ field is resolved.
200
+
201
+ Consider the following GraphQL request:
202
+
203
+ ```
204
+ {
205
+ me {
206
+ name
207
+ bestFriend {
208
+ name
209
+ }
210
+ friends(first: 5) {
211
+ name
212
+ bestFriend {
213
+ name
214
+ }
215
+ }
216
+ }
217
+ }
218
+ ```
219
+
220
+ Naively, if ` me ` , ` bestFriend ` and ` friends ` each need to request the backend,
221
+ there could be at most 12 database requests!
222
+
223
+ By using DataLoader, we could batch our requests to a ` User ` type, and
224
+ only require at most 4 database requests, and possibly fewer if there are cache hits.
225
+ Here's a full example using Graphiti:
226
+
227
+ ``` swift
228
+ struct User : Codable {
229
+ let id: Int
230
+ let name: String
231
+ let bestFriendID: Int
232
+ let friendIDs: [Int ]
233
+
234
+ func getBestFriend (context : UserContext, arguments : NoArguments, group : EventLoopGroup) throws -> EventLoopFuture<User> {
235
+ return try context.userLoader .load (key : user.bestFriendID , on : group)
236
+ }
237
+
238
+ struct FriendArguments {
239
+ first: Int
240
+ }
241
+ func getFriends (context : UserContext, arguments : FriendArguments, group : EventLoopGroup) throws -> EventLoopFuture<[User]> {
242
+ return try context.userLoader .loadMany (keys : user.friendIDs [0 ..< arguments.first ], on : group)
243
+ }
244
+ }
245
+
246
+ struct UserResolver {
247
+ public func me (context : UserContext, arguments : NoArguments) -> User {
248
+ ...
249
+ }
250
+ }
251
+
252
+ class UserContext {
253
+ let database = ...
254
+ let userLoader = DataLoader< Int , User> () { [unowned self ] keys in
255
+ return User.query (on : self .database ).filter (\.$id ~~ keys).all ().map { users in
256
+ keys.map { key in
257
+ users.first { $0 .id == key }!
258
+ }
259
+ }
260
+ }
261
+ }
262
+
263
+ struct UserAPI : API {
264
+ let resolver = UserResolver ()
265
+ let schema = Schema< UserResolver, UserContext> {
266
+ Type (User.self ) {
267
+ Field (" name" , at : \.content )
268
+ Field (" bestFriend" , at : \.getBestFriend , as : TypeReference< User> .self )
269
+ Field (" friends" , at : \.getFriends , as : [TypeReference< User> ]? .self ) {
270
+ Argument (" first" , at : .\first)
271
+ }
272
+ }
273
+
274
+ Query {
275
+ Field (" me" , at : UserResolver.hero , as : User.self )
276
+ }
277
+ }
278
+ }
279
+ ```
280
+
165
281
## Contributing 🤘
166
282
167
- All your feedback and help to improve this project is very welcome. Please create issues for your bugs, ideas and enhancement requests, or better yet, contribute directly by creating a PR. 😎
283
+ All your feedback and help to improve this project is very welcome. Please create issues for your bugs, ideas and
284
+ enhancement requests, or better yet, contribute directly by creating a PR. 😎
168
285
169
- When reporting an issue, please add a detailed instruction, and if possible a code snippet or test that can be used as a reproducer of your problem. 💥
286
+ When reporting an issue, please add a detailed example, and if possible a code snippet or test
287
+ to reproduce your problem. 💥
170
288
171
- When creating a pull request, please adhere to the current coding style where possible, and create tests with your code so it keeps providing an awesome test coverage level 💪
289
+ When creating a pull request, please adhere to the current coding style where possible, and create tests with your
290
+ code so it keeps providing an awesome test coverage level 💪
172
291
173
292
## Acknowledgements 👏
174
293
175
- This library is entirely a Swift version of Facebooks [DataLoader](https :// github.com/facebook/dataloader). Developed by [Lee Byron](https://github.com/leebyron) and
176
- [Nicholas Schrock](https :// github.com/schrockn) from [Facebook](https://www.facebook.com/).
294
+ This library is entirely a Swift version of Facebooks [ DataLoader] ( https://github.com/facebook/dataloader ) .
295
+ Developed by [ Lee Byron] ( https://github.com/leebyron ) and [ Nicholas Schrock] ( https://github.com/schrockn )
296
+ from [ Facebook] ( https://www.facebook.com/ ) .
297
+
298
+
177
299
178
300
[ swift-badge ] : https://img.shields.io/badge/Swift-5.2-orange.svg?style=flat
179
301
[ swift-url ] : https://swift.org
0 commit comments