В процессе разработки в системе «1С:Предприятие» часто возникает задача фиксации истории или текущего состояния различных объектов. Типичный пример — запись статуса документов (например, «Приходная накладная» и «Расходная накладная») в регистр сведений при их проведении или сохранении. На первый взгляд задача кажется тривиальной, однако при ее реализации разработчики регулярно сталкиваются с неочевидными архитектурными сложностями, ошибками объектной модели и проблемами производительности.
В этой статье мы подробно разберем, как решить данную задачу правильно, избежав классических ошибок, и рассмотрим лучшие практики программирования на платформе 1С.
Разберем типичную ситуацию, с которой сталкиваются начинающие разработчики при написании кода записи в регистр. Рассмотрим код, представленный в первом сообщении темы:
Процедура ЗаписатьСтатусДокумента(ПриходнаяНакладная,РасходнаяНакладная,СтатусыДокументов) Экспорт
Запрос= Новый Запрос;
Запрос.Текст= "ВЫБРАТЬ
| СтатусыДокументовСрезПоследних.Статус КАК Статус,
| СтатусыДокументовСрезПоследних.Документ КАК Документ
|ИЗ
| РегистрСведений.СтатусыДокументов.СрезПоследних КАК СтатусыДокументовСрезПоследних";
Выборка=Запрос.Выполнить.Выбрать;
ПОКА Выборка.Следующий ЦИКЛ
Если Не(ПриходнаяНакладная.Объект.Ссылка=Выборка.Документ И ПриходнаяНакладная.Объект.Статус=Выборка.Статус) Тогда
Менеджер= РегистрыСведений.СтатусыДокументов.СоздатьМенеджерЗаписи;
// Код записи...
КонецЕсли;
КонецЦикла;
КонецПроцедуры
При выполнении этого кода система выдает ошибку: «Значение не является значением объектного типа (Объект)». Выясним причину этой ошибки.
Разработчик путает контекст выполнения кода на клиенте и на сервере. Свойство Объект существует у формы (например, в модуле управляемой формы), где оно представляет собой данные формы, связанные с основным реквизитом. В модуле самого документа (модуле объекта) или в общем модуле никакого свойства Объект у документа нет. Для обращения к самому объекту внутри его модуля используется ключевое слово ЭтотОбъект, а для получения его уникального идентификатора в базе данных — свойство Ссылка.
Таким образом, обращение должно выглядеть следующим образом:
ЭтотОбъект.Ссылка (или просто Ссылка) и ЭтотОбъект.Статус (или просто Статус).ДокументСсылка.Статус.Другая распространенная ошибка, с которой столкнулись участники обсуждения — «Значение не является значением объектного типа (Ссылка)» или ситуация, когда статус записывается в регистр, но поле «Документ» остается пустым.
Это происходит из-за попытки записать статус нового, еще не сохраненного в базе данных документа. Проанализируем жизненный цикл документа:
ПередЗаписью у нового документа физически еще нет ссылки в базе данных. Свойство Ссылка в этот момент возвращает пустую ссылку (ДокументСсылка.Пустая()).Решение: Запись в независимый регистр сведений должна происходить в обработчике ПриЗаписи (в модуле объекта) или в ОбработкаПроведения. В момент выполнения процедуры ПриЗаписи ссылка на документ в базе данных уже гарантированно создана и физически существует, благодаря чему её можно безопасно использовать для записи в регистр.
На форуме было предложено два варианта записи данных в регистр сведений: с использованием МенеджерЗаписи и через НаборЗаписей. Рассмотрим оба варианта подробнее.
Вариант с использованием менеджера записи:
Процедура ЗаписатьСтатусДокумента(Документ, Статус) Экспорт
Менеджер = РегистрыСведений.СтатусыДокументов.СоздатьМенеджерЗаписи;
Менеджер.Период = ТекущаяДата();
Менеджер.Документ = Документ;
Менеджер.Статус = Статус;
Менеджер.Записать();
КонецПроцедуры
Этот способ прост, но имеет ряд недостатков. МенеджерЗаписи работает медленнее, так как при вызове метода Записать() он сначала пытается прочитать существующую запись из базы данных по ключевым полям (измерениям), чтобы определить, нужно ли делать вставку (INSERT) или обновление (UPDATE). Если записей много, это создает лишнюю нагрузку.
Вариант с использованием набора записей (предложенный в сообщении №13):
Процедура ЗаписатьСтатусДокумента(Документ, Статус) Экспорт
Набор = РегистрыСведений.СтатусыДокументов.СоздатьНаборЗаписей;
Набор.Отбор.Документ.Установить(Документ);
НоваяЗапись = Набор.Добавить();
НоваяЗапись.Период = ТекущаяДата();
НоваяЗапись.Документ = Документ;
НоваяЗапись.Статус = Статус;
Набор.Записать();
КонецПроцедуры
Важное предупреждение: Использование НаборЗаписей с установленным отбором — самый быстрый и безопасный способ для замещения данных в непериодическом регистре сведений. Он полностью очищает старые записи, соответствующие отбору, и записывает новые.
Однако, если ваш регистр является периодическим (то есть предназначен для хранения истории изменения статусов во времени), метод Записать() с установленным отбором по документу удалит всю накопленную историю статусов этого документа и оставит только одну текущую запись. Если перед вами стоит задача сохранять историю изменений (например, когда документ переходил из статуса «Черновик» в «На согласовании», а затем в «Утвержден»), использовать полную перезапись набора с отбором по документу нельзя! В таких случаях необходимо использовать МенеджерЗаписи или производить добавление в набор без предварительного затирания всей истории.
В обсуждении на форуме авторы используют ручную запись в регистр сведений, что однозначно указывает на работу с независимым регистром сведений. С точки зрения архитектуры «1С:Предприятие» здесь кроется серьезная ловушка.
Если документ, статус которого мы записали в регистр, впоследствии будет отменен, помечен на удаление или полностью удален из базы данных физически, запись в независимом регистре сведений останется нетронутой. Это неизбежно приведет к появлению так называемых «битых» ссылок (в регистре будет висеть запись с документом вида <Объект не найден>...) и накоплению мусора в базе данных.
Рекомендация: Если статус жестко привязан к жизненному циклу конкретного документа, регистр сведений правильнее сделать подчиненным регистратору (в свойствах регистра установить режим записи «Подчинение регистратору»).
В этом случае запись статуса выполняется стандартным заполнением таблицы движений документа в обработчике ОбработкаПроведения:
Процедура ОбработкаПроведения(Отказ, РежимПроведения)
// Проведение по другим регистрам...
// Запись в регистр статусов
Движение = Движения.СтатусыДокументов.Добавить();
Движение.Период = Дата;
Движение.Документ = Ссылка;
Движение.Статус = Статус;
Движения.СтатусыДокументов.Записывать = Истина;
КонецПроцедуры
При таком подходе платформа 1С берет на себя всю рутинную работу: при отмене проведения или удалении документа связанные с ним записи в регистре будут очищены автоматически и гарантированно.
Проанализируем функцию получения статуса, предложенную на форуме:
Функция ПолучитьСтатусДокумента(Документ) Экспорт
Запрос = Новый Запрос;
Запрос.УстановитьПараметр("Дата",ТекущаяДата());
Запрос.УстановитьПараметр("Документ",Документ);
Запрос.Текст ="ВЫБРАТЬ Статус
|ИЗ РегистрСведений.СтатусыДокументов.СрезПоследних(&Дата, Документ = &Документ)";
Выборка = Запрос.Выполнить.Выбрать();
Если Выборка.Следующий() Тогда
Возврат Выборка.Статус;
КонецЕсли;
Возврат Неопределено;
КонецФункции
Выполнение любых запросов к базе данных (особенно к виртуальным таблицам, таким как СрезПоследних) внутри транзакции записи — это крайне неоптимальное решение. Обработчики ПриЗаписи и ОбработкаПроведения выполняются строго внутри транзакции. Если база данных высоконагруженная, и одновременно несколько пользователей проводят документы, подобные запросы гарантированно приведут к взаимным блокировкам (Deadlocks) таблиц на уровне СУБД.
Чтобы понять, изменился ли статус документа по сравнению с тем, что уже записан в базе данных, делать запрос к регистру вообще не требуется! Достаточно сравнить текущее значение реквизита в оперативной памяти (у объекта) со значением, которое сохранено в базе данных:
// В модуле объекта документа
Процедура ПриЗаписи(Отказ)
// Если документ новый, то статус изменился по определению.
// Если документ старый, сравниваем значение в памяти со значением в БД.
СтатусИзменился = ЭтоНовый() Или (ЭтотОбъект.Статус <> ЭтотОбъект.Ссылка.Статус);
Если СтатусИзменился Тогда
ОбщийМодуль1.ЗаписатьСтатусДокумента(Ссылка, Статус);
КонецЕсли;
КонецПроцедуры
Обращение к реквизиту через ссылку (ЭтотОбъект.Ссылка.Статус) работает во много раз быстрее, так как платформа 1С использует механизм внутреннего кэширования объектов и не создает тяжелой транзакционной нагрузки на СУБД.
Вручную прописывать вызовы функций записи в модулях каждого документа («Приходная накладная», «Расходная накладная» и т.д.) — это плохой тон программирования. Это нарушает базовый принцип разработки DRY (Don't Repeat Yourself — не повторяйся) и значительно усложняет поддержку конфигурации при добавлении новых видов документов.
Решение: Создание Подписки на событие.
Рассмотрим пошагово, как это сделать:
ДокументОбъект.ПриходнаяНакладная, ДокументОбъект.РасходнаяНакладная).ПриЗаписи.Пример кода обработчика подписки в общем модуле:
Процедура ПодпискаПриЗаписиДокументаРегистрацияСтатуса(Источник, Отказ, РежимЗаписи, РежимПроведения) Экспорт
// Источник — это сам записываемый объект документа
Если Источник.ОбменДанными.Загрузка Тогда
Возврат; // Не выполняем логику при обмене данными
КонецЕсли;
СтатусИзменился = Источник.ЭтоНовый() Или (Источник.Статус <> Источник.Ссылка.Статус);
Если СтатусИзменился Тогда
Набор = РегистрыСведений.СтатусыДокументов.СоздатьНаборЗаписей;
Набор.Отбор.Документ.Установить(Источник.Ссылка);
НоваяЗапись = Набор.Добавить();
НоваяЗапись.Период = ТекущаяДата();
НоваяЗапись.Документ = Источник.Ссылка;
НоваяЗапись.Статус = Источник.Статус;
Набор.Записать();
КонецЕсли;
КонецПроцедуры
Использование механизма подписок на события позволяет полностью изолировать код логики записи статусов от кода самих документов. Если в будущем в вашей системе появится новый документ (например, «Заказ покупателя»), у которого также должен фиксироваться статус, вам не придется писать ни одной строчки кода — будет достаточно лишь отметить галочкой этот документ в составе источников существующей подписки на событие в конфигураторе.