DataLoader

DataLoader 是一个通用的工具,可以作为应用程序数据获取层的一部分使用,通过批量处理和缓存,为各种远程数据源(例如数据库或 Web 服务)提供简化且一致的 API。

这是 Facebook DataLoader 的 Swift 版本。

Swift License

开始使用 🚀

在您的 Package.swift 文件中包含此仓库。

.Package(url: "https://github.com/GraphQLSwift/DataLoader.git", .upToNextMajor(from: "1.1.0"))

要开始使用,请创建一个 DataLoader。 每个 DataLoader 实例代表一个唯一的缓存。 通常,如果不同的用户可以看到不同的内容,则在 Web 服务器中使用时,每个请求都会创建实例。

批量处理 🍪

批量处理不是一个高级特性,它是 DataLoader 的主要特性。

通过提供批量加载函数来创建 DataLoader

let userLoader = Dataloader<Int, User>(batchLoadFunction: { keys in
  try User.query(on: req).filter(\User.id ~~ keys).all().map { users in
    keys.map { key in
      DataLoaderFutureValue.success(users.filter{ $0.id == key })
    }
  }
})

返回的 DataLoaderFutureValues 的顺序必须与键的顺序匹配。

加载单个键

let future1 = try userLoader.load(key: 1, on: eventLoopGroup)
let future2 = try userLoader.load(key: 2, on: eventLoopGroup)
let future3 = try userLoader.load(key: 1, on: eventLoopGroup)

上面的示例只会获取两个用户,因为键为 1 的用户在列表中出现了两次。

加载多个键

还有一个一次加载多个键的方法

try userLoader.loadMany(keys: [1, 2, 3], on: eventLoopGroup)

执行

默认情况下,DataLoader 会从调用 load 方法开始等待一小段时间,收集键,然后再运行 batchLoadFunction 并完成 load futures。 这是为了让键积累并批量处理成更少数量的总请求。 可以使用 executionPeriod 选项配置此时间量。

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

较长的执行周期会减少数据请求的总数,但也会降低 load futures 的响应速度。

如果需要,您可以随时使用 .execute() 方法手动执行 batchLoadFunction 并完成 futures。

可以通过将 executionPeriod 设置为 nil 来禁用计划的执行,但请小心 - 在这种情况下,您必须手动调用 .execute()。 否则,futures 将永远不会完成!

禁用批量处理

可以通过将 batchingEnabled 选项设置为 false 来禁用批量处理。 在这种情况下,当加载键时,将立即调用 batchLoadFunction

缓存 💰

DataLoader 提供了一个记忆化缓存。 在使用键调用 .load() 后,结果值将在 DataLoader 对象的生命周期内被缓存。 这消除了冗余加载。

除了减轻数据存储的压力外,缓存结果还可以创建更少的对象,从而减轻应用程序的内存压力

let userLoader = DataLoader<Int, Int>(...)
let future1 = userLoader.load(key: 1, on: eventLoopGroup)
let future2 = userLoader.load(key: 1, on: eventLoopGroup)
print(future1 == future2) // true

每个请求的缓存

DataLoader 缓存替代 Redis、Memcache 或任何其他共享的应用程序级缓存。 DataLoader 首先是一个数据加载机制,它的缓存仅用于避免在对应用程序的单个请求的上下文中重复加载相同的数据。 为此,它维护一个简单的内存记忆化缓存(更准确地说:.load() 是一个记忆化的函数)。

避免多个来自不同用户的请求使用同一个 DataLoader 实例,这可能会导致缓存的数据不正确地出现在每个请求中。 通常,DataLoader 实例在请求开始时创建,并在请求结束后不再使用。

清除缓存

在某些不常见的情况下,可能需要清除请求缓存。

需要清除加载器的缓存的最常见示例是在同一请求中的 mutation 或更新之后,此时缓存的值可能已过期,并且将来的加载不应使用任何可能缓存的值。

这是一个使用 SQL UPDATE 的简单示例来说明。

// Request begins...
let userLoader = DataLoader<Int, Int>(...)

// And a value happens to be loaded (and cached).
userLoader.load(key: 4, on: eventLoopGroup)

// A mutation occurs, invalidating what might be in cache.
sqlRun('UPDATE users WHERE id=4 SET username="zuck"').whenComplete { userLoader.clear(key: 4) }

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

// Request completes.

缓存错误

