Почему при проведении документа движения в регистре накопления то появляются, то исчезают через раз?

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

При разработке механизмов проведения документов в платформе «1С:Предприятие 8» программисты часто сталкиваются с загадочным поведением системы: при последовательном перепроведении одного и того же документа записи в регистре накопления то создаются, то полностью исчезают. Этот эффект повторяется с идеальной цикличностью — строго через раз.

В этой статье мы подробно разберем, почему возникает эффект «мерцания» движений, как устроена транзакционная логика платформы «1С:Предприятие 8», в чем разница между декларативной и императивной записью, и как правильно реализовать проведение документа, в том числе из подписки на события.

Выясняем причину: почему движения пропадают строго через раз?

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

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

  1. Первое проведение (база данных пуста):
    • Мы проводим документ в первый раз. В таблицах СУБД по этому документу для регистра накопления еще нет никаких записей.
    • При считывании документа в оперативную память платформа инициализирует коллекцию движений внутри объекта документа. Для нашего регистра коллекция Движения.ИмяРегистра создается пустой, а ее внутреннее свойство Модифицированность устанавливается в значение Ложь, так как программно мы её не изменяли.
    • Срабатывает наш код в подписке на событие ПриЗаписи. Код создает сторонний, изолированный объект набора записей через РегистрыНакопления.ИмяРегистра.СоздатьНаборЗаписей(), заполняет его строками и выполняет метод Записать(Истина). Записи физически попадают в СУБД.
    • Перед завершением транзакции платформа выполняет финальную проверку коллекций из свойства Движения самого объекта документа. Платформа видит, что у коллекции нашего регистра свойство Модифицированность равно Ложь. Платформа делает логичный вывод: «Раз разработчик не трогал эту коллекцию внутри документа, значит, записывать её в базу данных не требуется».
    • Транзакция фиксируется. Движения, записанные нами вручную через изолированный набор записей, успешно сохраняются в базе данных.
  2. Второе проведение (движения уже есть в базе данных):
    • Мы повторно проводим тот же самый документ. В базе данных уже лежат движения, которые мы успешно записали на предыдущем шаге.
    • При старте транзакции проведения срабатывает стандартный механизм платформы 1С или типовых конфигураций (например, код БСП), отвечающий за автоматическую очистку движений перед проведением.
    • Этот механизм считывает существующие движения из базы данных в коллекцию документа и выполняет команду очистки: Движения.ИмяРегистра.Очистить().
    • Поскольку коллекция внутри объекта документа была принудительно очищена кодом платформы, ее флаг Модифицированность автоматически принимает значение Истина!
    • Далее управление передается в нашу подписку ПриЗаписи. Наш код (точно так же, как и при первом проведении) создает изолированный объект СоздатьНаборЗаписей(), заполняет его и принудительно записывает в СУБД через метод Записать(Истина). На текущую микросекунду движения в базе данных перезаписались и физически существуют.
    • Транзакция близится к завершению. Платформа снова проверяет коллекции свойства Движения объекта документа. Но на этот раз флаг Модифицированность у коллекции нашего регистра равен Истина (ведь система очистила её в начале транзакции!).
    • Платформа берет коллекцию из объекта документа (которая абсолютно пуста, ведь наш код заполнял сторонний набор записей, а не саму коллекцию Движения документа) и производит ее автоматическую запись в базу данных с флагом замещения.
    • Пустой набор записей из объекта документа полностью затирает и уничтожает те движения, которые мы только что записали вручную через СоздатьНаборЗаписей().
    • Транзакция завершается. Движения в базе данных полностью отсутствуют.
  3. Третье проведение (база данных снова пуста):
    • Поскольку на предыдущем шаге движения были стерты платформой, база данных чиста. Начинается новый цикл, который полностью дублирует сценарий Первого проведения. Движения снова появляются.

Таким образом, мы получаем бесконечный цикл «появление-исчезновение» записей при каждом последующем нажатии кнопки проведения.

Правильное решение: работа через коллекцию Движения

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

Рассмотрим, как изменить проблемный код в подписке на событие. Обратите внимание, что в подписке на событие у нас есть доступ к объекту документа через параметр Источник.

Давайте сравним старый (ошибочный) вариант кода и новый (правильный).

Ошибочный вариант кода:


// НЕПРАВИЛЬНО: Использование стороннего набора записей в транзакции документа
Записи = РегистрыНакопления.кт_ВзаиморасчетыЭкспедирования.СоздатьНаборЗаписей();
Записи.Отбор.Регистратор.Установить(Поступление);

Пока ВыборкаДетальныеЗаписи.Следующий() Цикл    
    Зап = Записи.ДобавитьРасход();
    Зап.Организация = ВыборкаДетальныеЗаписи.Организация;
    Зап.Период = Дата;
    // ... заполнение остальных реквизитов
КонецЦикла;
Записи.Записать(Истина);

Исправленный и корректный вариант кода:


// ПРАВИЛЬНО: Работа напрямую с коллекцией Движения объекта документа
// В обработчике подписки параметр "Источник" представляет собой сам ДокументОбъект

ДвижениеРегистра = Источник.Движения.кт_ВзаиморасчетыЭкспедирования;

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

// Включаем признак необходимости записи при завершении транзакции
ДвижениеРегистра.Записывать = Истина;

При использовании второго варианта платформа видит изменения в коллекции самого документа, автоматически устанавливает флаг модифицированности и гарантированно записывает новые движения в базу данных в самом конце транзакции, не затирая их пустым набором.

В чем разница между Записывать = Истина и явным вызовом метода Записать()?

В процессе работы с движениями у разработчиков часто возникает вопрос: зачем существует свойство Записывать (флаг типа Булево) и метод Записать(), и в каких случаях нужно использовать каждый из них?

Давайте детально разберем эти подходы, которые условно можно назвать декларативным и императивным.

1. Декларативный подход: Свойство Записывать = Истина

Когда мы пишем Движения.ИмяРегистра.Записывать = Истина, мы не производим немедленную запись в базу данных. Мы лишь декларируем (сообщаем платформе) наше намерение: «Я подготовил данные в оперативной памяти. Когда проведение документа будет успешно завершено и транзакция будет закрываться, запиши эти движения одной командой».

Преимущества декларативного подхода:

2. Императивный подход: Метод Движения.ИмяРегистра.Записать()

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

Когда необходим метод Записать()?

Явный вызов метода Записать() необходим при реализации так называемой «новой методики проведения» (или оперативного контроля остатков). Разберем стандартный алгоритм этого механизма:

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

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

Опасность императивного вызова метода Записать():

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

Важность использования Источник.Движения: Сводные преимущества

Подводя итог, выделим ключевые причины, по которым работа с движениями документа всегда должна вестись исключительно через коллекцию Движения (в модуле документа или через параметр Источник.Движения в подписках на события):

← На главную