Начало: Слои и модуль Сервиса, Валидация данных
Поговорим о модуле РаботаСЗадачамиРеализация
Цель модуля - инкапсуляция логики доступа к данным и предоставление единого интерфейса для работы с ними. Считаю, что внедренные подсистемы, являются частью данных, поэтому и доступ к их методам можно включать в этот интерфейс при необходимости.
Требования: никакой бизнес логики, на выходе максимально сырые данные.
Правильно разработанный модуль позволит переиспользовать его любым сервисом, не только 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-сервисов!