Repository files navigation

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.

This is a Swift version of the Facebook DataLoader.

Getting started 🚀

Include this repo in your Package.swift file.

.package(url:"https://github.com/GraphQLSwift/DataLoader.git", from:"2.0.0")

The AsyncDataLoader library is preferred. The DataLoader uses NIO for concurrency and is provided for backwards compatibility.

To get started, create a DataLoader. Each DataLoader instance represents a unique cache. Typically instances are created per request when used within a web-server if different users can see different things.

Batching 🍪

Batching is not an advanced feature, it's DataLoader's primary feature. Create a DataLoader by providing a batch loading function:

import AsyncDataLoader
letuserLoader=DataLoader<Int,User>(batchLoadFunction:{ keys intryUser.query(on: req).filter(\User.id ~~ keys).all().map{ users in
keys.map{ key inDataLoaderFutureValue.success(users.filter{ $0.id == key })}}})

The order of the returned DataLoaderFutureValues must match the order of the input keys.

Load individual keys

asyncletresult1= userLoader.load(key:1)asyncletresult2= userLoader.load(key:2)asyncletresult3= userLoader.load(key:1)

The example above will only fetch two users, because the user with key 1 is present twice in the list.

Load multiple keys

There is also a method to load multiple keys at once

tryawait userLoader.loadMany(keys:[1,2,3])

Execution

By default, a DataLoader will wait for a short time from the moment load is called to collect keys prior to running the batchLoadFunction and completing the load results. This allows keys to accumulate and batch into a smaller number of total requests. This amount of time is configurable using the executionPeriod option:

letmyLoader=DataLoader<String,String>(
options:DataLoaderOptions(executionPeriod:.milliseconds(50)),
batchLoadFunction:{ keys inself.someBatchLoader(keys: keys).map{DataLoaderFutureValue.success($0)}})

Longer execution periods reduce the number of total data requests, but also reduce the responsiveness of the load futures.

If desired, you can manually execute the batchLoadFunction and complete the futures at any time, using the .execute() method.

Scheduled execution can be disabled by setting executionPeriod to nil, but be careful - you must call .execute() manually in this case. Otherwise, the futures will never complete!

Disable batching

It is possible to disable batching by setting batchingEnabled to false. In this case, the batchLoadFunction will be invoked immediately when a key is loaded.

Caching 💰

DataLoader provides a memoization cache. After .load() is called with a key, the resulting value is cached for the lifetime of the DataLoader object. This eliminates redundant loads.

In addition to relieving pressure on your data storage, caching results also creates fewer objects which may relieve memory pressure on your application:

letuserLoader=DataLoader<Int,Int>(...)asyncletresult1= userLoader.load(key:1)asyncletresult2= userLoader.load(key:1)awaitprint(result1 == result2) // true

Caching per-Request

DataLoader caching does not replace Redis, Memcache, or any other shared application-level cache. DataLoader is first and foremost a data loading mechanism, and its cache only serves the purpose of not repeatedly loading the same data in the context of a single request to your Application. To do this, it maintains a simple in-memory memoization cache (more accurately: .load() is a memoized function).

Avoid multiple requests from different users using the DataLoader instance, which could result in cached data incorrectly appearing in each request. Typically, DataLoader instances are created when a Request begins, and are not used once the Request ends.

Clearing Cache

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

The most common example when clearing the loader's cache is necessary is after a mutation or update within the same request, when a cached value could be out of date and future loads should not use any possibly cached value.

Here's a simple example using SQL UPDATE to illustrate.

// Request begins...
letuserLoader=DataLoader<Int,Int>(...)
// And a value happens to be loaded (and cached).
tryawait userLoader.load(key:4)
// A mutation occurs, invalidating what might be in cache.
tryawaitsqlRun('UPDATE users WHERE id=4 SET username="zuck"')await userLoader.clear(key:4)
// Later the value load is loaded again so the mutated data appears.
tryawait userLoader.load(key:4)
// Request completes.

Caching Errors

If a batch load fails (that is, a batch function throws or returns a DataLoaderFutureValue.failure(Error)), then the requested values will not be cached. However if a batch function returns an Error instance for an individual value, that Error will be cached to avoid frequently loading the same Error.

In some circumstances you may wish to clear the cache for these individual Errors:

do{tryawait userLoader.load(key:1)}catch{if(/* determine if should clear error */){await userLoader.clear(key:1);
}throw error
}

Disabling Cache

In certain uncommon cases, a DataLoader which does not cache may be desirable. Calling DataLoader(options: DataLoaderOptions(cachingEnabled: false), batchLoadFunction: batchLoadFunction) will ensure that every call to .load() will produce a new Future, and previously requested keys will not be saved in memory.

However, when the memoization cache is disabled, your batch function will receive an array of keys which may contain duplicates! Each key will be associated with each call to .load(). Your batch loader should provide a value for each instance of the requested key.

For example:

letmyLoader=DataLoader<String,String>(
options:DataLoaderOptions(cachingEnabled:false),
batchLoadFunction:{ keys inself.someBatchLoader(keys: keys).map{DataLoaderFutureValue.success($0)}})tryawait myLoader.load(key:"A")tryawait myLoader.load(key:"B")tryawait myLoader.load(key:"A")
// > [ "A", "B", "A" ]

More complex cache behavior can be achieved by calling .clear() or .clearAll() rather than disabling the cache completely. For example, this DataLoader will provide unique keys to a batch function due to the memoization cache being enabled, but will immediately clear its cache when the batch function is called so later requests will load new values.

letmyLoader=DataLoader<String,String>(batchLoadFunction:{ keys inawait identityLoader.clearAll()returnsomeBatchLoad(keys: keys)})

Using with GraphQL 🎀

DataLoader pairs nicely well with GraphQL and Graphiti. GraphQL fields are designed to be stand-alone functions. Without a caching or batching mechanism, it's easy for a naive GraphQL server to issue new database requests each time a field is resolved.

Consider the following GraphQL request:

{
me {
name
bestFriend {
name
}
friends(first: 5) {
name
bestFriend {
name
}
}
}
}

Naively, if me, bestFriend and friends each need to request the backend, there could be at most 12 database requests!

By using DataLoader, we could batch our requests to a User type, and only require at most 4 database requests, and possibly fewer if there are cache hits. Here's a full example using Graphiti:

structUser:Codable{letid:Intletname:StringletbestFriendID:IntletfriendIDs:[Int]func getBestFriend(context:UserContext, arguments:NoArguments)throws->User{returntryawait context.userLoader.load(key: user.bestFriendID)}struct FriendArguments {
first: Int
}func getFriends(context:UserContext, arguments:FriendArguments)throws->[User]{returntryawait context.userLoader.loadMany(keys: user.friendIDs[0..<arguments.first])}}structUserResolver{publicfunc me(context:UserContext, arguments:NoArguments)->User{...}}classUserContext{letdatabase=...letuserLoader=DataLoader<Int,User>(){[weak self] keys inguardlet self =selfelse{throw ContextError }letusers=tryawaitUser.query(on:self.database).filter(\.$id ~~ keys).all()return keys.map{ key in
users.first{ $0.id == key }!
}}}structUserAPI:API{letresolver=UserResolver()letschema=Schema<UserResolver,UserContext>{Type(User.self){Field("name", at: \.content)Field("bestFriend", at: \.getBestFriend, as: TypeReference<User>.self)Field("friends", at: \.getFriends, as:[TypeReference<User>]?.self){Argument("first", at:.\first)}}Query{Field("me", at:UserResolver.hero, as:User.self)}}}

Contributing 🤘

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

When reporting an issue, please add a detailed example, and if possible a code snippet or test to reproduce your problem. 💥

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 💪

This repo uses the standard swift format, and includes lint checks to enforce these formatting standards. To format your code, run:

swift format --parallel --in-place --recursive ./

Acknowledgements 👏

This library is entirely a Swift version of Facebook's DataLoader. Developed by Lee Byron and Nicholas Schrock from Facebook.

About

DataLoader is a generic utility to be used as part of your Swift 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.

Topics

Resources

Stars

38 stars

Watchers

