overblog / dataloader-php
DataLoaderPhp是一个通用工具,可以作为应用程序数据获取层的一部分使用,通过批处理和缓存,在各种远程数据源(如数据库或Web服务)上提供简化和一致的API。
Requires
- php: ^8.1
Requires (Dev)
- guzzlehttp/promises: ^1.5.0 || ^2.0.0
- phpunit/phpunit: ^10.3
- react/promise: ^2.8.0
- webonyx/graphql-php: ^15.0
Suggests
- guzzlehttp/promises: To use with Guzzle promise
- react/promise: To use with ReactPhp promise
- webonyx/graphql-php: To use with Webonyx GraphQL native promise
Replaces
- overblog/promise-adapter: v1.0.0
README
DataLoaderPHP是一个通用工具,可以作为应用程序数据获取层的一部分使用,通过批处理和缓存,在各种远程数据源(如数据库或Web服务)上提供简化和一致的API。
要求
此库需要PHP >= 7.3才能运行。
入门
首先,使用composer安装DataLoaderPHP。
composer require "overblog/dataloader-php"
要开始使用,创建一个DataLoader
对象。
批处理
批处理不是高级功能,它是DataLoader的主要功能。通过提供批处理加载函数来创建加载器。
use Overblog\DataLoader\DataLoader; $myBatchGetUsers = function ($keys) { /* ... */ }; $promiseAdapter = new MyPromiseAdapter(); $userLoader = new DataLoader($myBatchGetUsers, $promiseAdapter);
批处理加载调用/回调接受一个键的数组,并返回一个解析为值的数组的Promise。
然后从加载器中加载单个值。DataLoaderPHP将在单个执行帧内合并所有单个加载(使用await
方法),然后调用您的批处理函数并带有所有请求的键。
$userLoader->load(1) ->then(function ($user) use ($userLoader) { return $userLoader->load($user->invitedByID); }) ->then(function ($invitedBy) { echo "User 1 was invited by $invitedBy"; }); // Elsewhere in your application $userLoader->load(2) ->then(function ($user) use ($userLoader) { return $userLoader->load($user->invitedByID); }) ->then(function ($invitedBy) { echo "User 2 was invited by $invitedBy"; }); // Synchronously waits on the promise to complete, if not using EventLoop. $userLoader->await(); // or `DataLoader::await()`
一个简单的应用程序可能需要四次往返到后端以获取所需的信息,但使用DataLoaderPHP,此应用程序最多只进行两次。
DataLoaderPHP允许您在不牺牲批数据加载性能的情况下解耦应用程序的相关部分。虽然加载器提供了一个加载单个值的API,但所有并发请求都将合并并呈现在您的批处理加载函数中。这允许您的应用程序在应用程序中安全地分配数据获取需求,并保持最小外发数据请求。
批处理函数
批处理加载函数接受一个键的数组,并返回一个解析为值的数组的Promise。必须遵守一些约束条件
- 值的数组必须与键的数组长度相同。
- 值数组的每个索引必须与键数组的相同索引相对应。
例如,如果您的批处理函数提供了一个键的数组:[ 2, 9, 6, 1 ]
,并且从后端服务加载返回了值
[ ['id' => 9, 'name' => 'Chicago'], ['id' => 1, 'name' => 'New York'], ['id' => 2, 'name' => 'San Francisco'] ]
我们的后端服务返回的结果顺序与请求的顺序不同,这可能是由于它这样做更有效率。此外,它省略了键6
的结果,我们可以将其解释为不存在该键的值。
为了遵守批处理函数的约束,它必须返回一个与键的数组长度相同的值的数组,并重新排序以确保每个索引与原始键[ 2, 9, 6, 1 ]
对齐
[ ['id' => 2, 'name' => 'San Francisco'], ['id' => 9, 'name' => 'Chicago'], null, ['id' => 1, 'name' => 'New York'] ]
缓存(当前PHP实例)
DataLoader为您的应用程序的单个请求中发生的所有加载提供了memoization缓存。在调用->load()
一次后,给定的键的结果被缓存以消除冗余加载。
除了减轻数据存储的压力外,按请求缓存结果还会创建更少的对象,这可能会减轻应用程序的内存压力。
$userLoader = new DataLoader(...); $promise1A = $userLoader->load(1); $promise1B = $userLoader->load(1); var_dump($promise1A === $promise1B); // bool(true)
清除缓存
在某些不常见的情况下,清除请求缓存可能是必要的。
清除加载器缓存的常见示例是在同一请求中的突变或更新之后,当缓存值可能已过时并且未来的加载不应使用任何可能的缓存值时。
以下是一个简单的示例,说明使用SQL UPDATE进行缓存清除。
use Overblog\DataLoader\DataLoader; // Request begins... $userLoader = new DataLoader(...); // And a value happens to be loaded (and cached). $userLoader->load(4)->then(...); // A mutation occurs, invalidating what might be in cache. $sql = 'UPDATE users WHERE id=4 SET username="zuck"'; if (true === $conn->query($sql)) { $userLoader->clear(4); } // Later the value load is loaded again so the mutated data appears. $userLoader->load(4)->then(...); // Request completes.
缓存错误
如果批量加载失败(即,批量函数抛出或返回一个被拒绝的Promise),则请求的值将不会被缓存。然而,如果批量函数为单个值返回一个Error
实例,则该Error
将被缓存以避免频繁加载相同的Error
。
在某些情况下,您可能希望清除这些单个错误的缓存
$userLoader->load(1)->then(null, function ($exception) { if (/* determine if error is transient */) { $userLoader->clear(1); } throw $exception; });
禁用缓存
在某些不常见的情况下,可能需要使用不缓存的数据加载器。通过调用new DataLoader(myBatchFn, new Option(['cache' => false ]))
可以确保每次调用->load()
都会产生一个新的Promise,且请求的键将不会保存在内存中。
然而,当禁用记忆缓存时,您的批量函数将接收一个可能包含重复项的键数组!每个键将与每个对->load()
的调用相关联。您的批量加载器应为请求键的每个实例提供一个值。
例如
$myLoader = new DataLoader(function ($keys) { echo json_encode($keys); return someBatchLoadFn($keys); }, $promiseAdapter, new Option(['cache' => false ])); $myLoader->load('A'); $myLoader->load('B'); $myLoader->load('A'); // [ 'A', 'B', 'A' ]
通过调用->clear()
或->clearAll()
而不是完全禁用缓存,可以实现更复杂的缓存行为。例如,由于启用了记忆缓存,此数据加载器将为批量函数提供唯一的键,但在调用批量函数时立即清除其缓存,因此后续请求将加载新的值。
$myLoader = new DataLoader(function($keys) use ($identityLoader) { $identityLoader->clearAll(); return someBatchLoadFn($keys); }, $promiseAdapter);
API
class DataLoader
DataLoaderPHP通过提供批量加载函数创建了一个用于从特定数据后端(如SQL表的id列或MongoDB数据库中的文档名称)加载数据的公共API。
每个DataLoaderPHP
实例包含一个唯一的记忆缓存。在长时间运行的应用程序中使用时请谨慎,或者为具有不同访问权限的许多用户提供服务时,请考虑为每个Web请求创建一个新的实例。
new DataLoader(callable $batchLoadFn, PromiseAdapterInterface $promiseAdapter [, Option $options])
给定批量加载实例和选项,创建一个新的DataLoaderPHP
。
-
$batchLoadFn:一个可调用的回调,它接受一个键数组,并返回一个解析为数组值的Promise。
-
$promiseAdapter:任何实现
Overblog\PromiseAdapter\PromiseAdapterInterface
的对象。(见Overblog/Promise-Adapter) -
$options:一个可选的选项对象
-
batch:默认
true
。将此设置为false
以禁用批处理,而是立即用单个加载键调用batchLoadFn
。 -
maxBatchSize:默认
Infinity
。限制传递给batchLoadFn
的项目数。 -
cache:默认
true
。将此设置为false
以禁用缓存,而是在每次加载时在batchLoadFn
中创建一个新的Promise和新的键。 -
cacheKeyFn:一个用于生成给定加载键的缓存键的函数。默认为
key
。在对象是键且两个形状类似的对象应被视为等效时很有用。 -
cacheMap:用于此加载器的基础缓存的
CacheMap
实例。默认new CacheMap()
。
-
load($key)
加载一个键,返回一个表示该键的值的Promise。
- $key:要加载的键值。
loadMany($keys)
加载多个键,承诺一个值数组
list($a, $b) = DataLoader::await($myLoader->loadMany(['a', 'b']));
这等价于更冗长的
list($a, $b) = DataLoader::await(\React\Promise\all([ $myLoader->load('a'), $myLoader->load('b') ]));
- $keys:要加载的键值数组。
clear($key)
如果存在,从缓存中清除$key
处的值。返回自身以进行方法链。
- $key:要清除的键值。
clearAll()
清除整个缓存。在某个事件导致此特定DataLoaderPHP
中出现未知无效化时使用。返回自身以进行方法链。
prime($key, $value)
使用提供的键和值初始化缓存。如果键已存在,则不进行更改。(要强制初始化缓存,请先使用$loader->clear($key)->prime($key, $value)
清除键。返回自身以进行方法链。
static await([$promise][, $unwrap])
您可以使用 DataLoaderPHP 的 await 方法同步强制承诺完成。当调用 await 函数时,它应向承诺提供值或拒绝承诺。await 方法处理所有 DataLoaderPHP 实例中所有等待的承诺。
-
$promise:可选的承诺完成。
-
$unwrap:控制是否返回已完成的承诺的值,或者如果承诺被拒绝则抛出异常。默认值
true
。
与 Webonyx/GraphQL 一起使用
DataLoader 与 Webonyx/GraphQL 结合得非常好。GraphQL 字段设计为独立函数。如果没有缓存或批量处理机制,那么一个简单的 GraphQL 服务器每次解析字段时都可能会发起新的数据库请求。
考虑以下 GraphQL 请求
{ me { name bestFriend { name } friends(first: 5) { name bestFriend { name } } } }
简单地,如果 me
、bestFriend
和 friends
每个都需要请求后端,最多可能有 13 个数据库请求!
当使用 DataLoader 时,我们最多可以定义 User
类型 4 个数据库请求,如果缓存命中,可能更少。
<?php use GraphQL\GraphQL; use GraphQL\Type\Definition\ObjectType; use GraphQL\Type\Definition\Type; use Overblog\DataLoader\DataLoader; use Overblog\DataLoader\Promise\Adapter\Webonyx\GraphQL\SyncPromiseAdapter; use Overblog\PromiseAdapter\Adapter\WebonyxGraphQLSyncPromiseAdapter; /** * @var \PDO $dbh */ // ... $graphQLPromiseAdapter = new SyncPromiseAdapter(); $dataLoaderPromiseAdapter = new WebonyxGraphQLSyncPromiseAdapter($graphQLPromiseAdapter); $userLoader = new DataLoader(function ($keys) { /*...*/ }, $dataLoaderPromiseAdapter); GraphQL::setPromiseAdapter($graphQLPromiseAdapter); $userType = new ObjectType([ 'name' => 'User', 'fields' => function () use (&$userType, $userLoader, $dbh) { return [ 'name' => ['type' => Type::string()], 'bestFriend' => [ 'type' => $userType, 'resolve' => function ($user) use ($userLoader) { $userLoader->load($user['bestFriendID']); } ], 'friends' => [ 'args' => [ 'first' => ['type' => Type::int() ], ], 'type' => Type::listOf($userType), 'resolve' => function ($user, $args) use ($userLoader, $dbh) { $sth = $dbh->prepare('SELECT toID FROM friends WHERE fromID=:userID LIMIT :first'); $sth->bindParam(':userID', $user['id'], PDO::PARAM_INT); $sth->bindParam(':first', $args['first'], PDO::PARAM_INT); $friendIDs = $sth->execute(); return $userLoader->loadMany($friendIDs); } ] ]; } ]);
您还可以查看 示例。
与 Symfony 一起使用
请参阅 扩展包。
致谢
Overblog/DataLoaderPHP 是由 Facebook 开发的 dataLoader NodeJS 版本 的移植。
此外,文档的大部分内容已从 dataLoader NodeJS 版本移植过来 文档。
许可证
Overblog/DataLoaderPHP 在 MIT 许可证下发布。