如果批量加载失败(即,批量函数抛出或返回 DataLoaderFutureValue.failure(Error)),则不会缓存请求的值。 但是,如果批量函数为单个值返回一个 Error 实例,则将缓存该 Error 以避免频繁加载相同的 Error

在某些情况下,您可能希望清除这些单个错误的缓存

userLoader.load(key: 1, on: eventLoopGroup).whenFailure { error in 
    if (/* determine if should clear error */) {
        userLoader.clear(key: 1);
    }
    throw error
}

禁用缓存

在某些不常见的情况下,可能需要一个缓存的 DataLoader。 调用 DataLoader(options: DataLoaderOptions(cachingEnabled: false), batchLoadFunction: batchLoadFunction) 将确保每次调用 .load() 都会产生一个新的 Future,并且以前请求的键不会保存在内存中。

但是,当禁用记忆化缓存时,您的批量函数将收到一个可能包含重复项的键数组! 每个键将与每次调用 .load() 相关联。 您的批量加载器应为请求的键的每个实例提供一个值。

例如

let myLoader =  DataLoader<String, String>(
    options: DataLoaderOptions(cachingEnabled: false),
    batchLoadFunction: { keys in 
        self.someBatchLoader(keys: keys).map { DataLoaderFutureValue.success($0) }
    }
)

myLoader.load(key: "A", on: eventLoopGroup)
myLoader.load(key: "B", on: eventLoopGroup)
myLoader.load(key: "A", on: eventLoopGroup)

// > [ "A", "B", "A" ]

通过调用 .clear().clearAll() 而不是完全禁用缓存,可以实现更复杂的缓存行为。 例如,由于启用了记忆化缓存,此 DataLoader 将为批量函数提供唯一的键,但在调用批量函数时将立即清除其缓存,因此以后的请求将加载新值。

let myLoader = DataLoader<String, String>(batchLoadFunction: { keys in
    identityLoader.clearAll()
    return someBatchLoad(keys: keys)
})

与 GraphQL 一起使用 🎀

DataLoader 与 GraphQLGraphiti 配合得很好。 GraphQL 字段被设计为独立的函数。 如果没有缓存或批量处理机制,一个简单的 GraphQL 服务器很容易在每次解析字段时发出新的数据库请求。

考虑以下 GraphQL 请求

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

简单来说,如果 mebestFriendfriends 都需要请求后端,则最多可能有 12 个数据库请求!

通过使用 DataLoader,我们可以将对 User 类型的请求进行批处理,并且最多只需要 4 个数据库请求,如果存在缓存命中,则可能更少。 这是一个使用 Graphiti 的完整示例

struct User : Codable {
    let id: Int
    let name: String
    let bestFriendID: Int
    let friendIDs: [Int]
    
    func getBestFriend(context: UserContext, arguments: NoArguments, group: EventLoopGroup) throws -> EventLoopFuture<User> {
        return try context.userLoader.load(key: user.bestFriendID, on: group)
    }
    
    struct FriendArguments {
        first: Int
    }
    func getFriends(context: UserContext, arguments: FriendArguments, group: EventLoopGroup) throws -> EventLoopFuture<[User]> {
        return try context.userLoader.loadMany(keys: user.friendIDs[0..<arguments.first], on: group)
    }
}

struct UserResolver {
    public func me(context: UserContext, arguments: NoArguments) -> User {
        ...
    }
}

class UserContext {
    let database = ...
    let userLoader = DataLoader<Int, User>() { [weak self] keys in
        guard let self = self else { throw ContextError }
        return User.query(on: self.database).filter(\.$id ~~ keys).all().map { users in
            keys.map { key in
                users.first { $0.id == key }!
            }
        }
    }
}

struct UserAPI : API {
    let resolver = UserResolver()
    let schema = 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)
        }
    }
}

贡献 🤘

非常欢迎您提供反馈和帮助来改进这个项目。 请为您发现的错误、想法和增强请求创建 issues,或者更好的是,直接创建 PR 来做出贡献。 😎

报告 issue 时,请添加详细的示例,如果可能,添加代码片段或测试以重现您的问题。 💥

创建 pull request 时,请尽可能遵守当前的编码风格,并使用您的代码创建测试,以便它继续提供出色的测试覆盖率水平 💪

此仓库使用 SwiftFormat,并包含 lint 检查以强制执行这些格式标准。 要格式化您的代码,请安装 swiftformat 并运行

swiftformat .

致谢 👏

该库完全是 Facebook DataLoader 的 Swift 版本。 由来自 FacebookLee ByronNicholas Schrock 开发。