Зарисовки на тему оформления кода HTTP-сервиса 3. Провайдер данных
В третьей части цикла о коде HTTP-сервисов автор поднимает тему, которая обычно остаётся за кадром типовых инструкций: как правильно организовать слой провайдера данных, чтобы не плодить копипасту и не терять контроль над транзакциями. Вместо абстрактных советов — конкретный шаблон с разбором граничных случаев.
Почему провайдер данных — не просто «ещё один слой»
В справке 1С и на форумах чаще всего встречаются два подхода: либо весь код обработки запроса пишут прямо в теле HTTP-сервиса, либо выносят логику в общие модули с единственной функцией «ВыполнитьЗапросКБазе». Первый вариант быстро превращается в «спагетти», второй — порождает дублирование кода при работе с разными источниками данных (регистры, документы, справочники).
Ключевой тезис: Провайдер данных — это не прослойка «для красоты», а единственное место, где вы управляете жизненным циклом транзакции, блокировками и кэшированием. Если провайдер написан правильно, HTTP-сервис становится просто маршрутизатором запросов.
Механика под капотом: почему без провайдера — больно
Рассмотрим типовую ситуацию: HTTP-сервис принимает JSON с данными для создания документа. Без провайдера разработчик пишет:
Функция ОбработкаЗапроса(Запрос, Ответ)
Данные = ПрочитатьJSON(Запрос.ПолучитьТелоКакСтроку());
Док = Документы.ЗаказПокупателя.СоздатьДокумент();
ЗаполнитьЗначенияСвойств(Док, Данные);
Попытка
Док.Записать();
Ответ.УстановитьКодСостояния(200);
Исключение
Ответ.УстановитьКодСостояния(500);
КонецПопытки;
КонецФункции
Проблема: если через месяц потребуется добавить проверку остатков перед записью — код придётся менять в каждом HTTP-сервисе. Если нужно логировать все изменения — снова правка в N местах. Провайдер решает это централизованно.
Архитектура провайдера: три слоя вместо одного
Автор статьи предлагает разбить провайдер на три логических блока, каждый со своей зоной ответственности:
Слой 1: Контракт (интерфейс)
Определяет, какие данные провайдер ожидает на входе и что гарантирует на выходе. Реализуется через структуру с типизированными полями:
Функция КонтрактПровайдера() Экспорт
Контракт = Новый Структура;
Контракт.Вставить("ВходныеПараметры", Новый Структура("ДатаНачала, ДатаОкончания, Склад", Неопределено, Неопределено, Неопределено));
Контракт.Вставить("ВыходныеДанные", Новый ОписаниеТипов("ТаблицаЗначений"));
Контракт.Вставить("ПраваДоступа", "Чтение");
Возврат Контракт;
КонецФункции
Предупреждение: Никогда не используйтеНеопределенокак значение по умолчанию для обязательных параметров — это маскирует ошибки на этапе компиляции. Лучше явно проверять черезЗаполнено()внутри провайдера.
Слой 2: Исполнитель (ядро)
Содержит реальную логику работы с данными. Здесь выполняются запросы, создаются документы, обрабатываются ошибки. Важное отличие от «лобового» подхода — исполнитель не знает про HTTP, он работает только с переданными параметрами:
Функция ВыполнитьЗапросОстатков(Параметры) Экспорт
Результат = Новый ТаблицаЗначений;
Результат.Колонки.Добавить("Номенклатура", Новый ОписаниеТипов("СправочникСсылка.Номенклатура"));
Результат.Колонки.Добавить("Количество", Новый ОписаниеТипов("Число"));
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| Остатки.Номенклатура КАК Номенклатура,
| Остатки.КоличествоОстаток КАК Количество
|ИЗ
| РегистрНакопления.ОстаткиТоваров.Остатки(, &ДатаМомента) КАК Остатки
|ГДЕ
| Остатки.Склад = &Склад";
Запрос.УстановитьПараметр("ДатаМомента", Параметры.ДатаОкончания);
Запрос.УстановитьПараметр("Склад", Параметры.Склад);
Результат = Запрос.Выполнить().Выгрузить();
Возврат Результат;
КонецФункции
Слой 3: Адаптер (трансляция в HTTP)
Единственный слой, который знает про HTTP-контекст. Преобразует JSON/XML в структуру параметров, вызывает исполнителя, сериализует ответ:
Функция ОбработатьЗапросОстатков(ЗапросHTTP, ОтветHTTP) Экспорт
Параметры = Новый Структура;
Параметры.Вставить("ДатаНачала", Неопределено);
Параметры.Вставить("ДатаОкончания", ТекущаяДата());
Параметры.Вставить("Склад", Неопределено);
// Заполняем из тела запроса
Тело = ЗапросHTTP.ПолучитьТелоКакСтроку();
Если ПустаяСтрока(Тело) Тогда
ОтветHTTP.УстановитьКодСостояния(400);
ОтветHTTP.УстановитьТелоИзСтроки("Не передан JSON");
Возврат;
КонецЕсли;
ЧтениеJSON = Новый ЧтениеJSON;
ЧтениеJSON.УстановитьСтроку(Тело);
Попытка
Данные = ПрочитатьJSON(ЧтениеJSON);
Исключение
ОтветHTTP.УстановитьКодСостояния(400);
ОтветHTTP.УстановитьТелоИзСтроки("Ошибка разбора JSON: " + КраткоеПредставлениеОшибки(ИнформацияОбОшибке()));
Возврат;
КонецПопытки;
Если Данные.Свойство("Дата") Тогда
Параметры.ДатаОкончания = Данные.Дата;
КонецЕсли;
Если Данные.Свойство("Склад") Тогда
Параметры.Склад = Справочники.Склады.НайтиПоКоду(Данные.Склад);
КонецЕсли;
Результат = ВыполнитьЗапросОстатков(Параметры);
// Сериализация ответа
ЗаписьJSON = Новый ЗаписьJSON;
Ответ = Новый Массив;
Для Каждого Строка Из Результат Цикл
Ответ.Добавить(Новый Структура("Номенклатура, Количество", Строка.Номенклатура.Наименование, Строка.Количество));
КонецЦикла;
ЗаписьJSON.УстановитьСтроку();
ЗаписатьJSON(ЗаписьJSON, Ответ);
ОтветHTTP.УстановитьКодСостояния(200);
ОтветHTTP.УстановитьТелоИзСтроки(ЗаписьJSON.Закрыть(), "application/json");
КонецФункции
Нетривиальная ловушка: транзакции и блокировки
Самая частая ошибка при реализации провайдера — попытка управлять транзакцией внутри исполнителя. Представьте: HTTP-сервис вызывает два провайдера последовательно — сначала запись документа, потом обновление остатков. Если каждый провайдер открывает свою транзакцию, при сбое во втором первый уже зафиксирован. Результат — «битые» данные.
Правило: Транзакция должна открываться ровно один раз — в адаптере HTTP-сервиса. Исполнитель провайдера работает в контексте уже открытой транзакции и никогда не вызывает НачатьТранзакцию() сам.
Реализация безопасного вызова
Функция ОбработатьКомплексныйЗапрос(ЗапросHTTP, ОтветHTTP) Экспорт
НачатьТранзакцию();
Попытка
// Вызов первого провайдера
Провайдер1.ВыполнитьЗаписьДокумента(ПараметрыДокумента);
// Вызов второго провайдера
Провайдер2.ОбновитьОстатки(ПараметрыОстатков);
ЗафиксироватьТранзакцию();
ОтветHTTP.УстановитьКодСостояния(200);
Исключение
ОтменитьТранзакцию();
ОтветHTTP.УстановитьКодСостояния(500);
ОтветHTTP.УстановитьТелоИзСтроки(КраткоеПредставлениеОшибки(ИнформацияОбОшибке()));
КонецПопытки;
КонецФункции
Типичные ошибки и чек-лист
Ошибка 1: Смешивание логики доступа и представления
Провайдер не должен возвращать готовый JSON — его задача вернуть данные в виде таблицы значений или структуры. Сериализация — дело адаптера.
Ошибка 2: Игнорирование прав доступа
Если провайдер вызывается из HTTP-сервиса, который работает в привилегированном режиме, проверка прав должна быть явной внутри исполнителя:
Если Не Пользователи.ЕстьПраво("Чтение", Метаданные.РегистрыНакопления.ОстаткиТоваров) Тогда
ВызватьИсключение "Недостаточно прав для чтения остатков";
КонецЕсли;
Чек-лист «Провайдер данных»
- Контракт описан — структура входных/выходных параметров зафиксирована в отдельной функции.
- Транзакции только снаружи — провайдер не вызывает
НачатьТранзакцию(). - Ошибки не проглатываются — каждое исключение логируется через
ЗаписьЖурналаРегистрации. - Проверка прав — если HTTP-сервис привилегированный, права проверяются внутри провайдера.
- Кэширование — если данные редко меняются, используйте
ХранилищеНастроекдля кэша на 5-10 минут.
Что делать прямо сейчас
- Проверьте свои HTTP-сервисы: есть ли в них код, который напрямую обращается к регистрам или документам? Если да — вынесите его в отдельный общий модуль с префиксом
ПровайдерДанных. - Добавьте в каждый провайдер функцию
КонтрактПровайдера()— это дисциплинирует и упростит тестирование. - Убедитесь, что ни один провайдер не содержит вызовов
НачатьТранзакцию()— это ответственность вызывающего кода. - Настройте мониторинг: добавьте в адаптер замер времени выполнения каждого провайдера через
ТекущаяУниверсальнаяДатаВМиллисекундах().
Итог: Провайдер данных — это не «ещё одна абстракция», а инструмент, который превращает хаотичный код HTTP-сервисов в предсказуемую систему. Без него вы обречены на дублирование и ошибки при каждом изменении бизнес-логики.
Попробовать НОПик →