6 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Repository files navigation

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.

This is a Swift version of the Facebook DataLoader.

Getting started 🚀

Include this repo in your Package.swift file.

.package(url:"https://github.com/GraphQLSwift/DataLoader.git", from:"2.0.0")

The AsyncDataLoader library is preferred. The DataLoader uses NIO for concurrency and is provided for backwards compatibility.

To get started, create a DataLoader. Each DataLoader instance represents a unique cache. Typically instances are created per request when used within a web-server if different users can see different things.

Batching 🍪

Batching is not an advanced feature, it's DataLoader's primary feature. Create a DataLoader by providing a batch loading function:

import AsyncDataLoader
letuserLoader=DataLoader<Int,User>(batchLoadFunction:{ keys intryUser.query(on: req).filter(\User.id ~~ keys).all().map{ users in
keys.map{ key inDataLoaderFutureValue.success(users.filter{ $0.id == key })}}})

The order of the returned DataLoaderFutureValues must match the order of the input keys.

Load individual keys

asyncletresult1= userLoader.load(key:1)asyncletresult2= userLoader.load(key:2)asyncletresult3= userLoader.load(key:1)

The example above will only fetch two users, because the user with key 1 is present twice in the list.

Load multiple keys

There is also a method to load multiple keys at once

tryawait userLoader.loadMany(keys:[1,2,3])

Execution

By default, a DataLoader will wait for a short time from the moment load is called to collect keys prior to running the batchLoadFunction and completing the load results. This allows keys to accumulate and batch into a smaller number of total requests. This amount of time is configurable using the executionPeriod option:

letmyLoader=DataLoader<String,String>(
options:DataLoaderOptions(executionPeriod:.milliseconds(50)),
batchLoadFunction:{ keys inself.someBatchLoader(keys: keys).map{DataLoaderFutureValue.success($0)}})

Longer execution periods reduce the number of total data requests, but also reduce the responsiveness of the load futures.

If desired, you can manually execute the batchLoadFunction and complete the futures at any time, using the .execute() method.

Scheduled execution can be disabled by setting executionPeriod to nil, but be careful - you must call .execute() manually in this case. Otherwise, the futures will never complete!

Disable batching

It is possible to disable batching by setting batchingEnabled to false. In this case, the batchLoadFunction will be invoked immediately when a key is loaded.

Caching 💰

DataLoader provides a memoization cache. After .load() is called with a key, the resulting value is cached for the lifetime of the DataLoader object. This eliminates redundant loads.

In addition to relieving pressure on your data storage, caching results also creates fewer objects which may relieve memory pressure on your application:

letuserLoader=DataLoader<Int,Int>(...)asyncletresult1= userLoader.load(key:1)asyncletresult2= userLoader.load(key:1)awaitprint(result1 == result2) // true

Caching per-Request

DataLoader caching does not replace Redis, Memcache, or any other shared application-level cache. DataLoader is first and foremost a data loading mechanism, and its cache only serves the purpose of not repeatedly loading the same data in the context of a single request to your Application. To do this, it maintains a simple in-memory memoization cache (more accurately: .load() is a memoized function).

Avoid multiple requests from different users using the DataLoader instance, which could result in cached data incorrectly appearing in each request. Typically, DataLoader instances are created when a Request begins, and are not used once the Request ends.

Clearing Cache

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

The most common example when clearing the loader's cache is necessary is after a mutation or update within the same request, when a cached value could be out of date and future loads should not use any possibly cached value.

Here's a simple example using SQL UPDATE to illustrate.

// Request begins...
letuserLoader=DataLoader<Int,Int>(...)
// And a value happens to be loaded (and cached).
tryawait userLoader.load(key:4)
// A mutation occurs, invalidating what might be in cache.
tryawaitsqlRun('UPDATE users WHERE id=4 SET username="zuck"')await userLoader.clear(key:4)
// Later the value load is loaded again so the mutated data appears.
tryawait userLoader.load(key:4)
// Request completes.

Caching Errors

If a batch load fails (that is, a batch function throws or returns a DataLoaderFutureValue.failure(Error)), then the requested values will not be cached. However if a batch function returns an Error instance for an individual value, that Error will be cached to avoid frequently loading the same Error.

In some circumstances you may wish to clear the cache for these individual Errors:

do{tryawait userLoader.load(key:1)}catch{if(/* determine if should clear error */){await userLoader.clear(key:1);
}throw error
}

Disabling Cache

In certain uncommon cases, a DataLoader which does not cache may be desirable. Calling DataLoader(options: DataLoaderOptions(cachingEnabled: false), batchLoadFunction: batchLoadFunction) will ensure that every call to .load() will produce a new Future, and previously requested keys will not be saved in memory.

However, when the memoization cache is disabled, your batch function will receive an array of keys which may contain duplicates! Each key will be associated with each call to .load(). Your batch loader should provide a value for each instance of the requested key.

For example:

letmyLoader=DataLoader<String,String>(
options:DataLoaderOptions(cachingEnabled:false),
batchLoadFunction:{ keys inself.someBatchLoader(keys: keys).map{DataLoaderFutureValue.success($0)}})tryawait myLoader.load(key:"A")tryawait myLoader.load(key:"B")tryawait myLoader.load(key:"A")
// > [ "A", "B", "A" ]

More complex cache behavior can be achieved by calling .clear() or .clearAll() rather than disabling the cache completely. For example, this DataLoader will provide unique keys to a batch function due to the memoization cache being enabled, but will immediately clear its cache when the batch function is called so later requests will load new values.

letmyLoader=DataLoader<String,String>(batchLoadFunction:{ keys inawait identityLoader.clearAll()returnsomeBatchLoad(keys: keys)})

Using with GraphQL 🎀

DataLoader pairs nicely well with GraphQL and Graphiti. GraphQL fields are designed to be stand-alone functions. Without a caching or batching mechanism, it's easy for a naive GraphQL server to issue new database requests each time a field is resolved.

Consider the following GraphQL request:

{
me {
name
bestFriend {
name
}
friends(first: 5) {
name
bestFriend {
name
}
}
}
}

Naively, if me, bestFriend and friends each need to request the backend, there could be at most 12 database requests!

By using DataLoader, we could batch our requests to a User type, and only require at most 4 database requests, and possibly fewer if there are cache hits. Here's a full example using Graphiti:

structUser:Codable{letid:Intletname:StringletbestFriendID:IntletfriendIDs:[Int]func getBestFriend(context:UserContext, arguments:NoArguments)throws->User{returntryawait context.userLoader.load(key: user.bestFriendID)}struct FriendArguments {
first: Int
}func getFriends(context:UserContext, arguments:FriendArguments)throws->[User]{returntryawait context.userLoader.loadMany(keys: user.friendIDs[0..<arguments.first])}}structUserResolver{publicfunc me(context:UserContext, arguments:NoArguments)->User{...}}classUserContext{letdatabase=...letuserLoader=DataLoader<Int,User>(){[weak self] keys inguardlet self =selfelse{throw ContextError }letusers=tryawaitUser.query(on:self.database).filter(\.$id ~~ keys).all()return keys.map{ key in
users.first{ $0.id == key }!
}}}structUserAPI:API{letresolver=UserResolver()letschema=Schema<UserResolver,UserContext>{Type(User.self){Field("name", at: \.content)Field("bestFriend", at: \.getBestFriend, as: TypeReference<User>.self)Field("friends", at: \.getFriends, as:[TypeReference<User>]?.self){Argument("first", at:.\first)}}Query{Field("me", at:UserResolver.hero, as:User.self)}}}

Contributing 🤘

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

When reporting an issue, please add a detailed example, and if possible a code snippet or test to reproduce your problem. 💥

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 💪

This repo uses the standard swift format, and includes lint checks to enforce these formatting standards. To format your code, run:

swift format --parallel --in-place --recursive ./

Acknowledgements 👏

This library is entirely a Swift version of Facebook's DataLoader. Developed by Lee Byron and Nicholas Schrock from Facebook.

About

DataLoader is a generic utility to be used as part of your Swift 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.

Topics

Resources

Stars

38 stars

Watchers

