Как правильно выполнять интерактивные действия и отправку HTTP-запросов после проведения документа в 1С без блокировок

Программист 1С v8.3 (Управляемые формы) IT и автоматизация бизнеса
← На главную

В процессе разработки в среде «1С:Предприятие 8» часто возникает задача: после проведения документа запросить у пользователя дополнительную информацию (например, комментарий) или отправить данные во внешнюю систему по протоколу HTTP. Однако стандартная попытка решить эту задачу «в лоб» — вызвав окно ввода или выполнив HTTP-запрос непосредственно внутри транзакции записи/проведения — приводит к серьезным проблемам с производительностью системы.

Разберем детально, почему так происходит. Когда документ проводится, платформа открывает транзакцию базы данных. В этот момент накладываются транзакционные блокировки на таблицы регистров и самого документа. Если внутри этой транзакции интерфейс ожидает ввода от пользователя или отправляет HTTP-запрос (который может зависнуть из-за сетевых сбоев), транзакция остается открытой. Все остальные пользователи, пытающиеся записать документы, затрагивающие те же таблицы регистров, выстраиваются в очередь и в итоге получают ошибку ожидания блокировки СУБД.

В этой статье мы подробно рассмотрим архитектурно правильные паттерны программирования в 1С, которые позволяют полностью исключить зависание интерфейса и гарантируют стабильность базы данных.

Методика 1. Организация интерактивного ввода комментария (До начала транзакции)

Запомните главное правило: любой интерактивный ввод со стороны пользователя должен происходить строго за рамками транзакции проведения. Если нам нужно провести документ и записать в него комментарий, мы должны получить этот комментарий на клиенте еще до того, как начнется запись в базу данных.

Проанализируем, как реализовать этот алгоритм в форме элемента документа. Нам понадобятся события ПередЗаписью на клиенте и обработчик завершения ввода строки.

Рассмотрим пошагово пример реализации в модуле формы документа:


&НаКлиенте
Процедура ПередЗаписью(Отказ, ПараметрыЗаписи)
	
	// Проверяем, что документ именно проводится
	Если ПараметрыЗаписи.РежимЗаписи = РежимЗаписиДокумента.Проведение Тогда
		
		// Если комментарий еще не заполнен и мы не передали признак обхода проверки
		Если ПустаяСтрока(Объект.Комментарий) И Не (ПараметрыЗаписи.Свойство("КомментарийЗаполнен") И ПараметрыЗаписи.КомментарийЗаполнен) Тогда
			
			// Отменяем стандартную запись, чтобы не держать транзакцию
			Отказ = Истина;
			
			// Запрашиваем комментарий у пользователя асинхронно
			ПараметрыОписания = Новый Структура("ПараметрыЗаписи", ПараметрыЗаписи);
			ОписаниеОповещения = Новый ОписаниеОповещения("ПослеВводаКомментария", ЭтотОбъект, ПараметрыОписания);
			
			ПоказатьВводСтроки(ОписаниеОповещения, "", "Введите обязательный комментарий для проведения:", 150);
			
		КонецЕсли;
		
	КонецЕсли;

КонецПроцедуры

&НаКлиенте
Процедура ПослеВводаКомментария(ВведеннаяСтрока, ДополнительныеПараметры) Экспорт
	
	// Если пользователь закрыл окно или ввел пустую строку - прерываем процесс
	Если ВведеннаяСтрока = Неопределено Или ПустаяСтрока(СокрЛП(ВведеннаяСтрока)) Тогда
		ПоказатьПредупреждение(, "Проведение отменено: необходимо заполнить комментарий.");
		Возврат;
	КонецЕсли;
	
	// Заполняем реквизит объекта на клиенте
	Объект.Комментарий = СокрЛП(ВведеннаяСтрока);
	
	// Модифицируем параметры записи, чтобы избежать бесконечного цикла проверки
	ПараметрыЗаписи = ДополнительныеПараметры.ПараметрыЗаписи;
	ПараметрыЗаписи.Вставить("КомментарийЗаполнен", Истина);
	
	// Повторно инициируем запись объекта. Теперь проверка будет пройдена мгновенно
	Записать(ПараметрыЗаписи);
	
КонецПроцедуры

В этом сценарии транзакция проведения запускается только тогда, когда комментарий уже введен пользователем и подставлен в реквизит объекта. База данных не блокируется ни на одну лишнюю миллисекунду.

Решение для формы списка (Проведение из списка)

