Начало: Слои и модуль Сервиса, Валидация данных
Поговорим о модуле РаботаСЗадачамиРеализация
Цель модуля - инкапсуляция логики доступа к данным и предоставление единого интерфейса для работы с ними. Считаю, что внедренные подсистемы, являются частью данных, поэтому и доступ к их методам можно включать в этот интерфейс при необходимости.
Требования: никакой бизнес логики, на выходе максимально сырые данные.
Правильно разработанный модуль позволит переиспользовать его любым сервисом, не только http.
Подходы к доступу к данным
-
Объектная модель – только примитивы
-
Табличная модель – основной инструмент
-
Через программный интерфейс встроенных подсистем, как особенность среды
Объектная модель
Использую самую примитивную:
Функция ЗадачаСотрудникаПоСсылке(Значение) Экспорт
Возврат Документы.ЗадачаСотрудника.ПолучитьСсылку(Значение);
КонецФункции
Функция ЗадачаСотрудникаПоНомеру(Значение) Экспорт
Возврат Документы.ЗадачаСотрудника.НайтиПоНомеру(Значение);
КонецФункции
Функция ПользовательПустаяСсылка() Экспорт
Возврат Справочники.Пользователи.ПустаяСсылка()
КонецФункции
Функция ОбъектУдален(Значение) Экспорт
Возврат Значение.ПометкаУдаления;
КонецФункции
Функция АдресБазыВИнтернете() Экспорт
Возврат Константы.АдресПубликацииИнформационнойБазыВИнтернете.Получить();
КонецФункции
Табличная модель
Основной инструмент, но объем выборки должен быть ограничен (фильтры, пагинация). А на выходе только: выгрузки, выборки или агрегированные данные, никаких преобразований.
Пример выгрузки списка задач. Поддерживаю два способа пагинации: страничный и курсором. И фильтр.
Функция СписокЗадачВыгрузка(НастройкиПолученияЗадач, ФильтрЗадач, ЗадачНаСтраницу) Экспорт
Функция СписокЗадачВыгрузка(НастройкиПолученияЗадач, ФильтрЗадач, ЗадачНаСтраницу) Экспорт
// на основе настроек формируем запрос-пакет для реализации пагинации и накладываем фильтр
Запрос = НовыйЗапросПакетСпискаЗадачСПагинацией(НастройкиПолученияЗадач, ФильтрЗадач, ЗадачНаСтраницу);
// к пакету добавляем запрос-выборку с необходимым набором данных
Запрос.Текст = Запрос.Текст + ";" + Символы.ПС +
"ВЫБРАТЬ
| ЗадачаСотрудника.Ссылка КАК Ссылка,
| ПРЕДСТАВЛЕНИЕ(УНИКАЛЬНЫЙИДЕНТИФИКАТОР(ЗадачаСотрудника.Ссылка)) КАК УникальныйИдентификатор,
| ЗадачаСотрудника.Номер КАК Номер,
| ЗадачаСотрудника.Содержание КАК Содержание,
| ЗадачаСотрудника.Приоритет КАК Приоритет,
| ПРЕДСТАВЛЕНИЕ(УНИКАЛЬНЫЙИДЕНТИФИКАТОР(ЗадачаСотрудника.Исполнитель)) КАК Исполнитель,
| ПРЕДСТАВЛЕНИЕ(УНИКАЛЬНЫЙИДЕНТИФИКАТОР(ЗадачаСотрудника.Автор)) КАК Автор
|ИЗ
| Документ.ЗадачаСотрудника КАК ЗадачаСотрудника
| ВНУТРЕННЕЕ СОЕДИНЕНИЕ ВТ_ДанныеДляВозврата КАК ВТ_ДанныеДляВозврата
| ПО (ВТ_ДанныеДляВозврата.Ссылка = ЗадачаСотрудника.Ссылка)";
Возврат Запрос.Выполнить().Выгрузить()
КонецФункции
Конструкторы для настроек (и необходимые константы), которые принимает метод, также являются частью интерфейса. Настройки заполняются извне, сервисом.
Функция НовыеНастройкиПолученияЗадач() Экспорт
Результат = Новый Структура;
Результат.Вставить("СпособПагинации");
Результат.Вставить("НомерСтраницы");
Результат.Вставить("Лимит");
Результат.Вставить("ДоКурсора");
Результат.Вставить("ПослеКурсора");
Возврат Результат;
КонецФункции
Функция НовыйФильтрЗадач() Экспорт
Результат = Новый Структура;
Результат.Вставить("ДатаНачала");
Результат.Вставить("ДатаОкончания");
Результат.Вставить("ИдентификаторИсполнителя");
Результат.Вставить("ИдентификаторАвтора");
Возврат Результат;
КонецФункции
// константы для настроек
Функция СпособПагинацииСтраницы() Экспорт
Возврат "Страницы"
КонецФункции
Функция СпособПагинацииКурсор() Экспорт
Возврат "Курсор"
КонецФункции
Простой пример создания запроса для реализации пагинации курсором и страничной пагинации, выбор осуществляется через переданные настройки. На сформированный запрос накладываем фильтр.
Функция НовыйЗапросПакетСпискаЗадачСПагинацией(Пагинация, Фильтр, ЗадачНаСтраницу)
Функция НовыйЗапросПакетСпискаЗадачСПагинацией(Пагинация, Фильтр, ЗадачНаСтраницу)
// Подготавливаем запрос-пакет с минимальным набором данных - ссылка
// - применяем фильтр на оператор фильтра схемы
Запрос = Новый Запрос;
СхемаЗапроса = Новый СхемаЗапроса;
ПорядокПоВозрастанию = НаправлениеПорядкаСхемыЗапроса.ПоВозрастанию;
ПорядокПоУбыванию = НаправлениеПорядкаСхемыЗапроса.ПоУбыванию;
ОператорДляФильтра = Неопределено;
Если Пагинация.СпособПагинации = СпособПагинацииКурсор() Тогда
// Пагинация курсором, порядок по ссылке
// Направление порядка определяется, наличием параметра after - прямой, или его отсутствием - по убыванию
// Оператор схемы также используется для фильтра
ПоВозрастанию = ЗначениеЗаполнено(Пагинация.ПослеКурсора);
Отбор = СтрШаблон("УНИКАЛЬНЫЙИДЕНТИФИКАТОР(ЗадачаСотрудника.Ссылка) %1= &Курсор", ?(ПоВозрастанию, ">", "<"));
Курсор = ?(ПоВозрастанию, Пагинация.ПослеКурсора, Пагинация.ДоКурсора);
Направление = ?(ПоВозрастанию, ПорядокПоВозрастанию, ПорядокПоУбыванию);
Запрос.Текст =
"ВЫБРАТЬ ПЕРВЫЕ 20
| ЗадачаСотрудника.Ссылка КАК Ссылка
|ПОМЕСТИТЬ ВТ_ДанныеДляВозврата
|ИЗ
| Документ.ЗадачаСотрудника КАК ЗадачаСотрудника";
СхемаЗапроса.УстановитьТекстЗапроса(Запрос.Текст);
ЗапросПакет = СхемаЗапроса.ПакетЗапросов[0];
ОператорДляФильтра = ЗапросПакет.Операторы[0];
ОператорДляФильтра.КоличествоПолучаемыхЗаписей = Пагинация.Лимит;
ОператорДляФильтра.Отбор.Добавить(Отбор);
Запрос.УстановитьПараметр("Курсор", Новый УникальныйИдентификатор(Курсор));
ЭлементПорядка = ЗапросПакет.Порядок.Добавить("ЗадачаСотрудника.Ссылка");
ЭлементПорядка.Направление = Направление;
ИначеЕсли Пагинация.СпособПагинации = СпособПагинацииСтраницы() Тогда
// Пагинация страницами, прямой порядок по ссылке
// Выборка из двух вложенных запросов
// Первый вложенный - выбирает limit * page записей - прямой порядок, содержит оператор для фильтра
// Второй вложенный - выбирает limit записей - обратный порядок
// Выборка - возвращает прямой порядок
Запрос.Текст =
"ВЫБРАТЬ ПЕРВЫЕ 20
| Порция.Ссылка КАК Ссылка
|ПОМЕСТИТЬ ВТ_ДанныеДляВозврата
|ИЗ
| (ВЫБРАТЬ ПЕРВЫЕ 20
| ПолнаяПорция.Ссылка КАК Ссылка
| ИЗ
| (ВЫБРАТЬ ПЕРВЫЕ 120
| ЗадачаСотрудника.Ссылка КАК Ссылка
| ИЗ
| Документ.ЗадачаСотрудника КАК ЗадачаСотрудника) КАК ПолнаяПорция) КАК Порция";
СхемаЗапроса.УстановитьТекстЗапроса(Запрос.Текст);
ЗапросПакет = СхемаЗапроса.ПакетЗапросов[0];
Прямой = ЗапросПакет.Операторы[0];
Прямой.КоличествоПолучаемыхЗаписей = Пагинация.Лимит;
ЭлементПорядка = ЗапросПакет.Порядок.Добавить("Ссылка");
ЭлементПорядка.Направление = ПорядокПоВозрастанию;
ЗапросОбратныйПорядок = Прямой.Источники[0].Источник.Запрос;
Обратный = ЗапросОбратныйПорядок.Операторы[0];
Обратный.КоличествоПолучаемыхЗаписей = ЗадачНаСтраницу;
ЭлементПорядка = ЗапросОбратныйПорядок.Порядок.Добавить("Ссылка");
ЭлементПорядка.Направление = ПорядокПоУбыванию;
ЗапросПрямойПорядок = Обратный.Источники[0].Источник.Запрос;
ОператорДляФильтра = ЗапросПрямойПорядок.Операторы[0];
ОператорДляФильтра.КоличествоПолучаемыхЗаписей = Пагинация.Лимит * Пагинация.НомерСтраницы;
ЭлементПорядка = ЗапросПрямойПорядок.Порядок.Добавить("Ссылка");
ЭлементПорядка.Направление = ПорядокПоВозрастанию;
КонецЕсли;
ПрименитьФильтрЗадач(Запрос, Фильтр, ОператорДляФильтра);
Запрос.Текст = СхемаЗапроса.ПолучитьТекстЗапроса();
Возврат Запрос
КонецФункции
Процедура ПрименитьФильтрЗадач(Запрос, Фильтр, Оператор)
Если Оператор = Неопределено Тогда Возврат КонецЕсли;
Если ЗначениеЗаполнено(Фильтр.ДатаНачала) Тогда
Оператор.Отбор.Добавить("ЗадачаСотрудника.Дата >= &ДатаНачала");
Запрос.УстановитьПараметр("ДатаНачала", Фильтр.ДатаНачала);
КонецЕсли;
Если ЗначениеЗаполнено(Фильтр.ДатаОкончания) Тогда
Оператор.Отбор.Добавить("ЗадачаСотрудника.Дата <= &ДатаОкончания");
Запрос.УстановитьПараметр("ДатаОкончания", Фильтр.ДатаОкончания);
КонецЕсли;
Если ЗначениеЗаполнено(Фильтр.ИдентификаторИсполнителя) Тогда
Оператор.Отбор.Добавить("УНИКАЛЬНЫЙИДЕНТИФИКАТОР(ЗадачаСотрудника.Исполнитель) = &ИдентификаторИсполнителя");
Запрос.УстановитьПараметр("ИдентификаторИсполнителя", Новый УникальныйИдентификатор(Фильтр.ИдентификаторИсполнителя));
КонецЕсли;
Если ЗначениеЗаполнено(Фильтр.ИдентификаторАвтора) Тогда
Оператор.Отбор.Добавить("УНИКАЛЬНЫЙИДЕНТИФИКАТОР(ЗадачаСотрудника.Автор) = &ИдентификаторАвтора");
Запрос.УстановитьПараметр("ИдентификаторАвтора", Новый УникальныйИдентификатор(Фильтр.ИдентификаторАвтора));
КонецЕсли;
КонецПроцедуры
Пример, получения агрегированных данных с наложением фильтра
Функция СписокЗадачКоличество(Фильтр) Экспорт
Функция СписокЗадачКоличество(Фильтр) Экспорт
Запрос = Новый Запрос;
СхемаЗапроса = Новый СхемаЗапроса;
Запрос.Текст =
"ВЫБРАТЬ
| КОЛИЧЕСТВО(РАЗЛИЧНЫЕ ЗадачаСотрудника.Ссылка) КАК КоличествоЗадач
|ИЗ
| Документ.ЗадачаСотрудника КАК ЗадачаСотрудника";
СхемаЗапроса.УстановитьТекстЗапроса(Запрос.Текст);
ЗапросПакет = СхемаЗапроса.ПакетЗапросов[0];
ОператорДляФильтра = ЗапросПакет.Операторы[0];
// Метод показан в предыдущем примере
ПрименитьФильтрЗадач(Запрос, Фильтр, ОператорДляФильтра);
Запрос.Текст = СхемаЗапроса.ПолучитьТекстЗапроса();
РезультатЗапроса = Запрос.Выполнить();
Если РезультатЗапроса.Пустой() Тогда Возврат 0 КонецЕсли;
Выборка = РезультатЗапроса.Выбрать();
Выборка.Следующий();
Возврат Выборка.КоличествоЗадач;
КонецФункции
Пример, выборки с ограничением
Функция ФайлыЗадачВыборка(Ссылки) Экспорт
Функция ФайлыЗадачВыборка(Ссылки) Экспорт
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| ЗадачаСотрудникаПрисоединенныеФайлы.Ссылка КАК Ссылка,
| ПРЕДСТАВЛЕНИЕ(УНИКАЛЬНЫЙИДЕНТИФИКАТОР(ЗадачаСотрудникаПрисоединенныеФайлы.Ссылка)) КАК УникальныйИдентификатор,
| ЗадачаСотрудникаПрисоединенныеФайлы.ВладелецФайла КАК ВладелецФайла,
| ЗадачаСотрудникаПрисоединенныеФайлы.Наименование КАК Наименование,
| ЗадачаСотрудникаПрисоединенныеФайлы.Расширение КАК Расширение
|ИЗ
| Справочник.ЗадачаСотрудникаПрисоединенныеФайлы КАК ЗадачаСотрудникаПрисоединенныеФайлы
|ГДЕ
| ЗадачаСотрудникаПрисоединенныеФайлы.ВладелецФайла В(&Ссылки)";
Запрос.УстановитьПараметр("Ссылки", Ссылки);
Возврат Запрос.Выполнить().Выбрать();
КонецФункции
Программный интерфейс встроенных подсистем
От простого – получение константы или вызова преобразований
#Область ОбращенияКПодсистемам
Функция ЗаблокированоОбновлением() Экспорт
Возврат ОбновлениеИнформационнойБазы.НеобходимоОбновлениеИнформационнойБазы()
КонецФункции
Функция ТаблицаЗначенийВМассив(Данные) Экспорт
Возврат ОбщегоНазначения.ТаблицаЗначенийВМассив(Данные)
КонецФункции
#КонецОбласти
Функция ОписаниеФайла(Файл) Экспорт
Возврат РаботаСФайлами.ДанныеФайла(Файл)
КонецФункции
Функция ВладелецФайла(Файл) Экспорт
Возврат ОбщегоНазначения.ЗначениеРеквизитаОбъекта(Файл, "ВладелецФайла")
КонецФункции
Процедура ЗаданиеПолучитьРеквизиты(Ссылка, Статус, Результат) Экспорт
Реквизиты = ОбщегоНазначения.ЗначенияРеквизитовОбъекта(Ссылка, "Статус,Результат");
Статус = Реквизиты.Статус;
Если Статус = Перечисления.СтатусЗаданияОчередиОбработкиЗадач.Выполнено Тогда
Результат = Реквизиты.Результат.Получить();
КонецЕсли;
КонецПроцедуры
Как выносить логику
В начале обработчика каждого запроса модуля сервиса присутствуют проверки:
// Модуль сервиса РаботаСЗадачами
Функция ПолучитьЗадачи(Запрос)
Если РаботаСЗадачамиHTTP.ЗаблокированоОбновлением() Тогда Возврат ОтветЗаблокированоОбновлением() КонецЕсли;
Заголовки = Новый Соответствие;
Если РаботаСЗадачамиHTTP.ПревышеноЧислоЗапросов(Заголовки) Тогда
Возврат ОтветПревышеноЧислоЗапросов(Заголовки)
КонецЕсли;
// ...
Нас интересует реализация РаботаСЗадачамиHTTP.ПревышеноЧислоЗапросов(Заголовки)
Это защита нашего сервиса от большого числа запросов в какую-то определенную единицу времени. Мы фиксируем (в регистре сведений) время каждого запроса и при каждом очередном запросе сверяем с установленным допустимым максимумом. Если максимум не достигнут, то выполняем запрос, достигнут – формируем ответ с соответствующими заголовками и прерываем выполнение.
Максимально неверная реализация:
// ОМ РаботаСЗадачамиHTTP
Функция ПревышеноЧислоЗапросов(РезультатЗаголовки) Экспорт
Если РаботаСЗадачамиРеализация.ПревышеноЧислоЗапросов() Тогда
РезультатЗаголовки.Вставить("Retry-After", 20); // какое-то число сек. ожидания
Возврат Истина
КонецЕсли;
Возврат Ложь
КонецФункции
// ОМ РаботаСЗадачамиРеализация
Функция ПревышеноЧислоЗапросов() Экспорт
// какие-то магические числа
МаксимумЗапросов = 2;
ВремяОжидания = 20;
ТекущееВремя = ТекущаяУниверсальнаяДатаВМиллисекундах();
Граница = ТекущееВремя - ВремяОжидания * 1000;
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| КОЛИЧЕСТВО(СчетчикЗапросовHTTP.ВремяЗапросаВМиллисекундах) КАК КоличествоЗапросов
|ИЗ
| РегистрСведений.СчетчикЗапросовHTTP КАК СчетчикЗапросовHTTP
|ГДЕ
| СчетчикЗапросовHTTP.ВремяЗапросаВМиллисекундах > &ВремяЗапросаВМиллисекундах";
Запрос.УстановитьПараметр("ВремяЗапросаВМиллисекундах", Граница);
Выборка = Запрос.Выполнить().Выбрать();
Выборка.Следующий();
// логика, которую не контролирует контроллер
Если Выборка.КоличествоЗапросов > МаксимумЗапросов Тогда
Возврат Истина;
КонецЕсли;
// логика, которую не контролирует контроллер
Запись = РегистрыСведений.СчетчикЗапросовHTTP.СоздатьМенеджерЗаписи();
Запись.ВремяЗапросаВМиллисекундах = ТекущееВремя;
Запись.Записать();
Возврат Ложь;
КонецФункции
Корректируем:
- ВремяОжидания и МаксимумЗапросов, о них должен знать контроллер, оформим пока в виде функций-констант, потенциально это значения объектов-констант, чтобы иметь возможность менять динамически, не модифицируя код.
- Метод ОМ РаботаСЗадачамиРеализация ПревышеноЧислоЗапросов() разобъем на два метода: ЧислоЗапросов(Граница) – возвращает количество сделанных запросов от Граница и ЗафиксироватьВремяЗапроса(ТекущееВремя) – зафиксировать время очередного запроса.
Таким образом мы позволим Контроллеру взять контроль над ситуацией)
// ОМ РаботаСЗадачамиHTTP
Функция ПревышеноЧислоЗапросов(РезультатЗаголовки) Экспорт
ТекущееВремя = ТекущаяУниверсальнаяДатаВМиллисекундах();
ВремяОжидания = КонтрольЗапросовВремя();
Граница = ТекущееВремя - ВремяОжидания * 1000;
УстановитьПривилегированныйРежим(Истина);
Если РаботаСЗадачамиРеализация.ЧислоЗапросов(Граница) > КонтрольЗапросовМаксимум() Тогда
РезультатЗаголовки.Вставить("Retry-After", ВремяОжидания);
Возврат Истина
КонецЕсли;
РаботаСЗадачамиРеализация.ЗафиксироватьВремяЗапроса(ТекущееВремя);
Возврат Ложь
КонецФункции
Функция КонтрольЗапросовМаксимум()
Возврат 2
КонецФункции
Функция КонтрольЗапросовВремя()
Возврат 20
КонецФункции
// ОМ РаботаСЗадачамиРеализация
#Область УправлениеЧисломЗапросов
Функция ЧислоЗапросов(Граница) Экспорт
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| КОЛИЧЕСТВО(СчетчикЗапросовHTTP.ВремяЗапросаВМиллисекундах) КАК КоличествоЗапросов
|ИЗ
| РегистрСведений.СчетчикЗапросовHTTP КАК СчетчикЗапросовHTTP
|ГДЕ
| СчетчикЗапросовHTTP.ВремяЗапросаВМиллисекундах > &ВремяЗапросаВМиллисекундах";
Запрос.УстановитьПараметр("ВремяЗапросаВМиллисекундах", Граница);
Выборка = Запрос.Выполнить().Выбрать();
Выборка.Следующий();
Возврат Выборка.КоличествоЗапросов
КонецФункции
Процедура ЗафиксироватьВремяЗапроса(ТекущееВремя) Экспорт
Запись = РегистрыСведений.СчетчикЗапросовHTTP.СоздатьМенеджерЗаписи();
Запись.ВремяЗапросаВМиллисекундах = ТекущееВремя;
Запись.Записать();
КонецПроцедуры
#КонецОбласти
Новые объекты
При создании объектов удобно использовать метод Заполнить() объекта, который принимает параметры заполнения. Для подготовки параметров заполнения создаем специальный метод интерфейса провайдера данных, который возьмет их из менеджера объекта (если они там предложены) или подготовит с нуля.
Функция ЗадачаСотрудникаПараметрыЗаполнения() Экспорт
Функция ЗадачаСотрудникаПараметрыЗаполнения() Экспорт
Результат = Новый Структура;
Результат.Вставить("Дата", ТекущаяДатаСеанса());
Результат.Вставить("Проект", Справочники.Проекты.Основной);
Результат.Вставить("Приоритет", Перечисления.ПриоритетЗадачи.ПустаяСсылка());
Результат.Вставить("Содержание", "");
Результат.Вставить("Исполнитель", Справочники.Пользователи.ПустаяСсылка());
Результат.Вставить("Автор", Справочники.Пользователи.ПустаяСсылка());
Возврат Результат
КонецФункции
// и использовать для создания объекта
Процедура СоздатьЗадачуСотрудника(ПараметрыЗаполнения, РезультатНомер, Отказ) Экспорт
ДокОбъект = Документы.ЗадачаСотрудника.СоздатьДокумент();
ДокОбъект.Заполнить(ПараметрыЗаполнения);
Если Не ДокОбъект.ПроверитьЗаполнение() Тогда
Отказ = Истина;
Возврат
КонецЕсли;
ДокОбъект.Записать();
РезультатНомер = ДокОбъект.Номер
КонецПроцедуры
Модификация объектов
К счастью, сервис моего пет-проекта не содержим методов по изменению объектов. Если они были, то пришлось бы задуматься о следующем:
- Объект, который необходимо изменить, заблокирован
- Перед изменением целевой объект необходимо заблокировать
- Целевой объект изменен ранее, возможно, запрос устарел?
На этом тему закрываю. Модуль РаботаСЗадачамиРеализация – провайдер данных моего пет проекта не содержит бизнес логику, возвращает исключительно сырые данные и может быть переиспользован другими сервисами.

Критикуйте, делитесь, предлагайте.
Всем успехов в проектировании http-сервисов!