Предпосылки разработки методов API с пагинацией
В нашей компании разрабатывался сайт на собственном движке, и поскольку источником всех данных для него была учётная система на платформе 1С, а в штате было два программиста 1С, в качестве бэкенда было принято решение разработать отдельную систему на той же 1С.
Весь необходимый API был описан в формате Swagger (OpenAPI) руководителем проекта разработки сайта, его методы мы реализовали с помощью HTTP-сервисов 1С.
Метод API для получения номенклатуры для сайта изначально не предполагал никаких отборов, т.к. до какого-то момента выполнялся за приемлемое время, но спустя несколько месяцев из-за увеличившегося количества номенклатуры и связанных с ней данных метод стал выполняться очень долго, клиент не дожидался ответа.
С таймаутами заморачиваться не стали, т.к. они и так были установлены достаточно большими, по 300 секунд.
Решили сделать версию 2 того же метода, но уже с возможностью пагинации.
Особенности реализации пагинации средствами 1С
При разработке методов с пагинацией нужно учитывать следующие особенности:
- В языке запросов 1С нет оператора OFFSET, соответственно, смещение записей для получения определённой порции данных нужно реализовать как-то иначе.
Ограничить количество записей можно с помощью оператора ВЫБРАТЬ ПЕРВЫЕ {количество объектов в порции}, тут всё очевидно.
Разве что количество объектов в порции придётся подставлять через СтрШаблон(), СтрЗаменить() или конкатенацию.
Но как выбрать только записи, начиная с определённой?
Логичным решением выглядит нумерация записей и последующий отбор по номеру.
В запросах 1С доступна функция АВТОНОМЕРЗАПИСИ(), которая последовательно присваивает номер записи, начиная с 1, полю с вызовом этой функции.
А дальше по этому номеру можно отобрать нужную порцию данных.
Это позволит не выбирать все данные с последующим получением нужных данных в коде, например, по индексам, а получать сразу готовую коллекцию записей с помощью запроса.
- Запросы к методу с одними и теми же полями "limit" (количество объектов в порции данных) и "next"* (номер первого объекта следующей порции) в теле запроса всегда** должны возвращать один и тот же набор объектов.
* Названия полей приведены так, как они указаны у нас, вы можете их называть, разумеется, на ваше усмотрение.
** Передача тела запроса предполагает запрос POST, т.е. об идемпотентности тут речи нет, но метод, возвращающий разные результаты при одних и тех же параметрах запроса, должен вызывать вопросы у клиента. Для большей очевидности (в плане ожидания идемпотентности) можно было бы реализовать метод GET с параметрами в строке запроса, но мы пошли по пути POST с телом JSON.
Тут тоже всё вроде бы очевидно: данные должны быть отсортированы, чтобы одни и те же номера всегда присваивались одним и тем же записям.
Для этого лучше всего подходит код элемента в справочнике, т.к. коды в справочниках никто при нормальном учёте не меняет, а уникальный идентификатор нового объекта теоретически может быть сгенерирован таким, что при сортировке по ссылке новый элемент справочника попадёт куда-то в середину списка. Если ошибаюсь, поправьте, пожалуйста.
- Запросы к методу должны возвращать именно запрошенное (или меньшее) количество записей.
Хотя левое соединение с другими таблицами может приводить к увеличению количества записей, изначально было не вполне очевидно, что возвращаемое методом количество записей может быть больше запрашиваемого.
Чтобы этого избежать, основные данные предварительно нужно получать отдельно в запрашиваемом количестве и только потом связывать с дополнительными данными. В последней порции данных может быть меньше.
Из-за того, что данные временных таблиц в общем случае*** упорядочивать нельзя, связывание основных данных с дополнительными нужно выполнять в дополнительном запросе.
*** Можно в случае использования оператора ВЫБРАТЬ ПЕРВЫЕ.
Пример реализации
Часть комментариев добавил для пояснения тех или иных решений, остальные комментарии и описание взяты из фактической реализации.
Многоточием в тексте запросов обозначены прочие поля.
В первом запросе получаем, фактически, все записи таблицы. Дополнительно не оптимизировали, т.к. отрабатывает быстро, да и клиент у этого метода всего один.
// Возвращает данные товаров с пагинацией для дальнейшего преобразования в JSON.
//
// Ввиду отсутствия в языке запросов 1С аналога оператора OFFSET
// пагинацию через запрос можно сделать только обходными путями, как в коде этой функции:
// сгенерировать номер записи через АВТОНОМЕРЗАПИСИ(), упорядочить данные по номеру записи,
// выбрать нужное количество записей, начиная с указанного номера.
//
// Номенклатуру нужно всегда предварительно получать отдельно,
// чтобы количество возвращаемой номенклатуры реально соответствовало количеству запрошенной номенклатуры,
// а не количеству записей, образованных левым соединением.
//
// Параметры:
// КоличествоЗаписей - Число - Количество записей, которые нужно вернуть.
// НачинаяСЗаписи - Число - Номер записи, начиная с которой нужно вернуть данные.
//
// Возвращаемое значение:
// Массив - Массив структур для дальнейшего преобразования в JSON.
//
Функция ПолучитьТоварыСПагинацией(Знач КоличествоЗаписей, Знач НачинаяСЗаписи) Экспорт
// Принимаем за 1000 максимальное количество элементов справочника Номенклатура,
// которое можно получить за один запрос.
// При неправильном значении запрошенных записей вернется 1000 записей,
// а номер первого элемента следующей порции будет возвращен в ответе сервера.
Если (КоличествоЗаписей = 0) Или (КоличествоЗаписей > 1000) Тогда
КоличествоЗаписей = 1000;
КонецЕсли;
// Упорядочить временную таблицу можно только с ограничением количества записей.
// Устанавливаем какое-нибудь малодостижимое количество.
Запрос = Новый Запрос;
Запрос.Текст = СтрШаблон("ВЫБРАТЬ ПЕРВЫЕ 100000
| АВТОНОМЕРЗАПИСИ() КАК НомерЗаписи,
| Номенклатура.Ссылка КАК Ссылка,
| Номенклатура.Код КАК Код,
| Номенклатура.Наименование КАК Наименование,
| ...
|ПОМЕСТИТЬ ДанныеНоменклатуры
|ИЗ
| Справочник.Номенклатура КАК Номенклатура
|ГДЕ
| НЕ Номенклатура.ПометкаУдаления
| И НЕ Номенклатура.ЭтоГруппа
|
|УПОРЯДОЧИТЬ ПО
| Код
|;
|
|////////////////////////////////////////////////////////////////////////////////
|ВЫБРАТЬ ПЕРВЫЕ %1
| ДанныеНоменклатуры.НомерЗаписи КАК НомерЗаписи,
| ДанныеНоменклатуры.Ссылка КАК Ссылка,
| ДанныеНоменклатуры.Код КАК Код,
| ДанныеНоменклатуры.Наименование КАК Наименование,
| ...
|ИЗ
| ДанныеНоменклатуры КАК ДанныеНоменклатуры
|ГДЕ
| ДанныеНоменклатуры.НомерЗаписи >= &НачинаяСЗаписи
|
|УПОРЯДОЧИТЬ ПО
| НомерЗаписи", Формат(КоличествоЗаписей, "ЧГ="));
Запрос.УстановитьПараметр("НачинаяСЗаписи", НачинаяСЗаписи);
// Для получения нужных записей данные должны быть обязательно предварительно упорядочены.
// Для временных таблиц это невозможно, поэтому нужные записи выгружаем в таблицу и передаем
// в следующий запрос, чтобы подтянуть все остальные данные.
ДанныеЗапрошеннойНоменклатуры = Запрос.Выполнить().Выгрузить();
Запрос.Текст = "ВЫБРАТЬ
| ДанныеЗапрошеннойНоменклатуры.Ссылка КАК Ссылка,
| ДанныеЗапрошеннойНоменклатуры.Код КАК Код,
| ДанныеЗапрошеннойНоменклатуры.Наименование КАК Наименование,
| ...
|ПОМЕСТИТЬ ДанныеНоменклатуры
|ИЗ
| &ДанныеЗапрошеннойНоменклатуры КАК ДанныеЗапрошеннойНоменклатуры
|
|ИНДЕКСИРОВАТЬ ПО
| Ссылка
|;
|
|////////////////////////////////////////////////////////////////////////////////
|// Здесь связь с другими таблицами и получение итогового набора записей.";
Запрос.УстановитьПараметр("ДанныеЗапрошеннойНоменклатуры", ДанныеЗапрошеннойНоменклатуры);
РезультатЗапроса = Запрос.Выполнить();
ВыборкаТоваров = РезультатЗапроса.Выбрать(ОбходРезультатаЗапроса.ПоГруппировкам);
Товары = Новый Массив;
Пока ВыборкаТоваров.Следующий() Цикл
// ПолучитьДанныеНоменклатуры() - это функция, возвращающая структуру
// с данными номенклатуры и связанными с ней данными
// для дальнейшего преобразования в объект JSON.
ДанныеТекущейНоменклатуры = ПолучитьДанныеНоменклатуры(ВыборкаТоваров);
Товары.Добавить(ДанныеТекущейНоменклатуры);
КонецЦикла;
Возврат Товары;
КонецФункции