При разработке и автоматизации задач в среде «1С:Предприятие 8» программисты часто сталкиваются с необходимостью массового заполнения или обновления данных. Типичный сценарий: у контрагента не заполнен основной договор (реквизит ссылочного типа), и нам требуется программно создать новый договор и привязать его к этому контрагенту. Однако при написании интуитивного, на первый взгляд, кода возникает критическая ошибка: «Поле объекта недоступно для записи». В этой статье мы подробно разберем, почему возникает эта ошибка, изучим разницу между ссылками и объектами, а также рассмотрим правильные и оптимизированные алгоритмы решения этой задачи.
Для начала проанализируем код, который часто приводит к ошибке:
ТекКонтрагент = ВыборкаДетальныеЗаписи.Ссылка.ПолучитьОбъект();
ТекКонтрагент.ДоговорПоУмолчанию.НомерДоговора = "1"; // Ошибка возникает здесь!
ТекКонтрагент.ДоговорПоУмолчанию.ДатаДоговора = ТекущаяДата();
ТекКонтрагент.Записать();
В этой конструкции разработчик пытается обратиться к реквизитам договора «через точку» от объекта контрагента. Платформа 1С блокирует это действие и выдает ошибку по двум фундаментальным причинам:
ДоговорПоУмолчанию хранит значение типа СправочникСсылка._ДемоДоговорыКонтрагентов. Ссылка в 1С — это лишь уникальный идентификатор (GUID) записи в базе данных. Когда мы пишем ТекКонтрагент.ДоговорПоУмолчанию.НомерДоговора, платформа выполняет неявное считывание данных из базы (разыменование ссылки). Любое обращение к полям через ссылку доступно строго в режиме «только для чтения». Изменить данные в базе можно исключительно через объект (СправочникОбъект), полученный с помощью метода ПолучитьОбъект().ДоговорПоУмолчанию не заполнен, он содержит значение ПустаяСсылка. У пустой ссылки физически нет записи в базе данных. Попытка прочитать или изменить ее свойства бессмысленна, так как объекта, который можно было бы изменить или получить, еще не существует в природе.Таким образом, чтобы решить задачу, нам нужно не перезаписывать свойства существующей ссылки, а создать полноценный новый элемент справочника договоров, заполнить его реквизиты, записать в базу данных и только после этого привязать полученную ссылку на новый договор к нашему контрагенту.
Чтобы корректно создать подчиненный элемент справочника (в нашем случае — договор, подчиненный контрагенту) и привязать его к владельцу, мы должны применить следующий пошаговый алгоритм:
СоздатьЭлемент(). Это инициализирует в оперативной памяти новый объект типа СправочникОбъект.Владелец. В него мы передаем ссылку на контрагента.Записать() для объекта договора. В этот момент запись физически создается в базе данных, и у договора появляется уникальная ссылка.ПолучитьОбъект() (если у нас в цикле обхода выборки доступна только ссылка), записываем полученную ссылку на новый договор в реквизит контрагента ДоговорПоУмолчанию.Записать() для объекта контрагента, чтобы зафиксировать изменения в базе данных.Рассмотрим простую и понятную программную реализацию описанного алгоритма в цикле обработки выборки данных:
Пока ВыборкаДетальныеЗаписи.Следующий() Цикл
СсылкаКонтрагента = ВыборкаДетальныеЗаписи.Ссылка;
// Шаг 1. Создаем новый элемент в справочнике договоров
НовыйДоговор = Справочники._ДемоДоговорыКонтрагентов.СоздатьЭлемент();
// Шаг 2. Задаем владельца (контрагента)
НовыйДоговор.Владелец = СсылкаКонтрагента;
// Шаг 3. Заполняем реквизиты договора
НовыйДоговор.НомерДоговора = "1";
НовыйДоговор.ДатаДоговора = ТекущаяДата();
НовыйДоговор.Наименование = "Основной договор № 1 от " + Формат(ТекущаяДата(), "ДФ=dd.MM.yyyy");
// Шаг 4. Записываем договор в базу данных
НовыйДоговор.Записать();
// Шаг 5. Получаем объект контрагента и устанавливаем созданный договор по умолчанию
ОбъектКонтрагент = СсылкаКонтрагента.ПолучитьОбъект();
ОбъектКонтрагент.ДоговорПоУмолчанию = НовыйДоговор.Ссылка;
// Шаг 6. Сохраняем изменения контрагента
ОбъектКонтрагент.Записать();
КонецЦикла;
Этот код полностью работоспособен и решает поставленную задачу без вызова ошибок. Однако при работе с реальными информационными базами, где количество контрагентов может исчисляться тысячами, такой подход будет работать крайне медленно. Каждая запись объекта производит обращение к диску и запускает многочисленные подписки на события и проверки. Перейдем к профессиональной оптимизации этого процесса. В ситуациях, когда задачу необходимо выполнить непосредственно в пользовательском интерфейсе без написания программного кода, применяется платформа для создания программных роботов One RPA.
Проанализируем профессиональный подход к написанию подобных обработок. Мы применим транзакции для ускорения записи, режим загрузки данных для отключения лишних проверок, а также добавим проверку на дубликаты договоров, чтобы не захламлять базу данных лишней информацией. В задачах точечной миграции объектов между похожими базами без создания дублей применяется обработка для переноса данных между похожими конфигурациями 1С с настройкой произвольных полей поиска.
Перед созданием нового договора крайне важно проверить: возможно, у контрагента уже есть созданные договора в системе, но реквизит ДоговорПоУмолчанию просто остался незаполненным. В таком случае правильнее не плодить новые элементы, а привязать уже существующий договор.
Посмотрим на оптимизированный программный код:
Процедура ВыполнитьКоманду(ИдентификаторКоманды, ОбъектыНазначения, ПараметрыВыполненияКоманды) Экспорт
// Шаг 1. Выбираем контрагентов без договоров по умолчанию,
// а также попутно пытаемся найти любой уже существующий договор этого контрагента
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| Контрагенты.Ссылка КАК Ссылка,
| МИНИМУМ(СуществующиеДоговоры.Ссылка) КАК СуществующийДоговор
|ИЗ
| Справочник._ДемоКонтрагенты КАК Контрагенты
| ЛЕВОЕ СОЕДИНЕНИЕ Справочник._ДемоДоговорыКонтрагентов КАК СуществующиеДоговоры
| ПО СуществующиеДоговоры.Владелец = Контрагенты.Ссылка
| И НЕ СуществующиеДоговоры.ПометкаУдаления
|ГДЕ
| НЕ Контрагенты.ЭтоГруппа
| И Контрагенты.ДоговорПоУмолчанию = ЗНАЧЕНИЕ(Справочник._ДемоДоговорыКонтрагентов.ПустаяСсылка)
|ГРУППИРОВАТЬ ПО
| Контрагенты.Ссылка";
РезультатЗапроса = Запрос.Run();
Выборка = РезультатЗапроса.Выбрать();
// Использование транзакции ускоряет запись в десятки раз
НачатьТранзакцию();
Попытка
Пока Выборка.Следующий() Цикл
СсылкаКонтрагента = Выборка.Ссылка;
СсылкаНаДоговор = ЗначениеЗаполнено(Выборка.СуществующийДоговор) ? Выборка.СуществующийДоговор : Неопределено;
// Если у контрагента вообще нет ни одного договора — создаем новый
Если СсылкаНаДоговор = Неопределено Тогда
НовыйДоговор = Справочники._ДемоДоговорыКонтрагентов.СоздатьЭлемент();
НовыйДоговор.Владелец = СсылкаКонтрагента;
НовыйДоговор.НомерДоговора = "1";
НовыйДоговор.ДатаДоговора = ТекущаяДата();
НовыйДоговор.Наименование = "Основной договор № 1 от " + Формат(ТекущаяДата(), "ДФ=dd.MM.yyyy");
// Свойство ОбменДанными.Загрузка = Истина отключает тяжелые прикладные проверки при записи
НовыйДоговор.ОбменДанными.Загрузка = Истина;
НовыйДоговор.Записать();
СсылкаНаДоговор = НовыйДоговор.Ссылка;
КонецЕсли;
// Получаем объект контрагента для обновления ссылки на договор
ОбъектКонтрагент = СсылкаКонтрагента.ПолучитьОбъект();
ОбъектКонтрагент.ДоговорПоУмолчанию = СсылкаНаДоговор;
// Отключаем лишние обработчики событий для ускорения выполнения групповой операции
ОбъектКонтрагент.ОбменДанными.Загрузка = Истина;
ОбъектКонтрагент.Записать();
КонецЦикла;
// Фиксируем все изменения в базе данных одним пакетом
ЗафиксироватьТранзакцию();
Сообщить("Заполнение договоров по умолчанию успешно завершено!");
Исключение
// В случае любой непредвиденной ошибки откатываем все изменения назад
ОтменитьТранзакцию();
ЗаписатьЛогСобытий(ОписаниеОшибки()); // Процедура логирования разработчика
ВызватьИсключение "Не удалось выполнить обработку. Причина: " + ОписаниеОшибки();
КонецПопытки;
КонецПроцедуры
Разберем детально, за счет чего оптимизированный код работает в разы быстрее и безопаснее классического решения:
НачатьТранзакцию() / ЗафиксироватьТранзакцию()): По умолчанию каждая запись любого объекта в 1С открывает свою внутреннюю неявную транзакцию. Если у вас 1000 контрагентов, база данных выполнит 2000 транзакций (запись договора и запись контрагента). Это порождает колоссальную нагрузку на жесткий диск и СУБД из-за постоянных операций подтверждения записи (commit). Обернув весь цикл в одну общую явную транзакцию, мы заставляем СУБД выполнить запись всей пачки данных в оперативной памяти и зафиксировать ее на диск единовременно. Скорость работы увеличивается в 10–50 раз.ОбменДанными.Загрузка = Истина: Это важнейший инструмент при написании сервисных обработок по массовому изменению данных. Установка этого свойства сообщает платформе, что объект записывается в режиме импорта данных. При этом отключаются все тяжелые платформенные проверки заполнения реквизитов, кастомные обработчики событий в модулях объектов (например, процедуры ПередЗаписью или ПриЗаписи), а также подписки на события конфигурации (например, регистрация изменений для обменов). Это не только ускоряет процесс, но и предотвращает появление случайных ошибок, связанных с бизнес-логикой конфигурации, которая не рассчитана на подобные массовые программные корректировки.ЛЕВОЕ СОЕДИНЕНИЕ со справочником договоров в запросе. Благодаря этому мы сразу видим, есть ли у конкретного контрагента хоть один живой договор. Если он найден, мы просто устанавливаем его по умолчанию в поле ДоговорПоУмолчанию без создания нового элемента. Это защищает базу данных от накопления «мусорных» дублирующихся записей.Применяя данные подходы в своей ежедневной практике разработки на платформе «1С:Предприятие 8», вы навсегда избавитесь от досадных ошибок обращения к реквизитам через ссылки и обеспечите высокую скорость работы ваших алгоритмов даже на высоконагруженных и объемных информационных базах. Для контроля корректности итоговых проводок после подобных массовых корректировок на больших объемах данных применяется автоматическая сверка бухгалтерских и налоговых данных.