6 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

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.

This is a Swift version of the Facebook DataLoader.

Getting started 🚀

Include this repo in your Package.swift file.

.package(url:"https://github.com/GraphQLSwift/DataLoader.git", from:"2.0.0")

The AsyncDataLoader library is preferred. The DataLoader uses NIO for concurrency and is provided for backwards compatibility.

To get started, create a DataLoader. Each DataLoader instance represents a unique cache. Typically instances are created per request when used within a web-server if different users can see different things.

Batching 🍪

Batching is not an advanced feature, it's DataLoader's primary feature. Create a DataLoader by providing a batch loading function:

import AsyncDataLoader
letuserLoader=DataLoader<Int,User>(batchLoadFunction:{ keys intryUser.query(on: req).filter(\User.id ~~ keys).all().map{ users in
keys.map{ key inDataLoaderFutureValue.success(users.filter{ $0.id == key })}}})

The order of the returned DataLoaderFutureValues must match the order of the input keys.

Load individual keys

asyncletresult1= userLoader.load(key:1)asyncletresult2= userLoader.load(key:2)asyncletresult3= userLoader.load(key:1)

The example above will only fetch two users, because the user with key 1 is present twice in the list.

Load multiple keys

There is also a method to load multiple keys at once

tryawait userLoader.loadMany(keys:[1,2,3])

Execution

By default, a DataLoader will wait for a short time from the moment load is called to collect keys prior to running the batchLoadFunction and completing the load results. This allows keys to accumulate and batch into a smaller number of total requests. This amount of time is configurable using the executionPeriod option:

letmyLoader=DataLoader<String,String>(
options:DataLoaderOptions(executionPeriod:.milliseconds(50)),
batchLoadFunction:{ keys inself.someBatchLoader(keys: keys).map{DataLoaderFutureValue.success($0)}})

Longer execution periods reduce the number of total data requests, but also reduce the responsiveness of the load futures.

If desired, you can manually execute the batchLoadFunction and complete the futures at any time, using the .execute() method.

Scheduled execution can be disabled by setting executionPeriod to nil, but be careful - you must call .execute() manually in this case. Otherwise, the futures will never complete!

Disable batching

It is possible to disable batching by setting batchingEnabled to false. In this case, the batchLoadFunction will be invoked immediately when a key is loaded.

Caching 💰

DataLoader provides a memoization cache. After .load() is called with a key, the resulting value is cached for the lifetime of the DataLoader object. This eliminates redundant loads.

In addition to relieving pressure on your data storage, caching results also creates fewer objects which may relieve memory pressure on your application:

letuserLoader=DataLoader<Int,Int>(...)asyncletresult1= userLoader.load(key:1)asyncletresult2= userLoader.load(key:1)awaitprint(result1 == result2) // true

Caching per-Request

DataLoader caching does not replace Redis, Memcache, or any other shared application-level cache. DataLoader is first and foremost a data loading mechanism, and its cache only serves the purpose of not repeatedly loading the same data in the context of a single request to your Application. To do this, it maintains a simple in-memory memoization cache (more accurately: .load() is a memoized function).

Avoid multiple requests from different users using the DataLoader instance, which could result in cached data incorrectly appearing in each request. Typically, DataLoader instances are created when a Request begins, and are not used once the Request ends.

Clearing Cache

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

The most common example when clearing the loader's cache is necessary is after a mutation or update within the same request, when a cached value could be out of date and future loads should not use any possibly cached value.

Here's a simple example using SQL UPDATE to illustrate.

// Request begins...
letuserLoader=DataLoader<Int,Int>(...)
// And a value happens to be loaded (and cached).
tryawait userLoader.load(key:4)
// A mutation occurs, invalidating what might be in cache.
tryawaitsqlRun('UPDATE users WHERE id=4 SET username="zuck"')await userLoader.clear(key:4)
// Later the value load is loaded again so the mutated data appears.
tryawait userLoader.load(key:4)
// Request completes.

Caching Errors

If a batch load fails (that is, a batch function throws or returns a DataLoaderFutureValue.failure(Error)), then the requested values will not be cached. However if a batch function returns an Error instance for an individual value, that Error will be cached to avoid frequently loading the same Error.

In some circumstances you may wish to clear the cache for these individual Errors:

do{tryawait userLoader.load(key:1)}catch{if(/* determine if should clear error */){await userLoader.clear(key:1);
}throw error
}

Disabling Cache

In certain uncommon cases, a DataLoader which does not cache may be desirable. Calling DataLoader(options: DataLoaderOptions(cachingEnabled: false), batchLoadFunction: batchLoadFunction) will ensure that every call to .load() will produce a new Future, and previously requested keys will not be saved in memory.

However, when the memoization cache is disabled, your batch function will receive an array of keys which may contain duplicates! Each key will be associated with each call to .load(). Your batch loader should provide a value for each instance of the requested key.

For example:

letmyLoader=DataLoader<String,String>(
options:DataLoaderOptions(cachingEnabled:false),
batchLoadFunction:{ keys inself.someBatchLoader(keys: keys).map{DataLoaderFutureValue.success($0)}})tryawait myLoader.load(key:"A")tryawait myLoader.load(key:"B")tryawait myLoader.load(key:"A")
// > [ "A", "B", "A" ]

More complex cache behavior can be achieved by calling .clear() or .clearAll() rather than disabling the cache completely. For example, this DataLoader will provide unique keys to a batch function due to the memoization cache being enabled, but will immediately clear its cache when the batch function is called so later requests will load new values.

letmyLoader=DataLoader<String,String>(batchLoadFunction:{ keys inawait identityLoader.clearAll()returnsomeBatchLoad(keys: keys)})

Using with GraphQL 🎀

DataLoader pairs nicely well with GraphQL and Graphiti. GraphQL fields are designed to be stand-alone functions. Without a caching or batching mechanism, it's easy for a naive GraphQL server to issue new database requests each time a field is resolved.

Consider the following GraphQL request:

{
me {
name
bestFriend {
name
}
friends(first: 5) {
name
bestFriend {
name
}
}
}
}

Naively, if me, bestFriend and friends each need to request the backend, there could be at most 12 database requests!

By using DataLoader, we could batch our requests to a User type, and only require at most 4 database requests, and possibly fewer if there are cache hits. Here's a full example using Graphiti:

structUser:Codable{letid:Intletname:StringletbestFriendID:IntletfriendIDs:[Int]func getBestFriend(context:UserContext, arguments:NoArguments)throws->User{returntryawait context.userLoader.load(key: user.bestFriendID)}struct FriendArguments {
first: Int
}func getFriends(context:UserContext, arguments:FriendArguments)throws->[User]{returntryawait context.userLoader.loadMany(keys: user.friendIDs[0..<arguments.first])}}structUserResolver{publicfunc me(context:UserContext, arguments:NoArguments)->User{...}}classUserContext{letdatabase=...letuserLoader=DataLoader<Int,User>(){[weak self] keys inguardlet self =selfelse{throw ContextError }letusers=tryawaitUser.query(on:self.database).filter(\.$id ~~ keys).all()return keys.map{ key in
users.first{ $0.id == key }!
}}}structUserAPI:API{letresolver=UserResolver()letschema=Schema<UserResolver,UserContext>{Type(User.self){Field("name", at: \.content)Field("bestFriend", at: \.getBestFriend, as: TypeReference<User>.self)Field("friends", at: \.getFriends, as:[TypeReference<User>]?.self){Argument("first", at:.\first)}}Query{Field("me", at:UserResolver.hero, as:User.self)}}}

Contributing 🤘

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

When reporting an issue, please add a detailed example, and if possible a code snippet or test to reproduce your problem. 💥

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 💪

This repo uses the standard swift format, and includes lint checks to enforce these formatting standards. To format your code, run:

swift format --parallel --in-place --recursive ./

Acknowledgements 👏

This library is entirely a Swift version of Facebook's DataLoader. Developed by Lee Byron and Nicholas Schrock from Facebook.

About

DataLoader is a generic utility to be used as part of your Swift 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.

Topics

Resources

Stars

38 stars

Watchers

6 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

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.

