overblog/dataloader-php

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

v1.0.0 2023-08-25 11:11 UTC

README

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

GitHub Actions Code Coverage Latest Stable Version

要求

此库需要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
      }
    }
  }
}

简单地,如果 mebestFriendfriends 每个都需要请求后端,最多可能有 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 许可证下发布。