Изображение от Freepik
Изображение от Freepik

В вашем сервисе есть API для запроса массива объектов с фильтрацией и пагинацией. Но что делать, если эти объекты переезжают в другой источник данных? Мигрировать существующие данные в новое место и перенастроить конфигурацию вашего сервиса. А если такая миграция недоступна? Найдётся вариант и на этот случай!

Предположим, что у нас есть ничем не примечательный микросервис или монолит, который среди прочего предоставляет API для получения списка объектов. Для выдачи списка применяется пагинация, основанная на offset/limit. В этом случае запрос списка данных содержит указание порядкового номера первой запрашиваемой записи или страницы (offset) и размер одной страницы (limit). В ответе, в дополнении к запрошенной странице данных, содержится информация об общем количестве элементов списка и общем количестве страниц. При использовании реляционных БД подобного можно достичь, используя отдельный запрос количества записей, попадающих в выборку, и запрос самих записей, попадающих на требуемую страницу. Такое API обычно поддерживает фильтрацию и сортировку списка и может использоваться как для отображения в интерфейсе пользователя, так и для других целей.

API получения списка объектов
API получения списка объектов

Спринты идут и появляются новые требования. Планируются изменения в наших объектах. Появляется новое место хранения данных и способ их получения. Например, переход на использование другой СУБД или несовместимые изменения схемы данных в текущей БД. При этом предусматривается переходный период, когда в системе должны функционировать обе версии объектов. По факту, этот переходный период может растянуться на месяца. Встаёт вопрос, как не сломать наше API запроса списка объектов, с учётом, того, что их необходимо получать из двух источников?

Запрос всех данных из двух источников с последующей агрегацией и фильтрацией силами рассматриваемого сервиса можно отбросить сразу, в силу неэффективности. Предлагаю рассмотреть подход, разделяющий все страницы объектов на две группы по признаку их источника. При этом все объекты из нового источника данных будут расположены в первых i страницах выдачи. Все объекты из старого источника данных будут расположены на страницах, начиная с i+1. Такое разделение на две группы происходит с учётом указанных в запросе фильтров.

Совместная пагинация старых и новых объектов
Совместная пагинация старых и новых объектов

Алгоритм состоит из следующих шагов:

  1. В исходном запросе присутствуют: запрашиваемая страница requested_page, желаемый размер страницы page_size и опциональные параметры фильтрации.

  2. Получение общего количества старых объектов total_items_old, попадающих в выборку.

  3. Вычисление количества страниц старых объектов
    total_pages_old = ceil(total_items_old/page_size).

  4. Получение общего количества новых объектов total_items_new, попадающих в выборку.

  5. Вычисление количества страниц новых объектов
    total_pages_new = ceil(total_items_new/page_size).

  6. Если запрашиваемая страница requested_page попадает в диапазон
    [1, total_pages_new], то делаем запрос данных из нового места хранения объектов.

  7. Иначе, делаем запрос в старое место хранения, не забыв преобразовать номер нужной страницы actual_page = requested_page - total_pages_new.

  8. В ответе в качестве общего количества страниц и объектов следует отдать (total_pages_old + total_pages_new) и (total_items_old + total_items_new) соответственно.

  9. В результате запроса нужно отдать список объектов, полученный в п.6 или п.7.

Схема алгоритма
Схема алгоритма

Описанный подход содержит несколько компромиссов. Серьёзный минус - это невозможность использовать сортировку агрегированного списка объектов. Менее значительный - наличие страницы с номером total_pages_new, которая в большинстве случаев будет содержать меньше записей, чем запрошено. Если такое поведение неприемлемо, то можно усложнить расчёт отступов, чтобы заполнить эту страницу объектами из старого источника данных.

Спасибо за внимание, пусть ваши временные решения не становятся постоянными.