This is a Swift version of the Facebook DataLoader.

Getting started 🚀

Include this repo in your Package.swift file.

.package(url:"https://github.com/GraphQLSwift/DataLoader.git", from:"2.0.0")

The AsyncDataLoader library is preferred. The DataLoader uses NIO for concurrency and is provided for backwards compatibility.

To get started, create a DataLoader. Each DataLoader instance represents a unique cache. Typically instances are created per request when used within a web-server if different users can see different things.

Batching 🍪

Batching is not an advanced feature, it's DataLoader's primary feature. Create a DataLoader by providing a batch loading function:

import AsyncDataLoader
letuserLoader=DataLoader<Int,User>(batchLoadFunction:{ keys intryUser.query(on: req).filter(\User.id ~~ keys).all().map{ users in
keys.map{ key inDataLoaderFutureValue.success(users.filter{ $0.id == key })}}})

The order of the returned DataLoaderFutureValues must match the order of the input keys.

Load individual keys

asyncletresult1= userLoader.load(key:1)asyncletresult2= userLoader.load(key:2)asyncletresult3= userLoader.load(key:1)

The example above will only fetch two users, because the user with key 1 is present twice in the list.

Load multiple keys

There is also a method to load multiple keys at once

tryawait userLoader.loadMany(keys:[1,2,3])

Execution

By default, a DataLoader will wait for a short time from the moment load is called to collect keys prior to running the batchLoadFunction and completing the load results. This allows keys to accumulate and batch into a smaller number of total requests. This amount of time is configurable using the executionPeriod option:

letmyLoader=DataLoader<String,String>(
options:DataLoaderOptions(executionPeriod:.milliseconds(50)),
batchLoadFunction:{ keys inself.someBatchLoader(keys: keys).map{DataLoaderFutureValue.success($0)}})

Longer execution periods reduce the number of total data requests, but also reduce the responsiveness of the load futures.

If desired, you can manually execute the batchLoadFunction and complete the futures at any time, using the .execute() method.

Scheduled execution can be disabled by setting executionPeriod to nil, but be careful - you must call .execute() manually in this case. Otherwise, the futures will never complete!

Disable batching

It is possible to disable batching by setting batchingEnabled to false. In this case, the batchLoadFunction will be invoked immediately when a key is loaded.

Caching 💰

DataLoader provides a memoization cache. After .load() is called with a key, the resulting value is cached for the lifetime of the DataLoader object. This eliminates redundant loads.

In addition to relieving pressure on your data storage, caching results also creates fewer objects which may relieve memory pressure on your application:

letuserLoader=DataLoader<Int,Int>(...)asyncletresult1= userLoader.load(key:1)asyncletresult2= userLoader.load(key:1)awaitprint(result1 == result2) // true

Caching per-Request

DataLoader caching does not replace Redis, Memcache, or any other shared application-level cache. DataLoader is first and foremost a data loading mechanism, and its cache only serves the purpose of not repeatedly loading the same data in the context of a single request to your Application. To do this, it maintains a simple in-memory memoization cache (more accurately: .load() is a memoized function).

Avoid multiple requests from different users using the DataLoader instance, which could result in cached data incorrectly appearing in each request. Typically, DataLoader instances are created when a Request begins, and are not used once the Request ends.

Clearing Cache

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

The most common example when clearing the loader's cache is necessary is after a mutation or update within the same request, when a cached value could be out of date and future loads should not use any possibly cached value.

Here's a simple example using SQL UPDATE to illustrate.

// Request begins...
letuserLoader=DataLoader<Int,Int>(...)
// And a value happens to be loaded (and cached).
tryawait userLoader.load(key:4)
// A mutation occurs, invalidating what might be in cache.
tryawaitsqlRun('UPDATE users WHERE id=4 SET username="zuck"')await userLoader.clear(key:4)
// Later the value load is loaded again so the mutated data appears.
tryawait userLoader.load(key:4)
// Request completes.

Caching Errors

If a batch load fails (that is, a batch function throws or returns a DataLoaderFutureValue.failure(Error)), then the requested values will not be cached. However if a batch function returns an Error instance for an individual value, that Error will be cached to avoid frequently loading the same Error.

In some circumstances you may wish to clear the cache for these individual Errors:

do{tryawait userLoader.load(key:1)}catch{if(/* determine if should clear error */){await userLoader.clear(key:1);
}throw error
}

Disabling Cache

In certain uncommon cases, a DataLoader which does not cache may be desirable. Calling DataLoader(options: DataLoaderOptions(cachingEnabled: false), batchLoadFunction: batchLoadFunction) will ensure that every call to .load() will produce a new Future, and previously requested keys will not be saved in memory.

However, when the memoization cache is disabled, your batch function will receive an array of keys which may contain duplicates! Each key will be associated with each call to .load(). Your batch loader should provide a value for each instance of the requested key.

For example:

letmyLoader=DataLoader<String,String>(
options:DataLoaderOptions(cachingEnabled:false),
batchLoadFunction:{ keys inself.someBatchLoader(keys: keys).map{DataLoaderFutureValue.success($0)}})tryawait myLoader.load(key:"A")tryawait myLoader.load(key:"B")tryawait myLoader.load(key:"A")
// > [ "A", "B", "A" ]

More complex cache behavior can be achieved by calling .clear() or .clearAll() rather than disabling the cache completely. For example, this DataLoader will provide unique keys to a batch function due to the memoization cache being enabled, but will immediately clear its cache when the batch function is called so later requests will load new values.

letmyLoader=DataLoader<String,String>(batchLoadFunction:{ keys inawait identityLoader.clearAll()returnsomeBatchLoad(keys: keys)})

Using with GraphQL 🎀

DataLoader pairs nicely well with GraphQL and Graphiti. GraphQL fields are designed to be stand-alone functions. Without a caching or batching mechanism, it's easy for a naive GraphQL server to issue new database requests each time a field is resolved.

Consider the following GraphQL request:

{
me {
name
bestFriend {
name
}
friends(first: 5) {
name
bestFriend {
name
}
}
}
}

Naively, if me, bestFriend and friends each need to request the backend, there could be at most 12 database requests!

By using DataLoader, we could batch our requests to a User type, and only require at most 4 database requests, and possibly fewer if there are cache hits. Here's a full example using Graphiti:

structUser:Codable{letid:Intletname:StringletbestFriendID:IntletfriendIDs:[Int]func getBestFriend(context:UserContext, arguments:NoArguments)throws->User{returntryawait context.userLoader.load(key: user.bestFriendID)}struct FriendArguments {
first: Int
}func getFriends(context:UserContext, arguments:FriendArguments)throws->[User]{returntryawait context.userLoader.loadMany(keys: user.friendIDs[0..<arguments.first])}}structUserResolver{publicfunc me(context:UserContext, arguments:NoArguments)->User{...}}classUserContext{letdatabase=...letuserLoader=DataLoader<Int,User>(){[weak self] keys inguardlet self =selfelse{throw ContextError }letusers=tryawaitUser.query(on:self.database).filter(\.$id ~~ keys).all()return keys.map{ key in
users.first{ $0.id == key }!
}}}structUserAPI:API{letresolver=UserResolver()letschema=Schema<UserResolver,UserContext>{Type(User.self){Field("name", at: \.content)Field("bestFriend", at: \.getBestFriend, as: TypeReference<User>.self)Field("friends", at: \.getFriends, as:[TypeReference<User>]?.self){Argument("first", at:.\first)}}Query{Field("me", at:UserResolver.hero, as:User.self)}}}

Contributing 🤘

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

When reporting an issue, please add a detailed example, and if possible a code snippet or test to reproduce your problem. 💥

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 💪

This repo uses the standard swift format, and includes lint checks to enforce these formatting standards. To format your code, run:

swift format --parallel --in-place --recursive ./

Acknowledgements 👏

This library is entirely a Swift version of Facebook's DataLoader. Developed by Lee Byron and Nicholas Schrock from Facebook.

About

DataLoader is a generic utility to be used as part of your Swift 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.

Topics

Resources

Stars

38 stars

Watchers

6 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

Repository files navigation

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.