Если пользователи часто проводят документы из формы списка (например, через контекстное меню или кнопку «Провести»), стандартные события формы элемента не отрабатывают. Чтобы решить эту проблему, выполним следующие шаги:

  1. Отключим стандартную кнопку «Провести» на командной панели динамического списка.
  2. Добавим собственную кнопку-команду «Провести с комментарием».
  3. В обработчике команды реализуем проверку выделенных строк.

Посмотрим на пример кода для модуля формы списка:


&НаКлиенте
Процедура ПровестиСКомментарием(Команда)
	
	ВыделенныеСтроки = Элементы.Список.ВыделенныеСтроки;
	Если ВыделенныеСтроки.Количество() = 0 Тогда
		Возврат;
	КонецЕсли;
	
	// Если выделено много строк, можем либо запретить операцию, либо открыть форму групповой обработки
	Если ВыделенныеСтроки.Количество() > 1 Тогда
		ПоказатьПредупреждение(, "Пожалуйста, выберите только один документ для проведения.");
		Возврат;
	КонецЕсли;
	
	ТекущийДокумент = ВыделенныеСтроки[0];
	
	// Описание оповещения для обработки введенного комментария
	ПараметрыОповещения = Новый Структура("ДокументСсылка", ТекущийДокумент);
	ОписаниеОповещения = Новый ОписаниеОповещения("ПослеВводаКомментарияСписок", ЭтотОбъект, ПараметрыОповещения);
	
	ПоказатьВводСтроки(ОписаниеОповещения, "", "Укажите комментарий для проведения документа:");
	
КонецПроцедуры

&НаКлиенте
Процедура ПослеВводаКомментарияСписок(ВведеннаяСтрока, ДополнительныеПараметры) Экспорт
	
	Если ВведеннаяСтрока = Неопределено Или ПустаяСтрока(СокрЛП(ВведеннаяСтрока)) Тогда
		Возврат;
	КонецЕсли;
	
	// Передаем проведение на сервер
	ПровестиДокументНаСервере(ДополнительныеПараметры.ДокументСсылка, СокрЛП(ВведеннаяСтрока));
	
	// Обновляем динамический список
	Элементы.Список.Обновить();
	
КонецПроцедуры

&НаСервереБезКонтекста
Процедура ПровестиДокументНаСервере(СсылкаНаДокумент, Комментарий)
	
	ДокументОбъект = СсылкаНаДокумент.ПолучитьОбъект();
	ДокументОбъект.Комментарий = Комментарий;
	
	// Для того чтобы при записи не происходило повторных интерактивных проверок в подписках
	ДокументОбъект.ДополнительныеСвойства.Вставить("ИнтерактивноеПроведение", Ложь);
	
	Попытка
		ДокументОбъект.Записать(РежимЗаписиДокумента.Проведение);
	Исключение
		ВызватьИсключение ИнформацияОбОшибке().Описание;
	КонецПопытки;
	
КонецПроцедуры

Методика 2. Выполнение действий строго после записи (Обработчик ожидания)

Что делать, если нам нужно выполнить интерактивное действие (например, открыть форму связанного объекта или показать детальный отчет) строго после того, как транзакция проведения успешно завершилась?

Для этого идеально подходит событие формы ПослеЗаписи в связке с подключением однократного обработчика ожидания. Событие ПослеЗаписи выполняется на клиенте, когда транзакция в БД уже полностью зафиксирована и все блокировки сняты. Подключение обработчика ожидания с минимальным интервалом времени (0.1 секунды) позволяет платформе завершить текущий поток выполнения интерфейса, обновить форму и запустить целевое действие в абсолютно чистом контексте.

Рассмотрим пример реализации:


&НаКлиенте
Процедура ПослеЗаписи(ПараметрыЗаписи)
	
	// Проверяем, что была выполнена именно процедура проведения
	Если ПараметрыЗаписи.РежимЗаписи = РежимЗаписиДокумента.Проведение Тогда
		
		// Подключаем однократный обработчик с интервалом 0.1 сек
		ПодключитьОбработчикОжидания("ВыполнитьДействияПослеПроведения", 0.1, Истина);
		
	КонецЕсли;
	
КонецПроцедуры

&НаКлиенте
Процедура ВыполнитьДействияПослеПроведения()
	
	// Данная процедура выполняется вне транзакции записи
	// Здесь можно безбоязненно открывать формы, выдавать диалоги или инициировать отправку данных
	ПоказатьОповещениеПользователя("Успешно проведен", , "Документ проведен. Начинаем подготовку к отправке данных...");
	
	// Пример вызова тяжелой отправки
	ОтправитьДанныеНаСерверПриложений();
	
КонецПроцедуры

Методика 3. Гарантированная отправка HTTP-запросов (Паттерн «Очередь задач»)

