В практике разработки на платформе 1С 8.3 часто возникает задача: при записи или проведении документа необходимо транслировать данные его табличной части в регистр сведений. Для упрощения копирования данных разработчики обычно используют встроенную функцию ЗаполнитьЗначенияСвойств. Однако при её реализации легко совершить критические ошибки, которые приводят к потере данных, перезаписи строк или критическому снижению производительности системы.
В этой статье мы подробно разберем, почему стандартный подход с использованием МенеджерЗаписи часто работает некорректно, как правильно использовать НаборЗаписей, почему нельзя делать запросы к СУБД внутри транзакции записи и как спроектировать оптимальную архитектуру взаимодействия документа и регистра сведений.
Рассмотрим исходную ситуацию. У нас есть документ с табличной частью, данные из которой нужно перенести в независимый регистр сведений. Начинающие разработчики часто пытаются решить эту задачу "в лоб" через форму документа, используя следующий код:
Процедура ПриЗаписиНаСервере(Отказ, ТекущийОбъект, ПараметрыЗаписи)
МенеджерЗаписи = РегистрыСведений.МОЛ.СоздатьМенеджерЗаписи();
Запрос = Новый Запрос;
// ... (текст запроса к табличной части документа в базе данных)
Выборка = Запрос.Выполнить().Выбрать();
Если Выборка.Следующий() Тогда
ЗаполнитьЗначенияСвойств(МенеджерЗаписи, Выборка);
КонецЕсли;
МенеджерЗаписи.Записать(Истина);
КонецПроцедуры
Проанализируем этот фрагмент и выясним, почему он приводит к некорректному поведению системы:
МенеджерЗаписи предназначен для работы ровно с одной записью регистра. Когда мы пишем Если Выборка.Следующий() Тогда, мы обрабатываем только самую первую строку из результата запроса. Все остальные строки табличной части документа просто игнорируются. Если же мы попытаемся обходить выборку в цикле и записывать каждый раз через один и тот же МенеджерЗаписи, то каждая последующая строка с одинаковыми ключевыми измерениями будет просто перезаписывать предыдущую. В итоге в регистре останется только одна запись.ПриЗаписиНаСервере — это событие формы. Если документ будет записан программно (например, при обмене данными, через групповую обработку или фоновым заданием), этот код вообще не выполнится, и данные в регистр не попадут.Документ.ПеремещениеМатериалов.ПМ внутри транзакции записи самого себя — крайне опасная практика. В СУБД на момент выполнения запроса могут находиться старые данные, либо данных может не быть вовсе (если документ новый). Из-за этого запрос возвращает пустой результат или неактуальную информацию.Теперь разберем по шагам, как исправить эти архитектурные ошибки и написать надежный, быстродействующий код.
Для того чтобы логика записи в регистр срабатывала всегда, независимо от того, как записывается документ (интерактивно пользователем или программно из кода), перенесем обработчик в Модуль объекта документа.
Если наш регистр сведений является независимым, то наиболее подходящим местом будет предопределенная процедура ПриЗаписи модуля объекта. Если же регистр подчинен регистратору, запись должна происходить в процедуре ОбработкаПроведения.
Поскольку в момент записи документа все актуальные данные его табличной части уже находятся в оперативной памяти (в свойстве ЭтотОбъект), нам нет необходимости обращаться к базе данных с помощью тяжелого SQL-запроса. Мы можем напрямую выгрузить табличную часть в объект ТаблицаЗначений. Это исключит лишнее чтение из СУБД и решит проблему "нечитаемых" данных нового документа.
Рассмотрим, как быстро получить и подготовить данные:
ТаблицаДанных = ЭтотОбъект.ПМ.Выгрузить();
// При необходимости мы можем свернуть таблицу по ключевым полям
ТаблицаДанных.Свернуть("МатериальноОтветственноеЛицо, СкладОтправитель", "Количество");
Для записи множества строк необходимо использовать объект НаборЗаписей. Он позволяет за одну транзакцию очистить старые движения по конкретному документу и записать новые.
Важное предупреждение: Никогда не вызывайте метод Записать() у набора записей без предварительной установки фильтра (отбора). Если вы не установите отбор по ключевому измерению (например, по ссылке на документ-источник), платформа полностью удалит все записи из регистра сведений и запишет туда только новые строки. Это приведет к катастрофической потере данных других документов!
Посмотрим на правильный шаблон работы с набором записей независимого регистра сведений:
НаборЗаписей = РегистрыСведений.МОЛ.СоздатьНаборЗаписей();
// Устанавливаем жесткий отбор по документу-регистратору или документу-владельцу
НаборЗаписей.Отбор.Документ.Установить(ЭтотОбъект.Ссылка);
// Очищаем старые записи перед формированием новых
НаборЗаписей.Записать();
Функция ЗаполнитьЗначенияСвойств(Приемник, Источник) выполняет сопоставление свойств объектов по их именам. По умолчанию она обходит абсолютно все свойства источника. Если источником служит строка табличной части документа, у которой может быть несколько десятков реквизитов, а в регистре нам нужно заполнить всего 3 поля, функция будет выполнять лишние неэффективные итерации.
Для оптимизации скорости работы мы рекомендуем использовать третий параметр этой функции — список заполняемых свойств. Платформа сопоставит только указанные поля, что ускорит работу алгоритма в разы при обработке больших объемов данных.
Применим этот подход на практике:
Для Каждого СтрокаТЧ Из ТаблицаДанных Цикл
НоваяЗапись = НаборЗаписей.Добавить();
// Передаем список только нужных нам полей в качестве третьего параметра
ЗаполнитьЗначенияСвойств(НоваяЗапись, СтрокаТЧ, "ФИО, Склад");
// Заполняем служебные поля, которых нет в ТЧ документа
НоваяЗапись.Период = ЭтотОбъект.Дата;
НоваяЗапись.Документ = ЭтотОбъект.Ссылка;
КонецЦикла;
Соберем все наши правила воедино. Разместим в Модуле объекта нашего документа ПеремещениеМатериалов процедуру ПриЗаписи:
Процедура ПриЗаписи(Отказ)
// Проверим, что документ не помечен на удаление.
// Если помечен — просто очищаем движения в регистре и выходим.
НаборЗаписей = РегистрыСведений.МОЛ.СоздатьНаборЗаписей();
НаборЗаписей.Отбор.Документ.Установить(ЭтотОбъект.Ссылка);
Если ЭтотОбъект.ПометкаУдаления Тогда
НаборЗаписей.Записать(Истина); // Записываем пустой набор для очистки
Возврат;
КонецЕсли;
// Получаем данные напрямую из оперативной памяти
ТаблицаТЧ = ЭтотОбъект.ПМ.Выгрузить();
// Заполняем набор записей
Для Каждого СтрокаТЧ Из ТаблицаТЧ Цикл
// Отфильтруем ненужные строки (например, пустые значения)
Если Не ЗначениеЗаполнено(СтрокаТЧ.МатериальноОтветственноеЛицо) Тогда
Продолжить;
КонецЕсли;
НоваяЗапись = НаборЗаписей.Добавить();
// Заполняем измерения и ресурсы из строки ТЧ
НоваяЗапись.ФИО = СтрокаТЧ.МатериальноОтветственноеЛицо;
НоваяЗапись.Склад = СтрокаТЧ.СкладОтправитель;
// Заполняем ключевые поля связи
НоваяЗапись.Период = ЭтотОбъект.Дата;
НоваяЗапись.Документ = ЭтотОбъект.Ссылка;
КонецЦикла;
// Записываем сформированный набор в базу данных одной транзакцией
Если НаборЗаписей.Количество() > 0 Тогда
НаборЗаписей.Записать(Истина);
Иначе
// Если строк не осталось, очищаем регистр по данному документу
НаборЗаписей.Записать(Истина);
EndIf;
КонецПроцедуры
Если данные в регистре сведений жестко связаны с жизненным циклом документа (при удалении документа данные должны удаляться, при отмене проведения — аннулироваться), то использование независимого регистра является архитектурной ошибкой. Правильнее сделать регистр сведений подчиненным регистратору.
В этом случае вам не придется заботиться об очистке движений вручную при удалении или изменении документа. Платформа сделает это автоматически.
Разберем, как будет выглядеть код в процедуре ОбработкаПроведения в модуле объекта документа:
Процедура ОбработкаПроведения(Отказ, РежимПроведения)
// Указываем платформе, что мы будем записывать движения по регистру МОЛ
Движения.МОЛ.Записывать = Истина;
// Очищаем существующие движения перед записью новых
Движения.МОЛ.Очистить();
ТаблицаТЧ = ЭтотОбъект.ПМ.Выгрузить();
Для Каждого СтрокаТЧ Из ТаблицаТЧ Цикл
НоваяЗапись = Движения.МОЛ.Добавить();
// Поля "Регистратор" и "Период" платформа заполнит автоматически!
НоваяЗапись.Период = ЭтотОбъект.Дата;
// Оптимизированное заполнение свойств
ЗаполнитьЗначенияСвойств(НоваяЗапись, СтрокаТЧ, "МатериальноОтветственноеЛицо, СкладОтправитель");
// Перенаправляем значения в поля регистра, если их имена не совпадают
НоваяЗапись.ФИО = СтрокаТЧ.МатериальноОтветственноеЛицо;
НоваяЗапись.Склад = СтрокаТЧ.СкладОтправитель;
КонецЦикла;
КонецПроцедуры
Часто разработчики спрашивают: "Что произойдет, если я установлю периодичность регистра сведений 'В пределах секунды'?"
Если регистр независимый и периодический (например, с периодичностью "Секунда"), то уникальность записи определяется комбинацией полей: Период + Измерения. Если в одну и ту же секунду будут записаны два разных документа с одинаковыми значениями измерений (например, один и тот же сотрудник и склад), система выдаст ошибку неуникальности данных либо перезапишет старую запись новой.
Чтобы избежать подобных коллизий в независимых регистрах, необходимо добавлять измерение Документ (тип ДокументСсылка.ПеремещениеМатериалов) в состав ключевых измерений регистра. Это гарантирует, что даже при полном совпадении времени, сотрудника и склада, записи останутся уникальными, так как они будут принадлежать разным документам-источникам.
Использование описанных выше подходов позволит вам создавать надежные механизмы интеграции документов с регистрами сведений, которые будут работать без ошибок, быстро и стабильно в любых конфигурациях на платформе 1С:Предприятие 8.3.