При разработке обработок загрузки данных из внешних источников (например, файлов Microsoft Excel) начинающие программисты часто сталкиваются с ситуацией, когда код, казалось бы, написан верно, но платформа выдает ошибку: «Поле объекта недоступно для записи». В этой статье мы подробно разберем, почему возникает эта ошибка, изучим разницу между ссылочными и объектными типами данных в «1С:Предприятии 8», а также научимся правильно обрабатывать исключительные ситуации и оптимизировать код для повышения производительности.
Рассмотрим фрагмент кода, который вызвал затруднения у автора темы:
Н = Справочники.Номенклатура.НайтиПоРеквизиту("Артикул", Артикул1);
Н.НоменклатурнаяГруппа = Группа;
Н.Записать();
При выполнении строки Н.НоменклатурнаяГруппа = Группа; платформа генерирует ошибку. Почему так происходит? Дело в том, что метод НайтиПоРеквизиту() возвращает значение типа СправочникСсылка.Номенклатура.
В системе «1С:Предприятие 8» все типы данных делятся на две важные категории для работы с базой данных:
СправочникСсылка, ДокументСсылка). Ссылка — это неизменяемый уникальный идентификатор записи в базе данных (UUID). Она предназначена исключительно для чтения данных, быстрого вывода информации в списки и установки связей между таблицами. Ссылка «весит» очень мало и не позволяет изменять реквизиты элемента. Пытаясь записать значение в реквизит ссылки, мы получаем ошибку блокировки записи на уровне платформы.СправочникОбъект, ДокументОбъект). Объект — это полная копия данных элемента, загруженная в оперативную память сервера или клиента. Только объектный тип имеет методы Записать(), Удалить() и позволяет редактировать значения реквизитов и табличных частей.Для того чтобы изменить данные элемента, нам необходимо получить его объектное представление из имеющейся ссылки. Делается это с помощью метода ПолучитьОбъект().
Посмотрим на исправленный вариант этого участка кода:
СсылкаНаНоменклатуру = Справочники.Номенклатура.НайтиПоРеквизиту("Артикул", Артикул1);
Если ЗначениеЗаполнено(СсылкаНаНоменклатуру) Тогда
НоменклатураОбъект = СсылкаНаНоменклатуру.ПолучитьОбъект();
НоменклатураОбъект.НоменклатурнаяГруппа = Группа;
НоменклатураОбъект.Записать();
КонецЕсли;
Теперь мы сначала получаем ссылку, проверяем её, затем извлекаем объект базы данных, вносим изменения в реквизит НоменклатурнаяГруппа и успешно сохраняем элемент методом Записать().
Однако на этом тернистый путь разработчика не заканчивается. При изменении логики программы автор столкнулся со следующей проблемой.
После добавления метода ПолучитьОбъект() программа в процессе выполнения может завершиться аварийно с ошибкой: «Ошибка при вызове метода контекста (ПолучитьОбъект) по причине: Элемент не выбран!».
Проанализируем ситуацию. Данная ошибка означает, что метод поиска НайтиПоРеквизиту() не смог найти в справочнике элемент с указанным артикулом. В этом случае метод возвращает так называемую пустую ссылку (Справочники.Номенклатура.ПустаяСсылка()).
У пустой ссылки нет реального физического объекта в базе данных, поэтому при попытке выполнить метод ПолучитьОбъект() платформа закономерно сообщает, что получать нечего — элемент не выбран.
Для решения этой проблемы автор темы попытался использовать конструкцию Попытка ... Исключение ... КонецПопытки. Разберем, почему это является плохой практикой (антипаттерном) и как нужно поступать правильно.
Использование блоков Попытка ... Исключение для контроля логики программы крайне нежелательно по следующим причинам:
Попытка может скрыть другие важные ошибки, например, отсутствие прав доступа у пользователя на запись справочника или блокировку базы данных СУБД. Вы просто не узнаете о реальной причине сбоя.Правильный подход заключается в явной предварительной проверке ссылки на пустоту. В 1С это можно сделать несколькими способами. Рассмотрим их подробнее.
Это самый универсальный и безопасный метод, рекомендуемый стандартами разработки фирмы «1С».
Если ЗначениеЗаполнено(СсылкаНаНоменклатуру) Тогда
// Здесь мы уверены, что ссылка не пустая, и можем безопасно получать объект
НоменклатураОбъект = СсылкаНаНоменклатуру.ПолучитьОбъект();
...
КонецЕсли;
Функция ЗначениеЗаполнено() вернет Ложь, если ссылка является пустой, имеет значение Неопределено или Null. Это предохраняет программу от аварийного завершения.
Каждая ссылка справочника имеет встроенный метод Пустая(). Мы можем использовать его для проверки:
Если Не СсылкаНаНоменклатуру.Пустая() Тогда
НоменклатураОбъект = СсылкаНаНоменклатуру.ПолучитьОбъект();
...
КонецЕсли;
Этот метод работает быстро, однако таит в себе скрытую опасность. Если переменная СсылкаНаНоменклатуру по какой-то причине примет значение Неопределено, программа аварийно завершится с ошибкой «Метод объекта не обнаружен (Пустая)». Поэтому первый способ с функцией ЗначениеЗаполнено() является более надежным.
В редких случаях в базе данных могут существовать так называемые «битые» (или несуществующие) ссылки. Это происходит, когда объект был физически удален из базы данных, но ссылка на него сохранилась в реквизитах других документов или регистров. В такой ситуации ЗначениеЗаполнено() вернет Истина, а метод ПолучитьОбъект() вернет значение Неопределено.
Идеальный шаблон безопасного получения объекта выглядит так:
Если ЗначениеЗаполнено(СсылкаНаНоменклатуру) Тогда
НоменклатураОбъект = СсылкаНаНоменклатуру.ПолучитьОбъект();
Если НоменклатураОбъект <> Неопределено Тогда
// Безопасно меняем реквизиты и записываем
НоменклатураОбъект.НоменклатурнаяГруппа = Группа;
НоменклатураОбъект.Записать();
КонецЕсли;
КонецЕсли;
Представленный в теме код содержит еще одну фундаментальную проблему — выполнение запросов к базе данных и запись объектов внутри цикла Для. При обработке файлов Excel, содержащих тысячи строк, такой алгоритм будет работать крайне медленно. Давайте разберем по шагам, как оптимизировать этот процесс.
Каждый вызов метода НайтиПоРеквизиту() внутри цикла отправляет отдельный запрос к СУБД. Вместо этого правильнее использовать один запрос 1С, передав в него массив всех артикулов из файла Excel.
Сначала мы собираем все уникальные артикулы в массив:
МассивАртикулов = Новый Массив;
Для Строка = НачальнаяСтрока по КонечнаяСтрока цикл
Артикул = СокрЛП(Лист.Cells(Строка, 1).Value);
АртикулОчищенный = СтрЗаменить(Артикул, Символы.НПП, "");
Если ЗначениеЗаполнено(АртикулОчищенный) И МассивАртикулов.Найти(АртикулОчищенный) = Неопределено Тогда
МассивАртикулов.Добавить(АртикулОчищенный);
КонецЕсли;
КонецЦикла;
Затем одним запросом выбираем ссылки на номенклатуру:
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| Номенклатура.Ссылка КАК Ссылка,
| Номенклатура.Артикул КАК Артикул
|ИЗ
| Справочник.Номенклатура КАК Номенклатура
|ГДЕ
| Номенклатура.Артикул В(&МассивАртикулов)";
Запрос.УстановитьПараметр("МассивАртикулов", МассивАртикулов);
ТаблицаРезультатов = Запрос.Выполнить().Выгрузить();
Теперь у нас есть таблица соответствий артикулов и ссылок. Мы можем быстро находить нужную номенклатуру в оперативной памяти с помощью метода Найти() таблицы значений, не обращаясь к базе данных на каждой итерации цикла.
Запись объекта в базу данных — это очень «тяжелая» транзакционная операция. Платформа выполняет проверки уникальности, запускает подписки на события («ПередЗаписью», «ПриЗаписи»), регистрирует изменения для планов обмена. Если у номенклатуры уже установлена нужная номенклатурная группа, перезаписывать элемент нет никакого смысла.
Всегда добавляйте проверку на равенство значений перед записью:
Если НоменклатураОбъект.НоменклатурнаяГруппа <> Группа Тогда
НоменклатураОбъект.НоменклатурнаяГруппа = Группа;
НоменклатураОбъект.Записать();
КонецЕсли;
Если вам все же необходимо массово записать сотни или тысячи объектов, обязательно оборачивайте цикл записи в транзакцию. Это позволит СУБД выполнить запись единым пакетом, что ускорит работу программы в разы.
НачатьТранзакцию();
Попытка
// Цикл модификации и записи объектов справочника
...
ЗафиксироватьТранзакцию();
Исключение
ОтменитьТранзакцию();
Сообщить("Ошибка при записи пакета данных: " + ОписаниеОшибки());
КонецПопытки;
Обратим внимание на финальную часть исходного кода:
Эксель.Application.Quit();
При таком подходе в диспетчере задач операционной системы Windows очень часто остаются «висеть» заброшенные процессы EXCEL.EXE, которые со временем переполняют оперативную память сервера. Это происходит из-за того, что переменные 1С (в нашем случае Лист и Книга) продолжают удерживать ссылки на внутренние COM-объекты Excel.
Чтобы гарантировать полное закрытие приложения MS Excel, необходимо принудительно очищать переменные в обратном порядке, присваивая им значение Неопределено:
Лист = Неопределено;
Книга = Неопределено;
Если Эксель <> Неопределено Тогда
Эксель.Quit();
Эксель = Неопределено;
КонецЕсли;
Этот простой прием позволит содержать вашу операционную систему в чистоте и убережет сервер от нехватки ресурсов.
Подводя итоги, мы подробно разобрали ключевые различия между ссылками и объектами в 1С, научились избегать типичных ошибок начинающих разработчиков при записи данных, освоили методы безопасной проверки ссылок на заполненность, а также изучили основы оптимизации производительности кода при работе с СУБД. Применяйте эти правила на практике, и ваши программы будут работать быстро, надежно и без сбоев!