Зачем нужен явный вызов Движения.Записать() в процедуре ОбработкаПроведения в 1С

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

Каждый разработчик 1С на определенном этапе работы сталкивается с кодом в типовых конфигурациях (таких как "1С:Бухгалтерия 3.0", "1С:Управление торговлей 11" или "1С:ERP"), где в самом конце процедуры ОбработкаПроведения вызывается метод Движения.Записать(). На первый взгляд это кажется избыточным действием. Ведь платформа 1С:Предприятие по умолчанию спроектирована так, чтобы автоматически записывать все модифицированные движения документа при успешном завершении транзакции проведения. Зачем же дублировать эту операцию в коде программно?

Давайте подробно проанализируем этот вопрос, рассмотрим внутренние механизмы платформы, разберем поведение СУБД и выясним, в каких сценариях явный вызов метода записи является не просто полезным, а жизненно необходимым для корректной работы бизнес-логики.

Как платформа записывает движения по умолчанию

Для начала вспомним, как устроена автоматическая запись движений. У каждого документа в конфигурации есть свойство "Запись движений при проведении". Оно может принимать два основных значения:

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

Причина 1. Реализация новой методики проведения и контроль остатков

Это самая распространенная причина использования явной записи. Начиная с версии платформы 8.2, фирма "1С" рекомендует использовать так называемую "новую методику проведения" (также известную как проведение "от обратного").

Давайте сравним две методики проведения, чтобы понять разницу:

  1. Старая методика: Сначала накладывается управляемая блокировка на аналитику регистра, затем запросом читаются текущие остатки товаров, проверяется возможность списания, и только при успешной проверке формируются и записываются новые движения. Недостаток метода — избыточные блокировки и лишнее чтение данных, которые могут не понадобиться, если проведение все равно завершится отказом.
  2. Новая методика: Мы сразу, без предварительных проверок, формируем движения списания и записываем их в базу данных. И только после этого делаем один-единственный запрос, который проверяет наличие отрицательных остатков по регистру накопления. Если обнаружились "минусы", мы выводим сообщение об ошибке, устанавливаем флаг Отказ = Истина и отменяем транзакцию проведения.

Выясним причину, почему при новой методике проведения необходим явный вызов Движения.Записать(). Если мы не запишем движения принудительно внутри кода, наши новые проводки будут находиться исключительно в оперативной памяти (в коллекции Движения). База данных о них еще ничего знать не будет. Следовательно, последующий запрос к виртуальной таблице остатков (например, РегистрНакопления.ТоварыНаСкладах.Остатки) просто не "увидит" списания текущего документа, и контроль остатков выдаст неверный результат.

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

Причина 2. Очередность выполнения обработчиков событий и влияние подписок

Проанализируем, как явный вызов влияет на последовательность выполнения программного кода и подписок на события. Это очень тонкий архитектурный момент, о котором часто забывают начинающие разработчики.

Когда запись движений происходит автоматически (неявно) после завершения процедуры ОбработкаПроведения, порядок событий выглядит следующим образом:

  1. Выполняется код процедуры ОбработкаПроведения в модуле документа.
  2. Выполняются обработчики подписок на событие ОбработкаПроведения документа.
  3. Платформа инициирует запись наборов записей движений.
  4. Выполняются обработчики событий ПередЗаписью и ПриЗаписи в модулях наборов записей регистров.
  5. Выполняются подписки на события записи наборов записей регистров.

Если мы вызываем Движения.Записать() явно внутри тела ОбработкаПроведения, последовательность кардинально меняется:

  1. Внутри ОбработкаПроведения вызывается метод Движения.Записать().
  2. Сразу же запускаются обработчики ПередЗаписью и ПриЗаписи для всех записываемых регистров, а также подписки на запись этих регистров.
  3. После этого управление возвращается обратно в процедуру ОбработкаПроведения, и код выполняется дальше.
  4. Только после завершения процедуры проведения выполняются подписки на событие ОбработкаПроведения документа.

Такое поведение критически важно по двум причинам:

Причина 3. Формирование одних регистров на основании других

В сложных прикладных решениях часто встречается ситуация, когда движения одного регистра формируются на основании уже записанных движений другого регистра этого же документа. Например, документ сначала делает движения по регистру накопления "Выручка и себестоимость продаж", а затем на основании этих данных специальный общий модуль должен рассчитать и сформировать проводки в регистре бухгалтерии (журнале проводок).

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

  1. Формируем движения по первичному регистру.
  2. Вызываем Движения.ИмяПервичногоРегистра.Записать().
  3. Запросом получаем данные из этого регистра по текущему документу-регистратору.
  4. На основе полученных результатов формируем движения по вторичному регистру.

Причина 4. Предотвращение взаимоблокировок (Deadlocks)

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

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

Причина 5. Сброс флагов модифицированности и защита от двойной записи

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

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

Поэтому, когда процедура ОбработкаПроведения завершается, платформа видит, что у всех наборов свойство Записывать равно Ложь, и повторную запись в базу данных не производит. Производительность системы при этом никак не страдает — физическая запись на диск выполняется ровно один раз.

Разберем практический пример

Рассмотрим пример реализации новой методики проведения с явной записью движений и последующим контролем остатков с помощью запроса.


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

Резюме

Явный вызов метода Движения.Записать() в процедуре ОбработкаПроведения — это стандартный инструмент профессиональной разработки на платформе 1С:Предприятие. Мы используем его для:

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

← На главную