Разработчики, работающие с унаследованными системами на платформе 1С:Предприятие 7.7 (также известной в профессиональной среде как «клюшки»), регулярно сталкиваются с неочевидным поведением встроенного языка. Одним из таких классических вопросов, вызывающих споры даже у опытных специалистов, является поведение реквизитов документа при вызове метода Провести(). Давайте подробно проанализируем, что происходит под капотом платформы при программном изменении реквизитов шапки или табличной части и последующем вызове проведения, а также выясним, как ведет себя система в случае возникновения ошибок проведения.
Представим типичную ситуацию, когда в программном коде внешней обработки или модуля другого объекта выполняется поиск существующего документа, изменяется значение его реквизита, а затем вызывается метод проведения. Рассмотрим следующий шаблон кода:
Док = СоздатьОбъект("Документ.Новый1");
Если Док.НайтиДокумент(КакойТоДокумент) = 1 Тогда
// Было какое-то старое значение реквизита, присваиваем новое
Док.Реквизит1 = КакоеТоНовоеЗначение1;
Док.Провести();
КонецЕсли;
В связи с этим кодом возникают три практических вопроса, ответы на которые напрямую влияют на логику работы и надежность вашей системы:
Провести()?Реквизит1 в случае успешного проведения документа — новое или старое?Реквизит1 в случае неуспешного проведения (например, если проведение прервано программно или произошла критическая ошибка выполнения модуля)?Для того чтобы ответить на эти вопросы, нам необходимо детально изучить двухфазную модель фиксации данных в 1С:Предприятии 7.7.
Да, при вызове метода Провести() платформа 1С 7.7 всегда принудительно и неявно выполняет физическую запись документа в базу данных. Это поведение жестко зашито в ядро платформы. Документ не может находиться в проведенном состоянии, не будучи предварительно записанным в таблицы базы данных. В процессе вызова метода Провести() происходит сериализация всех реквизитов объекта из оперативной памяти и их сохранение в физические файлы таблиц (например, в файлы DH* для шапки документа и DT* для табличной части в версии DBF, либо в соответствующие таблицы SQL-базы).
Ответ на оба эти вопроса может удивить программистов, привыкших к транзакционной модели современных платформ, таких как 1С:Предприятие 8. В обоих случаях — и при успешном, и при неуспешном проведении — реквизит примет НОВОЕ значение!
Давайте выясним причину такого поведения. Дело в том, что в 1С 7.7 отсутствует сквозная атомарность между физической записью полей самого документа и транзакцией проведения. Процесс выполнения метода Провести() разделен на две абсолютно независимые фазы:
Первая фаза: Неявная запись. Платформа берет текущее состояние объекта из памяти (включая все измененные реквизиты, такие как Реквизит1) и записывает их в базу данных. Эта операция выполняется до открытия транзакции проведения и фиксируется в СУБД окончательно.
Вторая фаза: Проведение. Только после успешного сохранения полей документа открывается транзакция проведения, внутри которой запускается предопределенная процедура ОбработкаПроведения() из модуля документа. В рамках этой транзакции формируются движения по регистрам (бухгалтерским проводкам, регистрам накопления или расчетов), а также меняется статус документа в общем журнале 1SJOURN на «Проведен».
Если во второй фазе (внутри процедуры ОбработкаПроведения()) происходит ошибка — например, не хватает остатков товаров, вызывается СтатусВозврата(0), происходит деление на ноль или обращение к несуществующему объекту метаданных, — платформа инициирует откат транзакции (Rollback). Однако этот откат затрагивает только действия, совершенные во второй фазе. То есть удаляются сформированные движения регистров и возвращается флаг «Не проведен» в общем журнале. Физические реквизиты документа, которые были сохранены в таблицы DH* и DT* на первой фазе, назад к старым значениям не откатываются! Таким образом, новые значения реквизитов гарантированно остаются в базе данных.
Рассмотрим пример реального теста, который наглядно демонстрирует эту особенность платформы. Создадим документ, у которого есть реквизит Контрагент (тип «Справочник.Контрагенты») и числовой реквизит-флаг Ошибка. В модуле проведения этого документа напишем код, который умышленно вызывает ошибку времени выполнения при взведенном флаге:
Процедура ОбработкаПроведения()
Если Ошибка = 1 Тогда
// Намеренно обращаемся к несуществующему объекту метаданных для вызова системного сбоя
СоздатьОбъект("Справочник.НесуществующийСправочник");
КонецЕсли;
КонецПроцедуры
Теперь напишем внешнюю обработку, которая находит данный документ, меняет реквизиты и пытается выполнить проведение:
БегомПоДокам = СоздатьОбъект("Документ.Новый1");
Если БегомПоДокам.НайтиДокумент(ТекДок) = 1 Тогда
БегомПоДокам.Контрагент = ВыбранныйКонтрагент;
БегомПоДокам.Ошибка = 1; // взводим флаг для генерации ошибки в модуле
БегомПоДокам.Провести();
КонецЕсли;
При выполнении этого кода произойдет следующее:
Контрагент примет значение ВыбранныйКонтрагент, а реквизит Ошибка станет равным 1.ОбработкаПроведения().Контрагент и Ошибка сохранили новые значения, несмотря на то, что документ так и не был проведен.Для разработчиков, которые параллельно работают с версиями 8.х, такое поведение может показаться диким. Разберем ключевое различие в транзакционных моделях этих двух платформ:
Записать(РежимЗаписиДокумента.Проведение) сначала стартует транзакция базы данных, в рамках которой записываются реквизиты, а затем выполняется обработка проведения. Если в процессе проведения возникает ошибка или взводится флаг Отказ = Истина, вся транзакция СУБД полностью откатывается. Новые значения реквизитов документа стираются из таблиц, возвращаясь к исходному (до начала транзакции) состоянию. Если документ создавался с нуля, он вообще исчезает из базы данных.В процессе обсуждений на форумах начинающие разработчики или специалисты, желающие перестраховаться («лучше перестраховаться, чем недоглядеть»), часто предлагают писать код следующим образом:
Док.НайтиДокумент(КакойТоДокумент);
Док.Реквизит1 = КакоеТоНовоеЗначение1;
Док.Записать(); // Предварительная явная запись
Док.Провести(); // Последующее проведение
Давайте подробно разберем, почему подобная конструкция не просто избыточна, но и крайне вредна для производительности вашей информационной базы, особенно в многопользовательском режиме:
Записать() платформа выполняет блокировку физических таблиц документа и таблицы общего журнала документов 1SJOURN, а затем производит физическую запись данных на диск.Провести(), который, как мы уже выяснили, в первую очередь инициирует свою собственную неявную запись. Платформа повторно блокирует те же самые таблицы и повторно перезаписывает те же самые файлы на диске.Таким образом, для программной записи изменений и последующего проведения документа всегда достаточно использовать один-единственный метод — Провести().
При программной работе с объектом документа важно помнить еще об одном критическом ограничении платформы 1С 7.7. Когда вы изменяете реквизиты объекта в коде и вызываете методы Записать() или Провести(), предопределенная процедура модуля формы документа ПриЗаписи() не вызывается.
Процедура ПриЗаписи() является обработчиком интерактивных действий пользователя, совершаемых непосредственно в открытой форме документа на экране. Вся бизнес-логика, проверки заполнения полей, расчеты налогов или автозаполнение связанных табличных частей, которые разработчики конфигурации могли прописать внутри процедуры ПриЗаписи() в модуле формы, будут полностью проигнорированы при вызове методов записи из внешнего программного кода.
Если вам необходимо гарантировать выполнение этих проверок при программном проведении, вы должны следовать следующим правилам:
ОбработкаПроведения() модуля документа.Провести().Резюмируя особенности работы с документами в 1С 7.7, сформулируем основные правила:
Провести() всегда самостоятельно выполняет физическое сохранение всех измененных реквизитов в базу данных.Записать(); Провести(); — это создает избыточную нагрузку на дисковую систему и приводит к блокировкам таблиц у других пользователей. Достаточно вызывать только Провести().ПриЗаписи(), поэтому контролируйте корректность заполнения реквизитов непосредственно в программном коде вашей обработки.