This is a Swift version of the Facebook DataLoader.

Getting started 🚀

Include this repo in your Package.swift file.

.package(url:"https://github.com/GraphQLSwift/DataLoader.git", from:"2.0.0")

The AsyncDataLoader library is preferred. The DataLoader uses NIO for concurrency and is provided for backwards compatibility.

To get started, create a DataLoader. Each DataLoader instance represents a unique cache. Typically instances are created per request when used within a web-server if different users can see different things.

Batching 🍪

Batching is not an advanced feature, it's DataLoader's primary feature. Create a DataLoader by providing a batch loading function:

import AsyncDataLoader
letuserLoader=DataLoader<Int,User>(batchLoadFunction:{ keys intryUser.query(on: req).filter(\User.id ~~ keys).all().map{ users in
keys.map{ key inDataLoaderFutureValue.success(users.filter{ $0.id == key })}}})

The order of the returned DataLoaderFutureValues must match the order of the input keys.

Load individual keys

asyncletresult1= userLoader.load(key:1)asyncletresult2= userLoader.load(key:2)asyncletresult3= userLoader.load(key:1)

The example above will only fetch two users, because the user with key 1 is present twice in the list.

Load multiple keys

There is also a method to load multiple keys at once

tryawait userLoader.loadMany(keys:[1,2,3])

Execution

By default, a DataLoader will wait for a short time from the moment load is called to collect keys prior to running the batchLoadFunction and completing the load results. This allows keys to accumulate and batch into a smaller number of total requests. This amount of time is configurable using the executionPeriod option:

letmyLoader=DataLoader<String,String>(
options:DataLoaderOptions(executionPeriod:.milliseconds(50)),
batchLoadFunction:{ keys inself.someBatchLoader(keys: keys).map{DataLoaderFutureValue.success($0)}})

Longer execution periods reduce the number of total data requests, but also reduce the responsiveness of the load futures.

If desired, you can manually execute the batchLoadFunction and complete the futures at any time, using the .execute() method.

Scheduled execution can be disabled by setting executionPeriod to nil, but be careful - you must call .execute() manually in this case. Otherwise, the futures will never complete!

Disable batching

It is possible to disable batching by setting batchingEnabled to false. In this case, the batchLoadFunction will be invoked immediately when a key is loaded.

Caching 💰

DataLoader provides a memoization cache. After .load() is called with a key, the resulting value is cached for the lifetime of the DataLoader object. This eliminates redundant loads.

In addition to relieving pressure on your data storage, caching results also creates fewer objects which may relieve memory pressure on your application:

letuserLoader=DataLoader<Int,Int>(...)asyncletresult1= userLoader.load(key:1)asyncletresult2= userLoader.load(key:1)awaitprint(result1 == result2) // true

Caching per-Request

DataLoader caching does not replace Redis, Memcache, or any other shared application-level cache. DataLoader is first and foremost a data loading mechanism, and its cache only serves the purpose of not repeatedly loading the same data in the context of a single request to your Application. To do this, it maintains a simple in-memory memoization cache (more accurately: .load() is a memoized function).

Avoid multiple requests from different users using the DataLoader instance, which could result in cached data incorrectly appearing in each request. Typically, DataLoader instances are created when a Request begins, and are not used once the Request ends.

Clearing Cache

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

The most common example when clearing the loader's cache is necessary is after a mutation or update within the same request, when a cached value could be out of date and future loads should not use any possibly cached value.

Here's a simple example using SQL UPDATE to illustrate.

// Request begins...
letuserLoader=DataLoader<Int,Int>(...)
// And a value happens to be loaded (and cached).
tryawait userLoader.load(key:4)
// A mutation occurs, invalidating what might be in cache.
tryawaitsqlRun('UPDATE users WHERE id=4 SET username="zuck"')await userLoader.clear(key:4)
// Later the value load is loaded again so the mutated data appears.
tryawait userLoader.load(key:4)
// Request completes.

Caching Errors

If a batch load fails (that is, a batch function throws or returns a DataLoaderFutureValue.failure(Error)), then the requested values will not be cached. However if a batch function returns an Error instance for an individual value, that Error will be cached to avoid frequently loading the same Error.

In some circumstances you may wish to clear the cache for these individual Errors:

do{tryawait userLoader.load(key:1)}catch{if(/* determine if should clear error */){await userLoader.clear(key:1);
}throw error
}

Disabling Cache

In certain uncommon cases, a DataLoader which does not cache may be desirable. Calling DataLoader(options: DataLoaderOptions(cachingEnabled: false), batchLoadFunction: batchLoadFunction) will ensure that every call to .load() will produce a new Future, and previously requested keys will not be saved in memory.

However, when the memoization cache is disabled, your batch function will receive an array of keys which may contain duplicates! Each key will be associated with each call to .load(). Your batch loader should provide a value for each instance of the requested key.

For example:

letmyLoader=DataLoader<String,String>(
options:DataLoaderOptions(cachingEnabled:false),
batchLoadFunction:{ keys inself.someBatchLoader(keys: keys).map{DataLoaderFutureValue.success($0)}})tryawait myLoader.load(key:"A")tryawait myLoader.load(key:"B")tryawait myLoader.load(key:"A")
// > [ "A", "B", "A" ]

More complex cache behavior can be achieved by calling .clear() or .clearAll() rather than disabling the cache completely. For example, this DataLoader will provide unique keys to a batch function due to the memoization cache being enabled, but will immediately clear its cache when the batch function is called so later requests will load new values.

letmyLoader=DataLoader<String,String>(batchLoadFunction:{ keys inawait identityLoader.clearAll()returnsomeBatchLoad(keys: keys)})

Using with GraphQL 🎀

DataLoader pairs nicely well with GraphQL and Graphiti. GraphQL fields are designed to be stand-alone functions. Without a caching or batching mechanism, it's easy for a naive GraphQL server to issue new database requests each time a field is resolved.

Consider the following GraphQL request:

{
me {
name
bestFriend {
name
}
friends(first: 5) {
name
bestFriend {
name
}
}
}
}

Naively, if me, bestFriend and friends each need to request the backend, there could be at most 12 database requests!

By using DataLoader, we could batch our requests to a User type, and only require at most 4 database requests, and possibly fewer if there are cache hits. Here's a full example using Graphiti:

structUser:Codable{letid:Intletname:StringletbestFriendID:IntletfriendIDs:[Int]func getBestFriend(context:UserContext, arguments:NoArguments)throws->User{returntryawait context.userLoader.load(key: user.bestFriendID)}struct FriendArguments {
first: Int
}func getFriends(context:UserContext, arguments:FriendArguments)throws->[User]{returntryawait context.userLoader.loadMany(keys: user.friendIDs[0..<arguments.first])}}structUserResolver{publicfunc me(context:UserContext, arguments:NoArguments)->User{...}}classUserContext{letdatabase=...letuserLoader=DataLoader<Int,User>(){[weak self] keys inguardlet self =selfelse{throw ContextError }letusers=tryawaitUser.query(on:self.database).filter(\.$id ~~ keys).all()return keys.map{ key in
users.first{ $0.id == key }!
}}}structUserAPI:API{letresolver=UserResolver()letschema=Schema<UserResolver,UserContext>{Type(User.self){Field("name", at: \.content)Field("bestFriend", at: \.getBestFriend, as: TypeReference<User>.self)Field("friends", at: \.getFriends, as:[TypeReference<User>]?.self){Argument("first", at:.\first)}}Query{Field("me", at:UserResolver.hero, as:User.self)}}}

Contributing 🤘

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

When reporting an issue, please add a detailed example, and if possible a code snippet or test to reproduce your problem. 💥

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 💪

This repo uses the standard swift format, and includes lint checks to enforce these formatting standards. To format your code, run:

swift format --parallel --in-place --recursive ./

Acknowledgements 👏

This library is entirely a Swift version of Facebook's DataLoader. Developed by Lee Byron and Nicholas Schrock from Facebook.

About

DataLoader is a generic utility to be used as part of your Swift 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.

Topics

Resources

Stars

38 stars

Watchers

6 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

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.

