При разработке механизмов проведения документов в платформе «1С:Предприятие 8» программисты часто сталкиваются с загадочным поведением системы: при последовательном перепроведении одного и того же документа записи в регистре накопления то создаются, то полностью исчезают. Этот эффект повторяется с идеальной цикличностью — строго через раз.
В этой статье мы подробно разберем, почему возникает эффект «мерцания» движений, как устроена транзакционная логика платформы «1С:Предприятие 8», в чем разница между декларативной и императивной записью, и как правильно реализовать проведение документа, в том числе из подписки на события.
Для того чтобы понять природу этой проблемы, нам необходимо заглянуть во внутренние механизмы транзакционного цикла платформы. Ошибка кроется в использовании независимого набора записей через метод СоздатьНаборЗаписей() внутри транзакции проведения документа (или в подписке на событие ПриЗаписи документа) совместно с работой стандартных механизмов очистки движений.
Давайте пошагово проанализируем, что происходит в базе данных и оперативной памяти при последовательном перепроведении документа.
Движения.ИмяРегистра создается пустой, а ее внутреннее свойство Модифицированность устанавливается в значение Ложь, так как программно мы её не изменяли.ПриЗаписи. Код создает сторонний, изолированный объект набора записей через РегистрыНакопления.ИмяРегистра.СоздатьНаборЗаписей(), заполняет его строками и выполняет метод Записать(Истина). Записи физически попадают в СУБД.Движения самого объекта документа. Платформа видит, что у коллекции нашего регистра свойство Модифицированность равно Ложь. Платформа делает логичный вывод: «Раз разработчик не трогал эту коллекцию внутри документа, значит, записывать её в базу данных не требуется».Движения.ИмяРегистра.Очистить().Модифицированность автоматически принимает значение Истина!ПриЗаписи. Наш код (точно так же, как и при первом проведении) создает изолированный объект СоздатьНаборЗаписей(), заполняет его и принудительно записывает в СУБД через метод Записать(Истина). На текущую микросекунду движения в базе данных перезаписались и физически существуют.Движения объекта документа. Но на этот раз флаг Модифицированность у коллекции нашего регистра равен Истина (ведь система очистила её в начале транзакции!).Движения документа) и производит ее автоматическую запись в базу данных с флагом замещения.СоздатьНаборЗаписей().Таким образом, мы получаем бесконечный цикл «появление-исчезновение» записей при каждом последующем нажатии кнопки проведения.
Для того чтобы разорвать этот замкнутый круг и заставить платформу корректно работать с регистрами, мы должны отказаться от использования метода СоздатьНаборЗаписей() для регистратора внутри транзакции его проведения. Вместо этого необходимо использовать штатную коллекцию Движения, которая принадлежит самому объекту документа.
Рассмотрим, как изменить проблемный код в подписке на событие. Обратите внимание, что в подписке на событие у нас есть доступ к объекту документа через параметр Источник.
Давайте сравним старый (ошибочный) вариант кода и новый (правильный).
Ошибочный вариант кода:
// НЕПРАВИЛЬНО: Использование стороннего набора записей в транзакции документа
Записи = РегистрыНакопления.кт_ВзаиморасчетыЭкспедирования.СоздатьНаборЗаписей();
Записи.Отбор.Регистратор.Установить(Поступление);
Пока ВыборкаДетальныеЗаписи.Следующий() Цикл
Зап = Записи.ДобавитьРасход();
Зап.Организация = ВыборкаДетальныеЗаписи.Организация;
Зап.Период = Дата;
// ... заполнение остальных реквизитов
КонецЦикла;
Записи.Записать(Истина);
Исправленный и корректный вариант кода:
// ПРАВИЛЬНО: Работа напрямую с коллекцией Движения объекта документа
// В обработчике подписки параметр "Источник" представляет собой сам ДокументОбъект
ДвижениеРегистра = Источник.Движения.кт_ВзаиморасчетыЭкспедирования;
// Заполняем коллекцию документа напрямую
Пока ВыборкаДетальныеЗаписи.Следующий() Цикл
Зап = ДвижениеРегистра.ДобавитьРасход();
Зап.Организация = ВыборкаДетальныеЗаписи.Организация;
Зап.Период = Источник.Дата;
Зап.ДоговорСКонтрагентом = ВыборкаДетальныеЗаписи.абрДоговорЗаказчика;
Зап.Контрагент = ВыборкаДетальныеЗаписи.абрЗаказчик;
Зап.ДолгЗаказчика = ВыборкаДетальныеЗаписи.ЗаказчикСумма;
Зап.ЗЭ = ВыборкаДетальныеЗаписи.Ссылка;
КонецЦикла;
// Включаем признак необходимости записи при завершении транзакции
ДвижениеРегистра.Записывать = Истина;
При использовании второго варианта платформа видит изменения в коллекции самого документа, автоматически устанавливает флаг модифицированности и гарантированно записывает новые движения в базу данных в самом конце транзакции, не затирая их пустым набором.
В процессе работы с движениями у разработчиков часто возникает вопрос: зачем существует свойство Записывать (флаг типа Булево) и метод Записать(), и в каких случаях нужно использовать каждый из них?
Давайте детально разберем эти подходы, которые условно можно назвать декларативным и императивным.
1. Декларативный подход: Свойство Записывать = Истина
Когда мы пишем Движения.ИмяРегистра.Записывать = Истина, мы не производим немедленную запись в базу данных. Мы лишь декларируем (сообщаем платформе) наше намерение: «Я подготовил данные в оперативной памяти. Когда проведение документа будет успешно завершено и транзакция будет закрываться, запиши эти движения одной командой».
Преимущества декларативного подхода:
2. Императивный подход: Метод Движения.ИмяРегистра.Записать()
Вызов метода Движения.ИмяРегистра.Записать() заставляет платформу выполнить немедленный сброс данных из оперативной памяти на диск (в таблицы СУБД) прямо в текущей строке кода, не дожидаясь окончания процедуры проведения.
Когда необходим метод Записать()?
Явный вызов метода Записать() необходим при реализации так называемой «новой методики проведения» (или оперативного контроля остатков). Разберем стандартный алгоритм этого механизма:
Движения.ТоварыНаСкладах.Записать(). Данные физически попадают в таблицы СУБД.Если бы мы использовали декларативный признак Записывать = Истина, то в момент выполнения запроса движений нашего документа еще не было бы в базе данных, и мы не смогли бы проконтролировать остатки с их учетом.
Опасность императивного вызова метода Записать():
Если в коде проведения разных документов разработчики вызывают методы Записать() для различных регистров в произвольном порядке, СУБД начнет блокировать таблицы хаотично. При высокой интенсивности работы пользователей это неизбежно приведет к регулярным ошибкам взаимных блокировок (Deadlocks) и аварийному завершению транзакций.
Подводя итог, выделим ключевые причины, по которым работа с движениями документа всегда должна вестись исключительно через коллекцию Движения (в модуле документа или через параметр Источник.Движения в подписках на события):
Движения система автоматически накладывает управляемые блокировки на соответствующие пространства данных регистра, предотвращая чтение «грязных» или некорректных данных другими сессиями пользователей в момент проведения документа.