При разработке механизмов автоматического заполнения реквизитов в регистрах накопления или сведений программисты часто прибегают к использованию подписок на события. Однако неправильный выбор события или логическая ошибка в коде могут привести к критической остановке системы с ошибкой «Переполнение стека встроенного языка на сервере». В данной статье мы подробно разберем, почему возникает эта ошибка, проанализируем типичные промахи новичков и выработаем оптимальный алгоритм работы с наборами записей.
Рассмотрим пример, с которым столкнулся автор темы. Задача заключалась в том, чтобы при записи движений в регистр накопления Продажи автоматически заполнять добавленный реквизит Контрагент, беря данные из документа-регистратора. Для этого была создана подписка на событие ПриЗаписи для объекта РегистрНакопленияНаборЗаписей.Продажи.
Исходный код обработчика выглядел следующим образом:
Процедура РГПродажиПриЗаписи(Источник, Отказ, Замещение) Экспорт
Для Каждого ТекДвижение Из Источник Цикл
ТекДвижение.Контрагент = ТекДвижение.Регистратор.Клиент;
КонецЦикла;
Источник.Записать();
КонецПроцедуры
Выясним причину падения системы. Когда платформа выполняет метод Записать() для набора записей, она инициирует событие ПриЗаписи. Внутри этого события программно снова вызывается Источник.Записать(). Это заставляет платформу повторно войти в ту же самую процедуру подписки. Процесс повторяется бесконечно: вызов — вход — вызов — вход. Каждый такой вход занимает определенный объем памяти в стеке вызовов сервера. Поскольку условие выхода из этой цепочки отсутствует, стек мгновенно переполняется, и сервер 1С аварийно прерывает выполнение операции.
Проанализируем механику событий. В 1С события ПередЗаписью и ПриЗаписи являются частью транзакции записи объекта. Если вы уже находитесь «внутри» процесса записи, вам категорически нельзя вызывать метод Записать() для того же самого объекта (Источник). Все изменения, которые вы вносите в поля Источник (в случае ПередЗаписью), будут автоматически сохранены платформой в базе данных по завершении работы обработчика.
В обсуждении справедливо замечено, что использование события ПриЗаписи для модификации данных самого объекта — это плохая практика. Разберем разницу:
Источник в этот момент, изменения останутся только в оперативной памяти и не попадут в таблицу, если не вызвать Записать() (что, как мы выяснили, ведет к рекурсии).Таким образом, для заполнения реквизитов всегда следует перевешивать подписку на событие ПередЗаписью.
Посмотрим на строку ТекДвижение.Контрагент = ТекДвижение.Регистратор.Клиент;. На первый взгляд она кажется безобидной, но проанализируем ситуацию с точки зрения СУБД.
Когда вы обращаетесь к ТекДвижение.Регистратор, платформа получает ссылку. Но как только вы пишете .Клиент, система вынуждена выполнить отдельный SQL-запрос к таблице документа-регистратора, чтобы получить значение этого поля. Если в наборе записей 1000 строк (например, крупный чек или отчет о розничных продажах), платформа может выполнить 1000 запросов к базе данных в цикле. Это создает колоссальную нагрузку на сервер.
Правильнее будет получить ссылку на регистратор один раз, прочитать нужные данные и затем использовать их для всех строк набора.
Рассмотрим, как должен выглядеть профессиональный код подписки на событие ПередЗаписью для набора записей регистра накопления. Мы внедрим проверку на пустой набор и правильное получение данных регистратора.
Процедура РГПродажиПередЗаписью(Источник, Отказ, Замещение) Экспорт
// 1. Проверка на загрузку данных (важно для РИБ и обменов)
Если Источник.ОбменДанными.Загрузка Тогда
Возврат;
КонецЕсли;
// 2. Проверка наличия записей
Если Источник.Количество() = 0 Тогда
Возврат;
КонецЕсли;
// 3. Получаем регистратор из первой записи (он один для всего набора)
РегистраторСсылка = Источник[0].Регистратор;
// 4. Проверяем тип регистратора, если подписка назначена на несколько типов
// и получаем данные одним запросом или через точку ОДИН РАЗ
Если ТипЗнч(РегистраторСсылка) = Тип("ДокументСсылка.ЧекККМ") Тогда
// Используем ОбщегоНазначения.ЗначениеРеквизитаОбъекта для оптимизации
КонтрагентЗначение = ОбщегоНазначения.ЗначениеРеквизитаОбъекта(РегистраторСсылка, "Клиент");
// 5. Заполняем движения в цикле без лишних запросов
Для Каждого ТекДвижение Из Источник Цикл
ТекДвижение.Контрагент = КонтрагентЗначение;
КонецЦикла;
КонецЕсли;
// Метод Источник.Записать() НЕ ВЫЗЫВАЕМ! Платформа сделает это сама.
КонецПроцедуры
Разберем еще несколько нюансов, которые помогут избежать ошибок в будущем:
Записать() (хотя в случае с регистрами это почти никогда не нужно), используйте флаг отсечки рекурсии:
Если Источник.ДополнительныеСвойства.Свойство("ЭтоПрограммнаяЗапись") Тогда
Возврат;
КонецЕсли;
Источник.ДополнительныеСвойства.Вставить("ЭтоПрограммнаяЗапись", Истина);
Источник.Записать();
Повторное использование возвращаемых значений для получения данных клиента.Подведем итог: ошибка «Переполнение стека» — это сигнал о том, что ваш код зациклился. В случае с регистрами рецепт прост: используйте ПередЗаписью, никогда не вызывайте Записать() внутри подписки на этот же объект и минимизируйте количество обращений к базе данных внутри циклов.