shasoft / batch
Requires
Requires (Dev)
README
基本功能
分组调用最明显的应用是解决 N+1 查询问题。当数据访问框架执行 N 个额外的 SQL 查询以获取可以通过一个查询获取的数据时,就会出现此问题。例如,对于获取数据,有调用以下函数的调用,每个函数都执行一个 SQL 查询。使用此包时,6 个函数调用被分组为两个组,每组按函数调用类型分组。每组调用包含所有调用参数。
结果将是执行两个 SQL 查询而不是六个。
实现此类算法的代码如下
// Функция для получения асинхронных данных и их возврата в синхронный код // Анонимная функция должно возвращать обещание, // результат которого и будет возвращен из функции run $articles = BatchManager::run(function (): BatchPromise { // Функция all выполняет массив обещаний // и возвращает массив значений этих обещаний return BatchManager::all([ getArticle(1), getArticle(2), getArticle(3) ]); }); // Получить информацию о статье function getArticle(int $id): BatchPromise { // Создаем и возвращаем обещание return BatchManager::create( // Функция для обработки сгруппированных данных function (BatchGroup $group) { // Получить список уникальных значений аргумента с номером 0 $ids = $group->arg(0); // Выбрать из БД информацию о статьях $articles = sql('SELECT * FROM `articles` WHERE `id` IN (' . implode(',', $ids) . ')'); // Создадим обещания получения информации о пользователях $promises = []; foreach ($articles as $article) { $promises[] = getUser($article['author_id']); } BatchManager::all($promises)->then(function (array $users) use ($articles, $group) { // Сгруппировать пользователей по идентификатору $mUsers = []; foreach ($users as $user) { $mUsers[$user['id']] = $user; } // Проставить информацию об авторе foreach ($articles as $article) { $article['author'] = $mUsers[$article['author_id']]; } // Сгруппировать статьи по идентификатору $mArticles = []; foreach ($articles as $article) { $mArticles[$article['id']] = $article; } // Вернуть информацию о статьях $group->setResult(function (int $id) use ($mArticles) { return $mArticles[$id] ?? null; }); }); }, $id // Функция имеет только один аргумент (его номер = 0) ); } // Получить информацию о пользователе function getUser(int $id): BatchPromise { // Создаем и возвращаем обещание return BatchManager::create( // Функция для обработки сгруппированных данных function (BatchGroup $group) { // Получить список уникальных значений аргумента с номером 0 $ids = $group->arg(0); // Выбрать из БД информацию о пользователях $users = sql('SELECT * FROM `users` WHERE `id` IN (' . implode(',', $ids) . ')'); // Сгруппировать пользователей по идентификатору $mUsers = []; foreach ($users as $user) { $mUsers[$user['id']] = $user; } // Вернуть информацию о пользователе $group->setResult(function (int $id) use ($mUsers) { return $mUsers[$id] ?? null; }); }, $id // Функция имеет только один аргумент (его номер = 0) ); }
BatchManager::all
函数接收来自分组函数的 BatchPromise 列表作为输入,并在传递的数组中的所有承诺都执行时执行。
调用链
在这种情况下,所有承诺都支持调用链。
$articles = BatchManager::run(function (): BatchPromise { return getArticle(1) ->then(function($article) { // Функция получает значение, его можно изменить $article['genderName'] = $article['gender']=='M' ? 'мужчина' : 'женщина'; // и передать дальше по цепочке return $article; }) ->then(function($article) { // Функция получает значение, его можно изменить if( empty($article['avatar']) { $article['avatar'] = '/avatar/default.png'; } // и передать дальше по цепочке return $article; }); });
错误处理
每个函数(承诺)都可以生成错误。与同一 JavaScript 中的承诺不同,我在我的包中采取了不同的方法。而不是指定一个单独的函数来获取错误,您需要使用调用 BatchError:create()
来返回该错误。以下是一个重写 getUser
函数的示例。
// Получить информацию о пользователе function getUser(int $id): BatchPromise { // Создаем и возвращаем обещание return BatchManager::create( // Функция для обработки сгруппированных данных function (BatchGroup $group) { // Получить список уникальных значений аргументов с номером 0 $ids = $group->arg(0); // Выбрать из БД информацию о пользователях $users = sql('SELECT * FROM `users` WHERE `id` IN (' . implode(',', $ids) . ')'); // Сгруппировать пользователей по идентификатору $mUsers = []; foreach ($users as $user) { $mUsers[$user['id']] = $user; } // Вернуть информацию о пользователе $group->setResult(function (int $id) use ($mUsers) { // Возвращаем ошибку если по идентификатору ничего не выбралось return $mUsers[$id] ?? BatchError::create("Пользователь {$id} не найден",$id); }); }, $id ); }
创建错误时,必须指定文本(可选)和错误代码。错误处理类还包含静态方法,便于处理错误。
// Работа с ошибками class BatchError { // Создать ошибку static public function create(string $message, ?int $code = null): static; // Создать ошибку отсутствия значения static public function createUndefined(): static; // Значение является ошибкой? static public function has(mixed $value): bool; // Все значения массив являются ошибками? static public function hasErrors(array $values): bool; // Отфильтровать массив значений удалив: true - ошибки/ false - не ошибки static public function filter(array $values, bool $removeError = true): array; // Заполнить ошибки значениями static public function fill(array $values, mixed $value): array; }
优先级
在我们的情况下,组将按顺序执行。即在任何时候,列表中只有一个组。在更复杂的情况下,可能有多个组。在这种情况下,执行顺序很重要。以图片中的示例为例。假设我们队列中同时有两个组: getArticle 和 getUser。根据组的执行顺序,我们将执行不同的 SQL 查询数量。按照 getArticle, getUser
的顺序,我们将执行 2 个查询: getArticle,getUser。按照 getUser, getArticle, getUser
的顺序,我们将执行 3 个查询: getUser,getArticle,getUser。
在这种情况下,执行 getArticle 组后,将向 getUser 组添加调用,然后执行该组。在第二种情况下,执行 getUser 组,然后在执行 getArticle 组后创建另一个 getUser 组。功能中提供了一种方法,可以通过扩展承诺创建来设置优先级。
// Получить информацию о пользователе function getUser(int $id): BatchPromise { // Создаем и возвращаем обещание return BatchManager::createEx( // Функция указания расширенных настроек function (BatchGroupConfig $config) { // Указываем пониженный приоритет (чтобы эта группа выполнялась последней) $config->setPriority(BatchManager::PRIORITY_LOW); }, // Функция для обработки сгруппированных данных function (BatchGroup $group) { // ... }, $id ); }
现在,无论组在执行列表中的顺序如何,具有较低优先级的组都会最后执行。为了更有效地工作,必须为不创建其他组的组指定较低优先级,以便它们最后执行。
缓存
使用调用分组功能的另一个额外有用功能是缓存结果。为此,所有函数都分为几种类型
无缓存的函数。
这是默认函数。函数值不缓存
LifeTime 类型的函数 - 时间缓存。
最简单的缓存方式。
class ExampleCacheLifeTime { // Функция вида LifeTime - кэширование на время static public function fnLifeTime(int $x): BatchPromise { return BatchManager::createEx(function (BatchGroupConfig $groupConfig) { // Установить тип функции = LifeTime, установить время жизни = 5 минут $groupConfig->setCacheLifetime(5 * 60); }, function (BatchGroup $group) { // Функция получения результата для каждого набора аргументов $group->setResult(function (int $minValue) { // Вернуть случайное число от $minValue до 1000 // и кэшировать это значение на 5 минут return random_int(min($minValue, 1000), 1000); }); }, $x); } }
Get/Put 类型的函数 - 值变化时缓存。
Get 类型函数在扩展调用参数中设置,必须指定依赖于该函数的 Put 类型函数。如果没有依赖项,则缓存永久执行。如果有依赖项,则缓存值直到当前值更改。
// Пример функции Get и Put class ExampleCacheGetPut { // Хранение значений static protected array $data = []; // Функция вида Get static public function fnGet(int $x): BatchPromise { return BatchManager::createEx(function (BatchGroupConfig $groupConfig) { // Установить тип функции = Get $groupConfig->setCacheGet(); }, function (BatchGroup $group) { // Функция получения результата для каждого набора аргументов $group->setResult(function (int $x) { // Читать текущее значение $ret = self::$data[$x] ?? 0; // Установить зависимость от функции вида Put self::fnPut($x, $ret); // Вернуть результат return $ret; }); }, $x); } // Функция вида Put public function fnPut(int $x, int $value): BatchPromise { return BatchManager::createEx(function (BatchGroupConfig $groupConfig) { // Установить тип функции = Put // Указать список индексов ключевых аргументов $groupConfig->setCachePut(0); }, function (BatchGroup $group) { // Функция получения результата для каждого набора аргументов $group->setResult(function (int $x, int $value): void { // Установить значение self::$data[$x] = $value; }); }, $x, $value); } }
为了执行缓存,必须设置缓存对象。
// Установить глобальные интерфейсы КЭШа BatchConfig::setICache(CacheItemPoolInterface|callable|null $cacheGet, CacheItemPoolInterface|callable|null $cachePut = null): void;
可以作为参数指定缓存接口对象CacheItemPoolInterface,或者指定该对象返回的闭包。存在两种缓存:
- 用于保存类似Get(保存Get函数的结果值)的函数
- 用于保存类似Put(保存Put函数依赖的值)的函数。如果只指定$cacheGet参数,则自动将$cachePut参数设置为相同的值。
Get/Put类型的缓存是如何工作的?
步骤说明 (1) Put和Get缓存为空 (2) 调用函数
fnPut(2,7)
,结果在Put缓存中保存值为7
,键为fnPut,2
。 (3) 调用函数fnGet(2)
。函数检查Get缓存中的值,未找到,调用参数为2的fnGet函数,得到结果14,并将结果保存到缓存中+依赖的值。 (3а) 再次调用函数fnGet(2)
。函数检查Get缓存中键为fnGet,2
的值,找到并得到值14。同时读取依赖列表并检查所有依赖是否与Put缓存中的值相对应。在本例中,它们相对应,因此返回值14。即值从缓存中获取,不调用群组函数。 (4) 调用函数fnPut(2,9)
,结果在Put缓存中保存值为9
,键为fnPut,2
。 (5) 调用函数fnGet(2)
。函数检查Get缓存中键为fnGet,2
的值,找到并得到值14。同时读取依赖列表并检查所有依赖是否与Put缓存中的值相对应。在本例中,它们不相对应(7!=9),因此调用群组函数返回值18。然后保存Get缓存中得到的值+依赖的值。
缓存的工作模式
具有缓存的群组函数很好,但有时需要函数忽略缓存中的值并直接通过群组函数读取值。这样的功能是存在的。每个函数都返回一个包含方法setCacheMode
的BatchPromise对象,允许设置以下工作模式
- BatchPromise::MODE_CACHE_OFF - 关闭缓存。即无论函数类型如何,它都将像普通函数一样被调用,并且其工作结果将不会保存到缓存中。
- BatchPromise::MODE_CACHE_DIRECT - 关闭缓存,但保存更改到缓存。即无论函数类型如何,它都将像普通函数一样被调用,并且其工作结果将被保存到缓存中。
- BatchPromise::MODE_CACHE_ON - 打开缓存(默认值)。即函数在缓存模式下工作 - 读取和保存值到缓存。