shasoft/batch

批处理函数调用

维护者

详细信息

github.com/shasoft/batch

来源

v1.0.1 2023-11-09 18:05 UTC

This package is auto-updated.

Last update: 2024-09-09 20:17:01 UTC


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;
}

优先级

在我们的情况下,组将按顺序执行。即在任何时候,列表中只有一个组。在更复杂的情况下,可能有多个组。在这种情况下,执行顺序很重要。以图片中的示例为例。假设我们队列中同时有两个组: getArticlegetUser。根据组的执行顺序,我们将执行不同的 SQL 查询数量。按照 getArticle, getUser 的顺序,我们将执行 2 个查询: getArticlegetUser。按照 getUser, getArticle, getUser 的顺序,我们将执行 3 个查询: getUsergetArticlegetUser

在这种情况下,执行 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,或者指定该对象返回的闭包。存在两种缓存:

  1. 用于保存类似Get(保存Get函数的结果值)的函数
  2. 用于保存类似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对象,允许设置以下工作模式

  1. BatchPromise::MODE_CACHE_OFF - 关闭缓存。即无论函数类型如何,它都将像普通函数一样被调用,并且其工作结果将不会保存到缓存中。
  2. BatchPromise::MODE_CACHE_DIRECT - 关闭缓存,但保存更改到缓存。即无论函数类型如何,它都将像普通函数一样被调用,并且其工作结果将被保存到缓存中。
  3. BatchPromise::MODE_CACHE_ON - 打开缓存(默认值)。即函数在缓存模式下工作 - 读取和保存值到缓存。