Комментарии (21)


  1. BugM
    20.05.2023 11:59
    +3

    Это же не работает. Точнее работает, но выдает неверный результат.

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


    1. ARei0 Автор
      20.05.2023 11:59

      Да, вы правы, сортировка не будет работать, в предпоследнем абзаце это было указано:

      Серьёзный минус - это невозможность использовать сортировку агрегированного списка объектов


      1. BugM
        20.05.2023 11:59

        Это не минус. Это неработа программы. Пользователи очень расстраиваются когда вместо сортировки получают ерунду какую-то.


        1. breninsul
          20.05.2023 11:59

          может там ее и не было, сортировки этой


          1. BugM
            20.05.2023 11:59
            +2

            Несортированный список с пагинацией не имеет смысла. Даже в теории. БД может возвращать данные в любом порядке в этом случае.


            1. breninsul
              20.05.2023 11:59

              это правда. Но это не значит, что сортировка была)

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

              Ну и гарантии, что пока мы кликаем не добавилось записей, и у нас стало больше/меньше записей, соответсвенно страницы изменились все-равно нет.

              Т.е. пагинация работать будет нормально, если это редко обновляемые данные.


              1. BugM
                20.05.2023 11:59
                +1

                Надеюсь что вы это только в академических целях пишите.

                Любой вывод данных пользователю без сортировки должен зарубаться на ревью. Ну или на тестировании. Въедливые ручные тестировщики новых фич полезны. Автотесты такое не видят.

                В задаче не написали по чему сортировать? Сортируй по имени. Нет имени? Сортируй по номеру. Номера тоже нет? Сортируй по ID объектов. Его тоже нет? Тут пора что-то править глобально. Без PK жить совсем нельзя.


              1. ARei0 Автор
                20.05.2023 11:59

                Да, сортировка по произвольному полю не требуется в моём кейсе. Сортировки объектов в рамках отдельной схемы по PK (используется UUID) достаточно, чтобы данные выводились стабильно.

                Если требуется сортировка по произвольному полю, то, как уже упоминали в комментариях, без миграции данных в одну БД или других вариантов аггрегации (эластик) не обойтись.


  1. breninsul
    20.05.2023 11:59
    +2

    Мне кажется, что в таком случае логично все просто лить в эластик/сфинкс (pgsync или похожим софтом для ваших БД)?

    Ну и count это очень больно для БД. Все от количества данных, конечно, зависит. Но база вынуждена отсканировать всю-всю таблицу/таблицы для получения в целом бесполезного параметра.

    Оно надо? Обычно достаточно знать, что есть сл. страница, или еще 2,3,n страниц.


  1. Didimus
    20.05.2023 11:59

    Всегда было интересно, почему не делать пагинацию на клиенте? Неужели так сложно выгрузить 1000 товаров сразу, каждая запись меньше килобайта, а дальше браузер тянет картинки для отображаемых карточек. Это если интернет-магазин, например


    1. BugM
      20.05.2023 11:59
      +1

      А если товаров миллион?

      Для типичного маркетплейса миллион позиций это реально вполне.


      1. Didimus
        20.05.2023 11:59

        Никто вам не даст без фильтра вывести все позиции. Отобрал товарную категорию, отфильтровал. Например, блоки питания. Или ссд-диски. Или флэшки


        1. breninsul
          20.05.2023 11:59
          +2

          пробуйте, материтесь, делайте выводы )


    1. breninsul
      20.05.2023 11:59
      +2

      эээ .. ну по тому-что фронту не нужно 1000 записей?

      А по факту там будет не тысяча, а как указали, миллион.

      И сразу у вас задохнётся БД, а потом сеть. А потом и браузер клиента, но это уже не бэкендерские пробоемы)


    1. stackjava
      20.05.2023 11:59

      Самому бэку создать 1000 обьектов на каждый запрос тоже накладно.

      100 запросов в секунду считай 100 000 объектов в памяти одной ноды по одному апи.

      А в сервисе еще и другие апи есть, которые тоже создают нагрузку.


      1. Didimus
        20.05.2023 11:59

        Только ваш кейс с пажинацией создаёт в сто раз больше запросов к БД и на сети. При этом накладные расходы составляют большую часть нагрузки

        И пользователю приходится ждать, пока его запрос обрабатывается при каждом перещёлкивании страницы результатов


        1. stackjava
          20.05.2023 11:59

          Не факт что мне нужно увидеть 1000 товаров.

          Я могу останоаиться на 10 и провалиться в него. Пойму что не подходит, вернусь обратно и запрошу снова 1000 товаров.

          И так может повторяться много раз. И каждый раз буду запрашивать по 1000... Это оверхед. И по статистике человек редко проходит дальше определенного числа результатов выдачи...

          Так в яндекс поиске большая часть людей не проходит дальше 1 стр. И нужно ли было им отдавать 1000 результатов? Только бд, сервис, сеть нагрузили бы.


        1. breninsul
          20.05.2023 11:59

          это не правда, откуда большая нагрузка?

          Более того, "вам не даст без фильтра вывести все позиции".

          А когда окажется, что есть фильтр, покрывающий 99% позиций?

          Будем городить костыли?

          Ваша схема не работоспособна. Просто попробуйте применить ее на хоть сколько-нибудь большой базе


          1. Didimus
            20.05.2023 11:59

            «откуда большая нагрузка?» - я регулярно покупаю в интернет магазинах. И вижу тормоза

            «А когда окажется, что есть фильтр, покрывающий 99% позиций?» - не окажется, т.к. сперва вы проваливаетесь в категорию, представляющую очень ограниченную вьюшку. Тех же жестких дисков штук 100 разных


  1. dopusteam
    20.05.2023 11:59
    +1

    А в чем суть статьи? Мне кажется, решение не выглядит каким то неординарным или таким, до которого сложно дойти самому.

    Плюс проблема с сортировкой - вот её решение было бы интересно


    1. BugM
      20.05.2023 11:59
      +3

      На практике все скучно делается.

      Делаем вторую БД. Пишем и в новую и в старую. Читаем из старой. Потихоньку копируем. Как все скопировали переключаем чтение на новую. Отключаем запись в старую. Вычищаем хвосты.

      Скучно, долго, надежно.