Как стать автором
Обновить

Комментарии 11

Благодарю. Нашёл несколько реализаций. Как раз то что нужно

https://github.com/overblog/dataloader-php

https://github.2u2.cc/lordthorzonus/php-dataloader

Как я понял при беглом просмотре, там загрузка идет только по одному ключевому полю.

На что только не идут люди, лишь бы не дать базе данных сделать её основную работу, для которой она была написана.

Вы уверены, что всё вышеописанное будет оптимальнее банального джойна внутри БД ?

Конечно join будут оптимальнее. Но его придется писать руками для каждого случая. Ленивую загрузку в ORM не просто так придумали.

Вы хотели сказать активную или жадную (eager) загрузку. А точнее, в данном случае речь идет не о конкретной реализации загрузки связанных сущностей, а о самой идее их получения автоматом. То есть ответ получается короче, "ORM не просто так придумали".

join будет оптимальнее, если связь 1 к 1, а если на одну строку приходится 100 связанных, то придется по сети гонять дублированные данные, и тут же их отбросывать в приложении.

А здесь не понял. Что мешает отбрасывать их на уровне запроса? Заодно и план запроса улучшится, возможно, получится задействовать индексы и т.д.

Предположим, что я хочу достать всех пользователей, и по каждому список профилей, которые он редактировал. Если я сделаю join в лоб, то данные из таблицы пользователей продублируются на каждый профиль.

У меня два варианта — агрегировать профили в запросе в какой-нибудь json (но это уже сложнее "банального джойна"), или тащить результат как есть в приложение и убирать дубликаты там.

Данная схема будет работать только в случае если клиент готов сначала порционно сообщить всё что он хочет получить из БД - а потом подучить результат (как один пакет для единого получателя, так и в виде набора пакетов, с асинхронными подписчиками).

Иначе - очень сложно понять - а сколько же будет послано мелких запросов, и с какого момента надо начинать их группировать в общее ожидание. Да и само ожидание может быть очень не эффективным, особенно когда такие мелкие запросы связаны с интерактивной работой (что чаще всего и бывает). Тут тогда в реализации нужно сначала начинать асинхронное параллельное выполнение 1-2-х первых запросов, а уже следом ставить остальные однотипные запросы в очередь, и накапливать их по времени пока не выполнятся первые запросы (можно подождать ещё столько же - если накопление идёт раза в 2 быстрее, чем исполнение) и только затем запускать следующую порцию.

Но ни первой ни второй логики я в приведённой реализации не увидел.

И это не говоря ещё о кешировании - ведь такие запросы обычно часто ещё и повторяются и эффективно кешировать их результаты.

И тут проблема - если полагаться на СУБД - то при группировке запросов они для СУБД будут единым запросом - и именно его результат будет кэширован средствами СУБД - но если потом будут сформированы новые группы с частичным пересечением или вложением в ранее полученные - то для СУБД это будут новыми запросами и кеш не будет задействован. Да, безусловно, в СУБД есть ещё и кеширование физических элементов (например, страниц данных) в памяти (ну или вся СУБД In memory) - но это уже явно никак особенно сильно не скажется на приведённых в статье оптимизациях.

Эффективнее - это кешировать на стадии ОRM обработки и группировки запросов - но тут задача становится сложнее - ведь нужно правильно декомпозировать исходные запросы, идентифицировать ключ обращения к данным и формировать/получать по нему кеш, а потом ещё и обратно объединять результаты взятые из кеша и полученные из СУБД. И это всё очень не просто.

А если ещё и более глубоко подойти к анализу не совсем тривиальных условий - когда есть какая-то общая часть, а есть меняющаяся - то такое тоже надо определять и кешировать запрос по общей части, а по тривиальной уже делать фильтрацию по кэшированным (и "проиндексированным") в памяти ORM данным.

И при этом ещё надо следить за расходом памяти такого кеша - т.к. может не зря изначально запросы шли более узкими порциями друг за другом - может всё дело в больших объёмах самих порций, так и более общей выборки (без доп. условий).

В общем, такая постановка задачи только на первый взгляд кажется простой - а на деле тут скрыто много подводных камней и нюансов - для достижения реальной пользы от такой доп. обработки запросов внутри посредника (ORM)

На самом деле при данной схеме понятно когда нужно начинать выполнять - когда мы дошли до точки ожидания результата (до начала event loop). Т.е. вот у нас есть код:

// Запустить event loop и получить результат
$users = Batching::run(function (Batch $batch) {
    Batching::all([
        getUserForId(1),
        getUserForId(2),
        getUserForId(3)
    ])->then(function (array $users) use ($batch) {
        // Устанавливаем результат для вызова
        // Именно он будет возвращен из функции Batching::run
        $batch->setResult(function () use ($users) {
            return $users;
        });
    });
});

Замыкание выполнилось, в нем в event loop добавились задачи группировки. Их будет три штуки (так как три вызова функции getUserForId). И вот тут будут созданы три задачи

//-- Функция получения информации о пользователе
function getUserForId(int $id): Batching
{
    return Batching::create(function (Batch $batch) {...}
}

После этого запустится event loop. Он объединит все эти задачи в один пакет, вызовет замыкание, которое в свою очередь добавит четыре задачи (три вызова функции getProfilesForUserId + Batching::all). Т.е. стандартный event loop и выполнением задач, отличие в том, что задачи группируются перед тем как отправится на выполнение.

Если нужно выполнить 1-2 запроса, то выполняем их в отдельном Batching::run(..), результат отправляем куда на нужно, а затем запускаем следующие задание.


Кэширование тут делается на стадии данных, которые возвращаются. т.е. если вы возвращаете строки, полученные из БД, то будут они кэшироваться. Вернете объекты ORM - закэшируются эти объекты.

для достижения реальной пользы от такой доп. обработки запросов внутри посредника (ORM)

На самом деле тут как раз нет привязки к ORM или базе данных. Можно использовать API вообще без использование БД. К примеру требуется выгрузить файлы на FTP. Делаем вызовы, затем в пакетной обработке группируем по FTP серверам и производим загрузку. Если у нас несколько файлов для одного сервера, то будет установлено только одно соединение и через него все файлы загрузятся.

В Laravel, Yii для этого есть "жадная загрузка". Можно жадно загрузить не только профили пользователей, но и те данные, которые связаны с профилем. Есть возможность добавлять ограничение выборки и тд. Не понимаю зачем тут "изобретать велосипед".

Laravel - https://laravel.com/docs/10.x/eloquent-relationships#eager-loading

Yii2 - https://www.yiiframework.com/doc/guide/2.0/ru/db-active-record#lazy-eager-loading

// Как выполнить все эти запросы. 
$users = Users::whereIn('name', ['Саша', 'Петя', 'Вася'])
->with('profiles')
->get();
// select * from users where name in ('Саша', 'Петя', 'Вася');
// select * from profiles where user_id in (4, 5, 8);

// Пример использования
foreach($users as $user) {
  $user->name; // имя пользователя
  foreach($user->profiles as $profile) {
    $profile; // Профиль пользователя
  }
}

// Другой пример
$users = Users::whereIn($ids)
  ->with([
    'profiles',
    'profiles.roles', //Если к профайлам привязаны роли и нам они тоже нужны
    'images' => function ($query) { 
        // Получаем изображения привязанные к пользователю, но не все, 
        // а только аватарку
        $query->where('type', 'avatar');
    }
  ])
  ->get();

Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации

Истории