This is a Swift version of the Facebook DataLoader.

Getting started 🚀

Include this repo in your Package.swift file.

.package(url:"https://github.com/GraphQLSwift/DataLoader.git", from:"2.0.0")

The AsyncDataLoader library is preferred. The DataLoader uses NIO for concurrency and is provided for backwards compatibility.

To get started, create a DataLoader. Each DataLoader instance represents a unique cache. Typically instances are created per request when used within a web-server if different users can see different things.

Batching 🍪

Batching is not an advanced feature, it's DataLoader's primary feature. Create a DataLoader by providing a batch loading function:

import AsyncDataLoader
letuserLoader=DataLoader<Int,User>(batchLoadFunction:{ keys intryUser.query(on: req).filter(\User.id ~~ keys).all().map{ users in
keys.map{ key inDataLoaderFutureValue.success(users.filter{ $0.id == key })}}})

The order of the returned DataLoaderFutureValues must match the order of the input keys.

Load individual keys

asyncletresult1= userLoader.load(key:1)asyncletresult2= userLoader.load(key:2)asyncletresult3= userLoader.load(key:1)

The example above will only fetch two users, because the user with key 1 is present twice in the list.

Load multiple keys

There is also a method to load multiple keys at once

tryawait userLoader.loadMany(keys:[1,2,3])

Execution

By default, a DataLoader will wait for a short time from the moment load is called to collect keys prior to running the batchLoadFunction and completing the load results. This allows keys to accumulate and batch into a smaller number of total requests. This amount of time is configurable using the executionPeriod option:

letmyLoader=DataLoader<String,String>(
options:DataLoaderOptions(executionPeriod:.milliseconds(50)),
batchLoadFunction:{ keys inself.someBatchLoader(keys: keys).map{DataLoaderFutureValue.success($0)}})

Longer execution periods reduce the number of total data requests, but also reduce the responsiveness of the load futures.

If desired, you can manually execute the batchLoadFunction and complete the futures at any time, using the .execute() method.

Scheduled execution can be disabled by setting executionPeriod to nil, but be careful - you must call .execute() manually in this case. Otherwise, the futures will never complete!

Disable batching

It is possible to disable batching by setting batchingEnabled to false. In this case, the batchLoadFunction will be invoked immediately when a key is loaded.

Caching 💰

DataLoader provides a memoization cache. After .load() is called with a key, the resulting value is cached for the lifetime of the DataLoader object. This eliminates redundant loads.

In addition to relieving pressure on your data storage, caching results also creates fewer objects which may relieve memory pressure on your application:

letuserLoader=DataLoader<Int,Int>(...)asyncletresult1= userLoader.load(key:1)asyncletresult2= userLoader.load(key:1)awaitprint(result1 == result2) // true

Caching per-Request

DataLoader caching does not replace Redis, Memcache, or any other shared application-level cache. DataLoader is first and foremost a data loading mechanism, and its cache only serves the purpose of not repeatedly loading the same data in the context of a single request to your Application. To do this, it maintains a simple in-memory memoization cache (more accurately: .load() is a memoized function).

Avoid multiple requests from different users using the DataLoader instance, which could result in cached data incorrectly appearing in each request. Typically, DataLoader instances are created when a Request begins, and are not used once the Request ends.

Clearing Cache

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

The most common example when clearing the loader's cache is necessary is after a mutation or update within the same request, when a cached value could be out of date and future loads should not use any possibly cached value.

Here's a simple example using SQL UPDATE to illustrate.

// Request begins...
letuserLoader=DataLoader<Int,Int>(...)
// And a value happens to be loaded (and cached).
tryawait userLoader.load(key:4)
// A mutation occurs, invalidating what might be in cache.
tryawaitsqlRun('UPDATE users WHERE id=4 SET username="zuck"')await userLoader.clear(key:4)
// Later the value load is loaded again so the mutated data appears.
tryawait userLoader.load(key:4)
// Request completes.

Caching Errors

If a batch load fails (that is, a batch function throws or returns a DataLoaderFutureValue.failure(Error)), then the requested values will not be cached. However if a batch function returns an Error instance for an individual value, that Error will be cached to avoid frequently loading the same Error.

In some circumstances you may wish to clear the cache for these individual Errors:

do{tryawait userLoader.load(key:1)}catch{if(/* determine if should clear error */){await userLoader.clear(key:1);
}throw error
}

Disabling Cache

In certain uncommon cases, a DataLoader which does not cache may be desirable. Calling DataLoader(options: DataLoaderOptions(cachingEnabled: false), batchLoadFunction: batchLoadFunction) will ensure that every call to .load() will produce a new Future, and previously requested keys will not be saved in memory.

However, when the memoization cache is disabled, your batch function will receive an array of keys which may contain duplicates! Each key will be associated with each call to .load(). Your batch loader should provide a value for each instance of the requested key.

For example:

letmyLoader=DataLoader<String,String>(
options:DataLoaderOptions(cachingEnabled:false),
batchLoadFunction:{ keys inself.someBatchLoader(keys: keys).map{DataLoaderFutureValue.success($0)}})tryawait myLoader.load(key:"A")tryawait myLoader.load(key:"B")tryawait myLoader.load(key:"A")
// > [ "A", "B", "A" ]

More complex cache behavior can be achieved by calling .clear() or .clearAll() rather than disabling the cache completely. For example, this DataLoader will provide unique keys to a batch function due to the memoization cache being enabled, but will immediately clear its cache when the batch function is called so later requests will load new values.

letmyLoader=DataLoader<String,String>(batchLoadFunction:{ keys inawait identityLoader.clearAll()returnsomeBatchLoad(keys: keys)})

Using with GraphQL 🎀

DataLoader pairs nicely well with GraphQL and Graphiti. GraphQL fields are designed to be stand-alone functions. Without a caching or batching mechanism, it's easy for a naive GraphQL server to issue new database requests each time a field is resolved.

Consider the following GraphQL request:

{
me {
name
bestFriend {
name
}
friends(first: 5) {
name
bestFriend {
name
}
}
}
}

Naively, if me, bestFriend and friends each need to request the backend, there could be at most 12 database requests!

By using DataLoader, we could batch our requests to a User type, and only require at most 4 database requests, and possibly fewer if there are cache hits. Here's a full example using Graphiti:

structUser:Codable{letid:Intletname:StringletbestFriendID:IntletfriendIDs:[Int]func getBestFriend(context:UserContext, arguments:NoArguments)throws->User{returntryawait context.userLoader.load(key: user.bestFriendID)}struct FriendArguments {
first: Int
}func getFriends(context:UserContext, arguments:FriendArguments)throws->[User]{returntryawait context.userLoader.loadMany(keys: user.friendIDs[0..<arguments.first])}}structUserResolver{publicfunc me(context:UserContext, arguments:NoArguments)->User{...}}classUserContext{letdatabase=...letuserLoader=DataLoader<Int,User>(){[weak self] keys inguardlet self =selfelse{throw ContextError }letusers=tryawaitUser.query(on:self.database).filter(\.$id ~~ keys).all()return keys.map{ key in
users.first{ $0.id == key }!
}}}structUserAPI:API{letresolver=UserResolver()letschema=Schema<UserResolver,UserContext>{Type(User.self){Field("name", at: \.content)Field("bestFriend", at: \.getBestFriend, as: TypeReference<User>.self)Field("friends", at: \.getFriends, as:[TypeReference<User>]?.self){Argument("first", at:.\first)}}Query{Field("me", at:UserResolver.hero, as:User.self)}}}

Contributing 🤘

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

When reporting an issue, please add a detailed example, and if possible a code snippet or test to reproduce your problem. 💥

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 💪

This repo uses the standard swift format, and includes lint checks to enforce these formatting standards. To format your code, run:

swift format --parallel --in-place --recursive ./

Acknowledgements 👏

This library is entirely a Swift version of Facebook's DataLoader. Developed by Lee Byron and Nicholas Schrock from Facebook.

About

DataLoader is a generic utility to be used as part of your Swift 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.

Topics

Resources

Stars

38 stars

Watchers

6 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

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.

This is a Swift version of the Facebook DataLoader.