Выполнение HTTP-запросов напрямую из клиентского сеанса (даже через обработчик ожидания) имеет существенный недостаток. Если внешний сервер недоступен, завис или долго обрабатывает запрос, интерфейс пользователя «замерзнет» на время ожидания ответа (таймаут соединения обычно составляет от 10 до 30 секунд). Пользователь решит, что программа зависла, и попытается аварийно завершить работу 1С.

Архитектурно верное решение этой проблемы — использование паттерна «Очередь задач» (или Outbox Pattern).

Суть паттерна состоит в разделении процесса на два независимых шага:

  1. Быстрая регистрация задачи: В момент проведения документа внутри транзакции мы всего лишь делаем запись в специальный технический регистр сведений (например, ОчередьСообщенийИнтеграции). Запись в регистр выполняется мгновенно (за доли миллисекунды) и не создает блокировок.
  2. Асинхронная обработка: Специальное регламентное задание на сервере с определенной периодичностью (например, каждые 10–30 секунд) считывает новые записи из этого регистра, осуществляет фактические HTTP-вызовы в фоновом режиме и фиксирует результат отправки.

Давайте пошагово реализуем этот механизм.

Шаг 1. Создание структуры данных

Создадим регистр сведений ОчередьСообщенийИнтеграции со следующей структурой:

Шаг 2. Регистрация задачи в транзакции проведения

В модуле объекта документа в процедуре ОбработкаПроведения пишем быстрый код регистрации задачи:


Процедура ОбработкаПроведения(Отказ, РежимПроведения)
	
	// Основная логика проведения (проведение по регистрам учета)
	// ...
	
	// Регистрируем задачу на отправку данных по HTTP
	ЗарегистрироватьОтправкуДанных();
	
КонецПроцедуры

Процедура ЗарегистрироватьОтправкуДанных()
	
	// Подготавливаем данные для отправки во внешнюю систему
	СтруктураДанных = Новый Структура;
	СтруктураДанных.Вставить("НомерДокумента", ЭтотОбъект.Номер);
	СтруктураДанных.Вставить("ДатаДокумента", ЭтотОбъект.Дата);
	СтруктураДанных.Вставить("СуммаДокумента", ЭтотОбъект.СуммаДокумента);
	СтруктураДанных.Вставить("Комментарий", ЭтотОбъект.Комментарий);
	
	// Создаем менеджер записи регистра сведений
	МенеджерЗаписи = РегистрыСведений.ОчередьСообщенийИнтеграции.СоздатьМенеджерЗаписи();
	МенеджерЗаписи.Объект = ЭтотОбъект.Ссылка;
	МенеджерЗаписи.ИдентификаторЗадачи = Новый УникальныйИдентификатор();
	
	МенеджерЗаписи.СтатусОтправки = Перечисления.СтатусыОтправкиСообщений.КОтправке;
	МенеджерЗаписи.ДатаСоздания = ТекущаяДатаСеанса();
	МенеджерЗаписи.КоличествоПопыток = 0;
	МенеджерЗаписи.ДанныеДляОтправки = Новый ХранилищеЗначения(СтруктураДанных);
	
	// Быстрая запись в СУБД в рамках текущей транзакции
	МенеджерЗаписи.Записать(Истина);
	
КонецПроцедуры

Шаг 3. Реализация асинхронного обработчика очереди (Регламентное задание)

Создадим общий модуль ИнтеграцияВызовСервера (свойства: Сервер, Вызов сервера) и пропишем в нем процедуру, которая будет запускаться по расписанию регламентным заданием:


Процедура ОбработатьОчередьОтправкиHTTP() Экспорт
	
	// Выбираем все сообщения со статусом "КОтправке" или "Ошибка" (если попыток меньше 3)
	Запрос = Новый Запрос;
	Запрос.Текст = 
		"ВЫБРАТЬ
		|	Очередь.Объект КАК Объект,
		|	Очередь.ИдентификаторЗадачи КАК ИдентификаторЗадачи,
		|	Очередь.КоличествоПопыток КАК КоличествоПопыток,
		|	Очередь.ДанныеДляОтправки КАК ДанныеДляОтправки
		|ИЗ
		|	РегистрСведений.ОчередьСообщенийИнтеграции КАК Очередь
		|ГДЕ
		|	Очередь.СтатусОтправки = ЗНАЧЕНИЕ(Перечисление.СтатусыОтправкиСообщений.КОтправке)
		|	ИЛИ (Очередь.СтатусОтправки = ЗНАЧЕНИЕ(Перечисление.СтатусыОтправкиСообщений.Ошибка) И Очередь.КоличествоПопыток < 3)";
		
	РезультатЗапроса = Запрос.Выполнить();
	Если РезультатЗапроса.Пустой() Тогда
		Возврат;
	КонецЕсли;
	
	Выборка = РезультатЗапроса.Выбрать();
	Пока Выборка.Следующий() Цикл
		
		ДанныеДляОтправки = Выборка.ДанныеДляОтправки.Получить();
		Если ДанныеДляОтправки = Неопределено Тогда
			Продолжить;
		КонецЕсли;
		
		// Начинаем отправку
		ТекстОшибки = "";
		Успешно = ОтправитьЗапросНаВнешнийСервер(ДанныеДляОтправки, ТекстОшибки);
		
		// Обновляем статус задачи в регистре
		ОбновитьСтатусЗадачи(Выборка.Объект, Выборка.ИдентификаторЗадачи, Успешно, ТекстОшибки, Выборка.КоличествоПопыток);
		
	КонецЦикла;
	
