Здравствуйте, меня зовут Дмитрий Карловский и я.. не люблю читать книги, потому что пока перелистываешь страницу, ты вырываешься из увлекательного повествования. И стоит чуть замешкаться, как ты забываешь на чём оборвалось последнее предложение предыдущей страницы, и приходится листать обратно, чтобы перечитать его. И если с физическими книгами это не так страшно, то вот с выдачей rest-сервера всё куда печальней - ведь сейчас на странице одни данные, а через секунду - уже совершенно другие. Давайте подумаем как же так получилось, кто виноват и главное - что делать.
Проблема
Итак, нам нужно выдать все сообщения по запросу "паджинация", начиная с самых свежих (последние изменённые сверху) или ещё в каком хитром порядке. Всё хорошо, пока у нас этих сообщений меньше сотни - мы просто делаем селект из базы и возвращаем данные:
Запрос от клиента:
GET /message/text=паджинация/
Запрос к базе:
SELECT FROM Message WHERE text LICENE "паджинация" ORDER BY changed DESC
Схема JSON ответа клиенту:
Array<{ id : number , text : string }>
Но число сообщений растёт и у нас появляются следующие неприятности:
Запросы к базе становятся всё более медленными, так как приходится выгребать всё больше данных.
Пересылка данных по сети занимает всё больше времени.
Рендеринг этих данных на клиенте становится всё дольше и дольше.
Начиная с определённого порога задержки становятся столь существенными, что нашим сайтом становится невозможно пользоваться. Если, он, конечно ещё не прилёг, устав от большого числа параллельных тяжёлых запросов.
Самое простое решение, которое возможно первым приходит в голову, да и встретить его сейчас можно в любом тостере - выдавать данные не все скопом, а разбитые на страницы. Всё, что нам нужно сделать - это всего-лишь прокинуть один дополнительный параметр от клиента в запрос к базе:
GET /message/text=паджинация/page=5/
SELECT FROM Message WHERE text LICENE "паджинация" ORDER BY changed DESC SKIP 5 * 10 LIMIT 10
SELECT count(*) FROM Message WHERE text LICENE "паджинация"
{
pageItems : Array<{ id : number , text : string }>
totalCount : number
}
Ну да, нам всё-равно пришлось пересчитать все сообщения, чтобы клиент смог нарисовать список страниц или рассчитать высоту виртуального скролла, но нам хотя бы не нужно все эти 100500 сообщений доставать из базы.
И всё было бы хорошо, если бы у нас был какой-нибудь не популярный форум на давно уже не актуальную тематику. Но нам пишут и пишут, пишут и пишут, и пока пользователь читает пятую страницу, список сообщений меняется до неузнаваемости: добавляются новые и удаляются старые. Таким образом мы получаем проблемы двух типов c точки зрения пользователя:
На следующей станице могут вновь показаться сообщения, что уже были на предыдущей.
Некоторые сообщения пользователь вообще не увидит, так как они успели переехать с 6 страницы на 5 ровно между переходом пользователя с 5 на 6.
Кроме того, у нас остались проблемы с производительностью. Каждый переход на следующую страницу приводит к тому, что нам надо заново делать аж два поисковых запроса в базу со всё большим числом пропускаемых элементов с предыдущих страниц.
Да и грамотная реализация на клиентской стороне не то что бы совсем простая - нужно всегда быть готовым к тому, что любой ответ сервера может вернуть новое общее число сообщений, а значит нам надо будет перерисовать паджинатор и редиректнуть на другую страницу, если в текущей внезапно оказалось пусто. И конечно же нельзя падать в случае появления дубликатов.
Кроме того, иногда клиенту нужно обновлять результаты поиска, но в нагрузку ему всё-равно будут приходить данные, которые у него уже и так могут быть от предыдущих запросов.
Как видно, паджинация имеет множество проблем. Неужели нет решения лучше?
Решение
Сперва давайте обратим внимание, что при работе с базой есть 2 разные по своей сути операции:
Поиск. Относительно тяжёлая операция поиска указателей на данные по некоторому запросу.
Выборка. Относительно простая операция собственно получения данных.
Идеально было бы:
Один раз произвести поиск и где-то запомнить его результаты в виде снепшота на определённый момент времени.
Быстро выбирать данные мелкими порциями по мере необходимости.
Где хранить снепшоты? тут есть 2 варианта:
На сервере. Но тогда мы забиваем его кучей мусора с результатами поисков, которые со временем надо вычищать.
Ка клиенте. Но тогда надо сразу же передавать весь снепшот клиенту.
Давайте оценим размер снепшота, который представляет из себя просто список идентификаторов. Сомнительно, чтобы пользователю хватило терпения домотать хотя бы до 100 страницы, не воспользовавшись фильтрацией и сортировкой. Допустим на страницу у нас приходится по 20 элементов. Каждый идентификатор у нас будет занимать в json представлении не более 10 байт. Перемножаем и получаем не более 20кб. А скорее всего намного меньше. Вполне разумным будет задать жёсткий лимит на размер выдачи в, допустим, 1000 элементов.
GET /message/text=паджинация/
SELECT id FROM Message WHERE text LICENE "паджинация" ORDER BY changed DESC LIMIT 1000
Array<number>
Теперь клиент может нарисовать хоть паджинатор, хоть виртуальный скролл, запрашивая данные лишь по интересующим его идентификаторам.
GET /message=49,48,47,46,45,42,41,40,39,37/
SELECT FROM Message WHERE id IN [49,48,47,46,45,42,41,40,39,37]
Array< { id : number , text : string } | { id : number , error : string } >
Что мы в итоге получаем:
Нормализованное API: поиск отдельно, выборка данных отдельно.
Минимизация числа поисковых запросов.
Можно не запрашивать данные, что уже загружены, или обновлять их в фоне.
Относительно простой и универсальный код на клиентской стороне.
Из недостатков можно отметить разве что:
Чтобы показать что-то пользователю нужно сделать минимум 2 последовательных запроса.
Нужно обрабатывать случай, когда идентификатор есть, а сами данные по нему уже не доступны.
https://page.hyoo.ru/#!=erz4hs_vmutcw
Здравствуйте, меня зовут Дмитрий Карловский и я.. не люблю читать книги, потому что пока перелистываешь страницу, ты вырываешься из увлекательного повествования. И стоит чуть замешкаться, как ты забываешь на чём оборвалось последнее предложение предыдущей страницы, и приходится листать обратно, чтобы перечитать его. И если с физическими книгами это не так страшно, то вот с выдачей rest-сервера всё куда печальней - ведь сейчас на странице одни данные, а через секунду - уже совершенно другие. Давайте подумаем как же так получилось, кто виноват и главное - что делать.
Проблема
Итак, нам нужно выдать все сообщения по запросу "паджинация", начиная с самых свежих (последние изменённые сверху) или ещё в каком хитром порядке. Всё хорошо, пока у нас этих сообщений меньше сотни - мы просто делаем селект из базы и возвращаем данные:
Запрос от клиента:
Запрос к базе:
Схема JSON ответа клиенту:
Но число сообщений растёт и у нас появляются следующие неприятности:
Начиная с определённого порога задержки становятся столь существенными, что нашим сайтом становится невозможно пользоваться. Если, он, конечно ещё не прилёг, устав от большого числа параллельных тяжёлых запросов.
Самое простое решение, которое возможно первым приходит в голову, да и встретить его сейчас можно в любом тостере - выдавать данные не все скопом, а разбитые на страницы. Всё, что нам нужно сделать - это всего-лишь прокинуть один дополнительный параметр от клиента в запрос к базе:
Ну да, нам всё-равно пришлось пересчитать все сообщения, чтобы клиент смог нарисовать список страниц или рассчитать высоту виртуального скролла, но нам хотя бы не нужно все эти 100500 сообщений доставать из базы.
И всё было бы хорошо, если бы у нас был какой-нибудь не популярный форум на давно уже не актуальную тематику. Но нам пишут и пишут, пишут и пишут, и пока пользователь читает пятую страницу, список сообщений меняется до неузнаваемости: добавляются новые и удаляются старые. Таким образом мы получаем проблемы двух типов c точки зрения пользователя:
Кроме того, у нас остались проблемы с производительностью. Каждый переход на следующую страницу приводит к тому, что нам надо заново делать аж два поисковых запроса в базу со всё большим числом пропускаемых элементов с предыдущих страниц.
Да и грамотная реализация на клиентской стороне не то что бы совсем простая - нужно всегда быть готовым к тому, что любой ответ сервера может вернуть новое общее число сообщений, а значит нам надо будет перерисовать паджинатор и редиректнуть на другую страницу, если в текущей внезапно оказалось пусто. И конечно же нельзя падать в случае появления дубликатов.
Кроме того, иногда клиенту нужно обновлять результаты поиска, но в нагрузку ему всё-равно будут приходить данные, которые у него уже и так могут быть от предыдущих запросов.
Как видно, паджинация имеет множество проблем. Неужели нет решения лучше?
Решение
Сперва давайте обратим внимание, что при работе с базой есть 2 разные по своей сути операции:
Идеально было бы:
Где хранить снепшоты? тут есть 2 варианта:
Давайте оценим размер снепшота, который представляет из себя просто список идентификаторов. Сомнительно, чтобы пользователю хватило терпения домотать хотя бы до 100 страницы, не воспользовавшись фильтрацией и сортировкой. Допустим на страницу у нас приходится по 20 элементов. Каждый идентификатор у нас будет занимать в json представлении не более 10 байт. Перемножаем и получаем не более 20кб. А скорее всего намного меньше. Вполне разумным будет задать жёсткий лимит на размер выдачи в, допустим, 1000 элементов.
Теперь клиент может нарисовать хоть паджинатор, хоть виртуальный скролл, запрашивая данные лишь по интересующим его идентификаторам.
Что мы в итоге получаем:
Из недостатков можно отметить разве что: