Эффективная разбивка на страницы на стороне сервера с использованием Mongodb
24 мая 2022 г.Как разработчик, вы, возможно, сталкивались с ситуацией, когда вам требовалось отобразить огромное количество данных для отображения в пользовательском интерфейсе в табличном формате/сетке без снижения производительности. Если вы разработчик, который тратит приличное количество времени на кодирование, возможно, вы уже догадались об этом.
Разбивка на страницы на стороне сервера! да, вы правы. Это наиболее оптимальный способ отображения больших данных.
Это почему?
Рассмотрим следующий сценарий. Предположим, что ваше приложение продает товары в Интернете, и у вас есть база продуктов, содержащая более миллиона позиций, которые добавляются в базу данных с помощью мастер-формы. Очевидно, что теперь конечный пользователь должен иметь возможность удалять и редактировать уже добавленные продукты.
Для этого вам нужно предоставить им интерфейс, предположительно, сетку, где пользователь может просматривать конкретный продукт, который он/она намеревается редактировать/удалять. Теперь подумайте о сценарии, в котором этот интерфейс заполняется всеми 1 миллионом продуктов. Беспорядочный. * не так ли? * Никто никогда не мог согласиться с тем, что вы дали, потому что это грязно, медленно и неудобно. Таким образом, в таких ситуациях интеллектуалы предлагают вам разбить таблицы на страницы с правильным размером страницы и номером страницы.
Да, ты снова прав. Вам просто нужна разбивка на страницы на стороне клиента, чтобы решить указанную проблему. Тогда почему мы должны использовать разбиение на страницы на стороне сервера?
Причина в производительности. Если вы используете разбивку на страницы на стороне клиента, даже если она реализована с использованием какой-либо сторонней библиотеки (например, таблицы данных jquery или сетки кендо) или вашей пользовательской логики, где-то данные должны храниться на стороне клиента. У этого подхода есть 5 проблем.
- Требуется высокая пропускная способность, поскольку данные (мы говорим о миллионе продуктов) должны проходить через Интернет из базы данных на сторону клиента.
- В случае реляционных баз данных данные должны быть сопоставлены с DTO перед возвратом, что требует огромных вычислений, а также потребления памяти за один раз.
- Высокая вероятность тайм-аута сервера базы данных, т.к. он нужен для выборки большого объема данных, возможно, в том числе и с фильтрацией.
- Если между ними что-то пойдет не так, все затраченные до сих пор вычисления будут потрачены впустую. Подумайте о ситуации, когда один продукт, находящийся на 99999-й позиции, имеет отсутствующее значение и не может быть отображен в DTO из-за обязательной проверки, которая создает исключение. Вся работа до сих пор выполняется механизмом базы данных, пока 99998-й продукт не пропадет даром. Все усилия, затраченные до сих пор методом картирования, также потрачены впустую. Конечный пользователь ничего не получает даже после всей борьбы системы.
- Скажем, в гипотетической ситуации все прошло нормально, но огромные данные приходится хранить либо в DOM, либо в памяти браузера. Бьюсь об заклад, что большая часть обработки данных на стороне клиента заставляет браузер работать неисправимо неправильно.
Реализация MongoDB разбивки на страницы на стороне сервера
В Интернете есть много статей и руководств, посвященных этой конкретной теме. Но большинство или все из них упускают очень простую вещь, необходимую для обработки разбивки на страницы на стороне сервера, то есть: - они не возвращают Общее количество данных, которые мы извлекаем. Да многие этого не закрывают. Так в чем проблема?
Хорошо, позвольте мне объяснить. Вещи, которые нам нужно отправить в качестве параметров на сервер из внешнего интерфейса, — это размер страницы (сколько данных должно отображаться на одной странице) и номер страницы (начиная с нуля). Без отправки сервером общего количества данных вместе с результатом первой страницы мы никогда не сможем определить, сколько страниц должно отображаться в пользовательском интерфейсе. Что это означает? Это означает, что вы никогда не позволите пользователю перейти с page-1 на page-3 или со page-3 на page-n. В этом случае мы можем только дать пользователю перейти на следующую страницу или на предыдущую. Следовательно, если пользователю нужно перейти на 30-ю страницу, он должен сначала пройти от 1 до 29. Я не думаю, что было бы хорошей идеей давать демонстрацию этого любому клиенту, особенно тем, кто надоедает по всему.
Мы будем использовать тот же пример продукта, который я упоминал ранее. Я буду максимально простым, чтобы мы все были на одной волне в будущем. Шаг за шагом мы попытаемся реализовать конвейер агрегации MongoDB , чтобы возвращать продукты в на стороне сервера с дополнительным фильтром категории продукта.
Это базовая структура всех продуктов, входящих в коллекцию Продукты . Предположим, что в этой коллекции более 1 миллиона таких товаров в разных категориях.
```javascript
"_id": идентификатор объекта ("5fb152f2bcc0ee5eb068ccf5"),
«Название» : «Товар-1»,
«Цена» : 25,3,
«Категория» : «Еда»
"_id": идентификатор объекта ("5fb152f2bcc0ee5eb068ccf6"),
«Название» : «Товар-1»,
"Цена": 263,0,
«Категория»: «Непродовольственные товары».
"_id": идентификатор объекта ("5fb152f2bcc0ee5eb068ccf7"),
«Название» : «Товар-1»,
«Цена» : 159,0,
«Категория»: «Стационарные»
Начнем с основ. Во-первых, мы попытаемся получить все продукты за один раз с дополнительным параметром фильтра, который является категорией продукта. Для этого нам нужно использовать только этап $match в конвейере.
```javascript
//в реальном мире это параметры из кода
категория вар = ноль; //дополнительный фильтр для категории
db.getCollection('Товары').aggregate
$матч:
$или:
{ ноль: категория },
{ "Категория": категория}
В приведенном выше коде этап $match сопоставляет все продукты в указанной категории, если какая-либо категория указана в качестве фильтра (если категория имеет значение), и возвращает все продукты во всех категориях, если ни одна из категорий не указана в качестве фильтра. фильтр(если категория нулевая)
На этапе $match значение в левой части условия соответствия всегда считается путем к свойству документа. Поэтому, если вы измените { null:category } на { category : null}, наши ожидания не оправдаются
Итак, мы знаем, как получить все данные за один раз, даже если к ним применен какой-либо фильтр или нет. Теперь мы можем выполнить разбиение на страницы на стороне сервера в приведенном выше коде. Для этого агрегатный конвейер дает нам два этапа: $skip и * $limit*
```javascript
//в реальном мире это параметры из кода
категория вар = ноль; //дополнительный фильтр для категории
вар размер страницы = 20;
вар номер_страницы = 0;
db.getCollection('Товары').aggregate
$матч:
$или:
{ ноль: категория },
{ "Категория": категория}
$skip: размер страницы * номер страницы
$limit:размер страницы
В дополнение к предыдущему коду мы передаем выходные данные с этапа $match на следующий этап конвейера, то есть на этап $skip. На этапе пропуска будут удалены первые n продуктов из всех продуктов, полученных в качестве входных данных, где n – значение, указанное на этапе $skip. Следующим этапом конвейера является $limit, который делает именно то, что говорит, за исключением того, что первые m продуктов будут удалены, остальные будут удалены, где m – значение, указанное в $limit, а результат будет передан в следующий этап в конвейере, если таковой имеется. В нашем случае в конвейере больше нет стадий и, следовательно, они возвращаются.
Примеры случаев
pageNumber=0
и pageSize=20
вернут первые 20 товаров.
pageNumber=1
& pageSize=20
вернет следующие 20 товаров после пропуска первых 20 (pageNumber*pageSize
)
pageNumber=25
& pageSize=20
вернет следующие 20 товаров после пропуска первых 500 (pageNumber*pageSize
)
Но, тем не менее, мы не смогли вернуть общее количество продуктов вместе с результатом, который необходим библиотекам сетки для отображения номеров страниц в пользовательском интерфейсе. На приведенном ниже рисунке можно дать конечному пользователю представление о том, сколько всего страниц доступно ему для просмотра.
Итак, теперь мы попытаемся получить общее количество продуктов вместе с самими продуктами. Раньше разработчики обычно получали подсчет и данные с помощью двух отдельных вызовов к БД из-за некоторых ограничений, которые были у MongoDB. Этот подход приведет к двойной фильтрации и сканированию огромных данных. Ядро базы данных должно будет использовать почти одинаковые вычисления, чтобы получить оба результата. Однако с появлением версии 3.4 мы можем использовать столь сложный *$facet * этап в разработке.
Обрабатывает несколько конвейеров агрегации на одном этапе с одним и тем же набором входных документов. Каждый подконвейер имеет собственное поле в выходном документе, где его результаты сохраняются в виде массива документов.
По сути, это говорит о том, что входные данные этапа $facet могут быть переданы через несколько отдельных конвейеров, а результаты каждого подконвейера будут сохранены как свойство выходного документа $facet. В нашем случае мы можем использовать $facet в качестве первого этапа с двумя подчиненными конвейерами внутри. Один для получения необходимых данных, а другой для получения общего количества. Давайте реорганизуем код, который мы написали до сих пор.
```javascript
//в реальном мире это параметры из кода
категория вар = ноль; //дополнительный фильтр для категории
вар размер страницы = 20;
вар номер_страницы = 0;
db.getCollection('Товары').aggregate
$фасет:
"Товары":
$матч:
$или:
{ ноль: категория },
{ "Категория": категория}
$skip: размер страницы * номер страницы
$limit:размер страницы
"Считать":
$ группа:
_id: ноль,
«Всего»: { $sum:1 }
Если мы выполним приведенный выше код, результат будет иметь один документ с двумя свойствами: Products, который содержит результирующие данные, и Count, который содержит общее количество фактических данных. В этом случае подсчетом будет общее количество продуктов, находящихся в БД, которое будет одинаковым для каждой возвращаемой страницы. Это количество может использоваться сторонними библиотеками или вашим пользовательским кодом для определения количества страниц, отображаемых в пользовательском интерфейсе.
Промежуточный результат
```javascript
"Товары" : [
"_id": идентификатор объекта ("5fb152f2bcc0ee5eb068ccf5"),
«Название» : «Товар-1»,
«Цена» : 25,3,
«Категория» : «Еда»
"_id": идентификатор объекта ("5fb152f2bcc0ee5eb068ccf6"),
«Название» : «Товар-1»,
«Цена» : 263,0,
«Категория»: «Непродовольственные товары».
"_id": идентификатор объекта ("5fb152f2bcc0ee5eb068ccf7"),
«Название» : «Товар-1»,
«Цена» : 159,0,
«Категория»: «Стационарные»
"Считать" : [
"_id": ноль,
«Всего» : 3,0
В приведенном выше результате требуется небольшая очистка, которая заключается в выравнивании продуктов и перемещении общего свойства внутри продуктов. Мы можем использовать $unwind, $addFields и $replaceRoot
```javascript
//в реальном мире это параметры из кода
категория вар = ноль; //дополнительный фильтр для категории
вар размер страницы = 20;
вар номер_страницы = 0;
db.getCollection('Товары').aggregate
$фасет:
"Товары":
$матч:
$или:
{ ноль: категория },
{ "Категория": категория}
$skip: размер страницы * номер страницы
$limit:размер страницы
"Считать":
$ группа:
_id: ноль,
«Всего»: { $sum:1 }
$раскрутить:"$Продукты"
$добавить поля:
"Products.Total": { $arrayElemAt:["$Count.Total",0]}
$ заменить корень:
newRoot:"$Продукты"
Конечный результат
```javascript
"_id": идентификатор объекта ("5fb152f2bcc0ee5eb068ccf5"),
«Название» : «Товар-1»,
«Цена» : 25,3,
«Категория» : «Еда»,
«Всего» : 3,0
"_id": ObjectId("5fb152f2bcc0ee5eb068ccf6"),
«Название» : «Товар-1»,
«Цена» : 263,0,
«Категория»: «Непродовольственные товары»,
«Всего» : 3,0
"_id": идентификатор объекта ("5fb152f2bcc0ee5eb068ccf7"),
«Название» : «Товар-1»,
«Цена» : 159,0,
«Категория»: «Стационарные»,
«Всего» : 3,0
Дальнейшие улучшения производительности
Есть некоторые точки давления, которые вы могли заметить при реализации вышеуказанного запроса MongoDB в реальном приложении с огромными данными. Например, этап $skip в конвейере становится очень дорогостоящим, особенно когда конечные пользователи пытаются перейти на более дальние страницы. Либо вы можете просто проигнорировать его, зная, как часто пользователю может понадобиться переходить на 300-ю страницу, либо вы можете полностью избежать стадии $skip , воспользовавшись естественным свойством упорядочения поля _id , которое будет уникальным для каждого документа по умолчанию. Хорошая реализация находится в этой [статье] (https://www.codementor.io/@arpitbhayani/fast-and-efficient-pagination-in-mongodb-9095flbqr?ref=hackernoon.com) Арпита Бхаяни. Но обратите внимание, что если вы планируете использовать этот подход, конечные пользователи потеряют возможность переходить с одной страницы на некоторые другие страницы, которые не являются соседними с текущей, т.е. пользователь должен сначала пройти от 1 до 29 страниц, если ему нужно перейти на 30-ю страницу. Поэтому используйте этот подход только в том случае, если вы не планируете предоставлять такую гибкость.
Кроме того, mongodb предоставляет очень эффективный механизм индексации, с помощью которого вы можете индексировать данные таким образом, чтобы их можно было использовать в качестве преимущества на этапах $match и $sort . Однако все зависит от того, как вы реализуете разбиение на страницы, поскольку приложение полностью принадлежит вам.
Удачного кодирования!
Эта статья была первоначально опубликована [здесь] (https://beingnin.medium.com/implement-server-side-pagination-in-mongodb-with-total-count-cfbb11b5c956)
Оригинал