КонецПроцедуры

Функция ОтправитьЗапросНаВнешнийСервер(ДанныеДляОтправки, ТекстОшибки)
	
	// Настройка HTTP-соединения
	СерверИсточник = "api.external-system.com";
	НТТРСоединение = Новый HTTPСоединение(СерверИсточник, 443, , , , 10, Новый ЗащищенноеСоединениеOpenSSL());
	
	// Формируем JSON
	ЗаписьJSON = Новый ЗаписьJSON;
	ЗаписьJSON.УстановитьСтроку();
	ЗаписатьJSON(ЗаписьJSON, ДанныеДляОтправки);
	СтрокаЗапроса = ЗаписьJSON.Закрыть();
	
	НТТРЗапрос = Новый HTTPЗапрос("/v1/documents");
	НТТРЗапрос.УстановитьТелоИзСтроки(СтрокаЗапроса, КодировкаТекста.UTF8, ИспользованиеByteOrderMark.НеИспользовать);
	НТТРЗапрос.Заголовки.Вставить("Content-Type", "application/json");
	
	Попытка
		HTTPОтвет = НТТРСоединение.ОтправитьДляОбработки(НТТРЗапрос);
		Если HTTPОтвет.КодСостояния = 200 Или HTTPОтвет.КодСостояния = 201 Тогда
			Возврат Истина;
		Иначе
			ТекстОшибки = "Ошибка сервера. Код ответа: " + HTTPОтвет.КодСостояния + ". Ответ: " + HTTPОтвет.ПолучитьТелоКакСтроку();
			Возврат Ложь;
		КонецЕсли;
	Исключение
		ИнформацияОбОшибке = ИнформацияОбОшибке();
		ТекстОшибки = ПодробноеПредставлениеОшибки(ИнформацияОбОшибке);
		Возврат Ложь;
	КонецПопытки;
	
КонецФункции

Процедура ОбновитьСтатусЗадачи(Объект, ИдентификаторЗадачи, Успешно, ТекстОшибки, ТекущиеПопытки)
	
	МенеджерЗаписи = РегистрыСведений.ОчередьСообщенийИнтеграции.СоздатьМенеджерЗаписи();
	МенеджерЗаписи.Объект = Объект;
	МенеджерЗаписи.ИдентификаторЗадачи = ИдентификаторЗадачи;
	
	МенеджерЗаписи.Прочитать();
	Если МенеджерЗаписи.Выбран() Тогда
		Если Успешно Тогда
			МенеджерЗаписи.СтатусОтправки = Перечисления.СтатусыОтправкиСообщений.Отправлено;
			МенеджерЗаписи.ТекстОшибки = "";
		Иначе
			МенеджерЗаписи.СтатусОтправки = Перечисления.СтатусыОтправкиСообщений.Ошибка;
			МенеджерЗаписи.ТекстОшибки = ТекстОшибки;
			МенеджерЗаписи.КоличествоПопыток = ТекущиеПопытки + 1;
		КонецЕсли;
		МенеджерЗаписи.Записать();
	КонецЕсли;
	
КонецПроцедуры

Какие архитектурные преимущества мы получили благодаря паттерну «Очередь задач»?

Использование механизмов БСП (Библиотека стандартных подсистем)

Если ваша конфигурация разработана на базе типовых решений 1С (УТ, ERP, КА, УНФ), вы можете не писать регламентные задания и обработку очереди с нуля, а воспользоваться готовыми инструментами БСП:

Применяя данные методики на практике, мы обеспечиваем высокую скорость проведения документов, исключаем появление взаимных блокировок СУБД и гарантируем сохранность и доставку всех передаваемых во внешние системы данных.

← На главную