Getting started 🚀

Include this repo in your Package.swift file.

.package(url:"https://github.com/GraphQLSwift/DataLoader.git", from:"2.0.0")

The AsyncDataLoader library is preferred. The DataLoader uses NIO for concurrency and is provided for backwards compatibility.

To get started, create a DataLoader. Each DataLoader instance represents a unique cache. Typically instances are created per request when used within a web-server if different users can see different things.

Batching 🍪

Batching is not an advanced feature, it's DataLoader's primary feature. Create a DataLoader by providing a batch loading function:

import AsyncDataLoader
letuserLoader=DataLoader<Int,User>(batchLoadFunction:{ keys intryUser.query(on: req).filter(\User.id ~~ keys).all().map{ users in
keys.map{ key inDataLoaderFutureValue.success(users.filter{ $0.id == key })}}})

The order of the returned DataLoaderFutureValues must match the order of the input keys.

Load individual keys

asyncletresult1= userLoader.load(key:1)asyncletresult2= userLoader.load(key:2)asyncletresult3= userLoader.load(key:1)

The example above will only fetch two users, because the user with key 1 is present twice in the list.

Load multiple keys

There is also a method to load multiple keys at once

tryawait userLoader.loadMany(keys:[1,2,3])

Execution

By default, a DataLoader will wait for a short time from the moment load is called to collect keys prior to running the batchLoadFunction and completing the load results. This allows keys to accumulate and batch into a smaller number of total requests. This amount of time is configurable using the executionPeriod option:

letmyLoader=DataLoader<String,String>(
options:DataLoaderOptions(executionPeriod:.milliseconds(50)),
batchLoadFunction:{ keys inself.someBatchLoader(keys: keys).map{DataLoaderFutureValue.success($0)}})

Longer execution periods reduce the number of total data requests, but also reduce the responsiveness of the load futures.

If desired, you can manually execute the batchLoadFunction and complete the futures at any time, using the .execute() method.

Scheduled execution can be disabled by setting executionPeriod to nil, but be careful - you must call .execute() manually in this case. Otherwise, the futures will never complete!

Disable batching

It is possible to disable batching by setting batchingEnabled to false. In this case, the batchLoadFunction will be invoked immediately when a key is loaded.

Caching 💰

DataLoader provides a memoization cache. After .load() is called with a key, the resulting value is cached for the lifetime of the DataLoader object. This eliminates redundant loads.

In addition to relieving pressure on your data storage, caching results also creates fewer objects which may relieve memory pressure on your application:

letuserLoader=DataLoader<Int,Int>(...)asyncletresult1= userLoader.load(key:1)asyncletresult2= userLoader.load(key:1)awaitprint(result1 == result2) // true

Caching per-Request

DataLoader caching does not replace Redis, Memcache, or any other shared application-level cache. DataLoader is first and foremost a data loading mechanism, and its cache only serves the purpose of not repeatedly loading the same data in the context of a single request to your Application. To do this, it maintains a simple in-memory memoization cache (more accurately: .load() is a memoized function).

Avoid multiple requests from different users using the DataLoader instance, which could result in cached data incorrectly appearing in each request. Typically, DataLoader instances are created when a Request begins, and are not used once the Request ends.

Clearing Cache

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

The most common example when clearing the loader's cache is necessary is after a mutation or update within the same request, when a cached value could be out of date and future loads should not use any possibly cached value.

Here's a simple example using SQL UPDATE to illustrate.

// Request begins...
letuserLoader=DataLoader<Int,Int>(...)
// And a value happens to be loaded (and cached).
tryawait userLoader.load(key:4)
// A mutation occurs, invalidating what might be in cache.
tryawaitsqlRun('UPDATE users WHERE id=4 SET username="zuck"')await userLoader.clear(key:4)
// Later the value load is loaded again so the mutated data appears.
tryawait userLoader.load(key:4)
// Request completes.

Caching Errors

If a batch load fails (that is, a batch function throws or returns a DataLoaderFutureValue.failure(Error)), then the requested values will not be cached. However if a batch function returns an Error instance for an individual value, that Error will be cached to avoid frequently loading the same Error.

In some circumstances you may wish to clear the cache for these individual Errors:

do{tryawait userLoader.load(key:1)}catch{if(/* determine if should clear error */){await userLoader.clear(key:1);
}throw error
}

Disabling Cache

In certain uncommon cases, a DataLoader which does not cache may be desirable. Calling DataLoader(options: DataLoaderOptions(cachingEnabled: false), batchLoadFunction: batchLoadFunction) will ensure that every call to .load() will produce a new Future, and previously requested keys will not be saved in memory.

However, when the memoization cache is disabled, your batch function will receive an array of keys which may contain duplicates! Each key will be associated with each call to .load(). Your batch loader should provide a value for each instance of the requested key.

For example:

letmyLoader=DataLoader<String,String>(
options:DataLoaderOptions(cachingEnabled:false),
batchLoadFunction:{ keys inself.someBatchLoader(keys: keys).map{DataLoaderFutureValue.success($0)}})tryawait myLoader.load(key:"A")tryawait myLoader.load(key:"B")tryawait myLoader.load(key:"A")
// > [ "A", "B", "A" ]

More complex cache behavior can be achieved by calling .clear() or .clearAll() rather than disabling the cache completely. For example, this DataLoader will provide unique keys to a batch function due to the memoization cache being enabled, but will immediately clear its cache when the batch function is called so later requests will load new values.

letmyLoader=DataLoader<String,String>(batchLoadFunction:{ keys inawait identityLoader.clearAll()returnsomeBatchLoad(keys: keys)})

Using with GraphQL 🎀

DataLoader pairs nicely well with GraphQL and Graphiti. GraphQL fields are designed to be stand-alone functions. Without a caching or batching mechanism, it's easy for a naive GraphQL server to issue new database requests each time a field is resolved.

Consider the following GraphQL request:

{
me {
name
bestFriend {
name
}
friends(first: 5) {
name
bestFriend {
name
}
}
}
}

Naively, if me, bestFriend and friends each need to request the backend, there could be at most 12 database requests!

By using DataLoader, we could batch our requests to a User type, and only require at most 4 database requests, and possibly fewer if there are cache hits. Here's a full example using Graphiti:

structUser:Codable{letid:Intletname:StringletbestFriendID:IntletfriendIDs:[Int]func getBestFriend(context:UserContext, arguments:NoArguments)throws->User{returntryawait context.userLoader.load(key: user.bestFriendID)}struct FriendArguments {
first: Int
}func getFriends(context:UserContext, arguments:FriendArguments)throws->[User]{returntryawait context.userLoader.loadMany(keys: user.friendIDs[0..<arguments.first])}}structUserResolver{publicfunc me(context:UserContext, arguments:NoArguments)->User{...}}classUserContext{letdatabase=...letuserLoader=DataLoader<Int,User>(){[weak self] keys inguardlet self =selfelse{throw ContextError }letusers=tryawaitUser.query(on:self.database).filter(\.$id ~~ keys).all()return keys.map{ key in
users.first{ $0.id == key }!
}}}structUserAPI:API{letresolver=UserResolver()letschema=Schema<UserResolver,UserContext>{Type(User.self){Field("name", at: \.content)Field("bestFriend", at: \.getBestFriend, as: TypeReference<User>.self)Field("friends", at: \.getFriends, as:[TypeReference<User>]?.self){Argument("first", at:.\first)}}Query{Field("me", at:UserResolver.hero, as:User.self)}}}

Contributing 🤘

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

When reporting an issue, please add a detailed example, and if possible a code snippet or test to reproduce your problem. 💥

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 💪

This repo uses the standard swift format, and includes lint checks to enforce these formatting standards. To format your code, run:

swift format --parallel --in-place --recursive ./

Acknowledgements 👏

This library is entirely a Swift version of Facebook's DataLoader. Developed by Lee Byron and Nicholas Schrock from Facebook.

About

DataLoader is a generic utility to be used as part of your Swift 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.

Topics

Resources

Stars

38 stars

Watchers

6 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

Repository files navigation

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.

This is a Swift version of the Facebook DataLoader.

Getting started 🚀

Include this repo in your Package.swift file.

.package(url:"https://github.com/GraphQLSwift/DataLoader.git", from:"2.0.0")

The AsyncDataLoader library is preferred. The DataLoader uses NIO for concurrency and is provided for backwards compatibility.

To get started, create a DataLoader. Each DataLoader instance represents a unique cache. Typically instances are created per request when used within a web-server if different users can see different things.

Batching 🍪

Batching is not an advanced feature, it's DataLoader's primary feature. Create a DataLoader by providing a batch loading function:

import AsyncDataLoader
letuserLoader=DataLoader<Int,User>(batchLoadFunction:{ keys intryUser.query(on: req).filter(\User.id ~~ keys).all().map{ users in
keys.map{ key inDataLoaderFutureValue.success(users.filter{ $0.id == key })}}})

The order of the returned DataLoaderFutureValues must match the order of the input keys.

Load individual keys

asyncletresult1= userLoader.load(key:1)asyncletresult2= userLoader.load(key:2)asyncletresult3= userLoader.load(key:1)

The example above will only fetch two users, because the user with key 1 is present twice in the list.

Load multiple keys

There is also a method to load multiple keys at once

tryawait userLoader.loadMany(keys:[1,2,3])

Execution

By default, a DataLoader will wait for a short time from the moment load is called to collect keys prior to running the batchLoadFunction and completing the load results. This allows keys to accumulate and batch into a smaller number of total requests. This amount of time is configurable using the executionPeriod option:

letmyLoader=DataLoader<String,String>(
options:DataLoaderOptions(executionPeriod:.milliseconds(50)),
batchLoadFunction:{ keys inself.someBatchLoader(keys: keys).map{DataLoaderFutureValue.success($0)}})

Longer execution periods reduce the number of total data requests, but also reduce the responsiveness of the load futures.

If desired, you can manually execute the batchLoadFunction and complete the futures at any time, using the .execute() method.

Scheduled execution can be disabled by setting executionPeriod to nil, but be careful - you must call .execute() manually in this case. Otherwise, the futures will never complete!

Disable batching

It is possible to disable batching by setting batchingEnabled to false. In this case, the batchLoadFunction will be invoked immediately when a key is loaded.

Caching 💰

DataLoader provides a memoization cache. After .load() is called with a key, the resulting value is cached for the lifetime of the DataLoader object. This eliminates redundant loads.

In addition to relieving pressure on your data storage, caching results also creates fewer objects which may relieve memory pressure on your application:

letuserLoader=DataLoader<Int,Int>(...)asyncletresult1= userLoader.load(key:1)asyncletresult2= userLoader.load(key:1)awaitprint(result1 == result2) // true

Caching per-Request

DataLoader caching does not replace Redis, Memcache, or any other shared application-level cache. DataLoader is first and foremost a data loading mechanism, and its cache only serves the purpose of not repeatedly loading the same data in the context of a single request to your Application. To do this, it maintains a simple in-memory memoization cache (more accurately: .load() is a memoized function).

Avoid multiple requests from different users using the DataLoader instance, which could result in cached data incorrectly appearing in each request. Typically, DataLoader instances are created when a Request begins, and are not used once the Request ends.

Clearing Cache

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

The most common example when clearing the loader's cache is necessary is after a mutation or update within the same request, when a cached value could be out of date and future loads should not use any possibly cached value.

Here's a simple example using SQL UPDATE to illustrate.

// Request begins...
letuserLoader=DataLoader<Int,Int>(...)
// And a value happens to be loaded (and cached).
tryawait userLoader.load(key:4)
// A mutation occurs, invalidating what might be in cache.
tryawaitsqlRun('UPDATE users WHERE id=4 SET username="zuck"')await userLoader.clear(key:4)
// Later the value load is loaded again so the mutated data appears.
tryawait userLoader.load(key:4)
// Request completes.

Caching Errors

If a batch load fails (that is, a batch function throws or returns a DataLoaderFutureValue.failure(Error)), then the requested values will not be cached. However if a batch function returns an Error instance for an individual value, that Error will be cached to avoid frequently loading the same Error.

In some circumstances you may wish to clear the cache for these individual Errors:

do{tryawait userLoader.load(key:1)}catch{if(/* determine if should clear error */){await userLoader.clear(key:1);
}throw error
}

Disabling Cache

In certain uncommon cases, a DataLoader which does not cache may be desirable. Calling DataLoader(options: DataLoaderOptions(cachingEnabled: false), batchLoadFunction: batchLoadFunction) will ensure that every call to .load() will produce a new Future, and previously requested keys will not be saved in memory.

However, when the memoization cache is disabled, your batch function will receive an array of keys which may contain duplicates! Each key will be associated with each call to .load(). Your batch loader should provide a value for each instance of the requested key.

For example:

letmyLoader=DataLoader<String,String>(
options:DataLoaderOptions(cachingEnabled:false),
batchLoadFunction:{ keys inself.someBatchLoader(keys: keys).map{DataLoaderFutureValue.success($0)}})tryawait myLoader.load(key:"A")tryawait myLoader.load(key:"B")tryawait myLoader.load(key:"A")
// > [ "A", "B", "A" ]

More complex cache behavior can be achieved by calling .clear() or .clearAll() rather than disabling the cache completely. For example, this DataLoader will provide unique keys to a batch function due to the memoization cache being enabled, but will immediately clear its cache when the batch function is called so later requests will load new values.

letmyLoader=DataLoader<String,String>(batchLoadFunction:{ keys inawait identityLoader.clearAll()returnsomeBatchLoad(keys: keys)})

Using with GraphQL 🎀

DataLoader pairs nicely well with GraphQL and Graphiti. GraphQL fields are designed to be stand-alone functions. Without a caching or batching mechanism, it's easy for a naive GraphQL server to issue new database requests each time a field is resolved.

Consider the following GraphQL request:

{
me {
name
bestFriend {
name
}
friends(first: 5) {
name
bestFriend {
name
}
}
}
}

Naively, if me, bestFriend and friends each need to request the backend, there could be at most 12 database requests!

By using DataLoader, we could batch our requests to a User type, and only require at most 4 database requests, and possibly fewer if there are cache hits. Here's a full example using Graphiti:

structUser:Codable{letid:Intletname:StringletbestFriendID:IntletfriendIDs:[Int]func getBestFriend(context:UserContext, arguments:NoArguments)throws->User{returntryawait context.userLoader.load(key: user.bestFriendID)}struct FriendArguments {
first: Int
}func getFriends(context:UserContext, arguments:FriendArguments)throws->[User]{returntryawait context.userLoader.loadMany(keys: user.friendIDs[0..<arguments.first])}}structUserResolver{publicfunc me(context:UserContext, arguments:NoArguments)->User{...}}classUserContext{letdatabase=...letuserLoader=DataLoader<Int,User>(){[weak self] keys inguardlet self =selfelse{throw ContextError }letusers=tryawaitUser.query(on:self.database).filter(\.$id ~~ keys).all()return keys.map{ key in
users.first{ $0.id == key }!
}}}structUserAPI:API{letresolver=UserResolver()letschema=Schema<UserResolver,UserContext>{Type(User.self){Field("name", at: \.content)Field("bestFriend", at: \.getBestFriend, as: TypeReference<User>.self)Field("friends", at: \.getFriends, as:[TypeReference<User>]?.self){Argument("first", at:.\first)}}Query{Field("me", at:UserResolver.hero, as:User.self)}}}

Contributing 🤘

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

When reporting an issue, please add a detailed example, and if possible a code snippet or test to reproduce your problem. 💥

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 💪

This repo uses the standard swift format, and includes lint checks to enforce these formatting standards. To format your code, run:

swift format --parallel --in-place --recursive ./

Acknowledgements 👏

This library is entirely a Swift version of Facebook's DataLoader. Developed by Lee Byron and Nicholas Schrock from Facebook.

About

DataLoader is a generic utility to be used as part of your Swift 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.

Topics

Resources

Stars

38 stars

Watchers

6 watching

Forks

Releases

Packages

Used by

Contributors

Languages