Skip to content

Commit 3d34c1b

Browse files
Merge pull request #10 from NeedleInAJayStack/feature/autoExecute
Feature/auto execute
2 parents 3c99c85 + fa2cb83 commit 3d34c1b

File tree

6 files changed

+393
-162
lines changed

6 files changed

+393
-162
lines changed

Package.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ let package = Package(
1212
.package(url: "https://github.com/apple/swift-nio.git", from: "2.0.0"),
1313
],
1414
targets: [
15-
.target(name: "DataLoader", dependencies: ["NIO"]),
15+
.target(name: "DataLoader", dependencies: ["NIO", "NIOConcurrencyHelpers"]),
1616
.testTarget(name: "DataLoaderTests", dependencies: ["DataLoader"]),
1717
],
1818
swiftLanguageVersions: [.v5]

README.md

+154-32
Original file line numberDiff line numberDiff line change
@@ -6,64 +6,92 @@ This is a Swift version of the Facebook [DataLoader](https://github.com/facebook
66
[![Swift][swift-badge]][swift-url]
77
[![License][mit-badge]][mit-url]
88

9-
## Installation 💻
9+
## Gettings started 🚀
1010

11-
Update your `Package.swift` file.
11+
Include this repo in your `Package.swift` file.
1212

1313
```swift
1414
.Package(url: "https://github.com/GraphQLSwift/DataLoader.git", .upToNextMajor(from: "1.1.0"))
1515
```
1616

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.
2019

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:
2224
```swift
2325
let userLoader = Dataloader<Int, User>(batchLoadFunction: { keys in
2426
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+
}
2630
}
2731
})
2832
```
29-
#### Load single key
33+
34+
The order of the returned DataLoaderFutureValues must match the order of the keys.
35+
36+
### Load individual keys
3037
```swift
3138
let future1 = try userLoader.load(key: 1, on: eventLoopGroup)
3239
let future2 = try userLoader.load(key: 2, on: eventLoopGroup)
3340
let future3 = try userLoader.load(key: 1, on: eventLoopGroup)
3441
```
3542

36-
Now there is only one thing left and that is to dispathc it `try userLoader.dispatchQueue(on: req.eventLoop)`
37-
3843
The example above will only fetch two users, because the user with key `1` is present twice in the list.
3944

40-
#### Load multiple keys
45+
### Load multiple keys
4146
There is also a method to load multiple keys at once
4247
```swift
4348
try userLoader.loadMany(keys: [1, 2, 3], on: eventLoopGroup)
4449
```
4550

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.
4978

50-
### Caching
5179

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.
5583

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:
5886

5987
```swift
6088
let userLoader = DataLoader<Int, Int>(...)
6189
let future1 = userLoader.load(key: 1, on: eventLoopGroup)
6290
let future2 = userLoader.load(key: 1, on: eventLoopGroup)
63-
assert(future1 === future2)
91+
print(future1 == future2) // true
6492
```
6593

66-
#### Caching per-Request
94+
### Caching per-Request
6795

6896
DataLoader caching *does not* replace Redis, Memcache, or any other shared
6997
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,
76104
DataLoader instances are created when a Request begins, and are not used once the
77105
Request ends.
78106

79-
#### Clearing Cache
107+
### Clearing Cache
80108

81109
In certain uncommon cases, clearing the request cache may be necessary.
82110

@@ -94,15 +122,15 @@ let userLoader = DataLoader<Int, Int>(...)
94122
userLoader.load(key: 4, on: eventLoopGroup)
95123

96124
// 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) }
98126

99127
// Later the value load is loaded again so the mutated data appears.
100128
userLoader.load(key: 4, on: eventLoopGroup)
101129

102130
// Request completes.
103131
```
104132

105-
#### Caching Errors
133+
### Caching Errors
106134

107135
If a batch load fails (that is, a batch function throws or returns a DataLoaderFutureValue.failure(Error)),
108136
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`.
112140
In some circumstances you may wish to clear the cache for these individual Errors:
113141

114142
```swift
115-
userLoader.load(key: 1, on: eventLoopGroup).catch { error in {
143+
userLoader.load(key: 1, on: eventLoopGroup).whenFailure { error in
116144
if (/* determine if should clear error */) {
117145
userLoader.clear(key: 1);
118146
}
119147
throw error
120148
}
121149
```
122150

123-
#### Disabling Cache
151+
### Disabling Cache
124152

125153
In certain uncommon cases, a DataLoader which *does not* cache may be desirable.
126154
Calling `DataLoader(options: DataLoaderOptions(cachingEnabled: false), batchLoadFunction: batchLoadFunction)` will ensure that every
@@ -162,18 +190,112 @@ let myLoader = DataLoader<String, String>(batchLoadFunction: { keys in
162190
})
163191
```
164192

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+
165281
## Contributing 🤘
166282

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. 😎
168285

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. 💥
170288

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 💪
172291

173292
## Acknowledgements 👏
174293

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+
177299

178300
[swift-badge]: https://img.shields.io/badge/Swift-5.2-orange.svg?style=flat
179301
[swift-url]: https://swift.org

0 commit comments

Comments
 (0)