Каждый разработчик на платформе 1С:Предприятие рано или поздно сталкивается с ошибкой «Поле объекта недоступно для записи». Чаще всего эта проблема возникает при попытке программного изменения реквизитов документов, справочников или строк их табличных частей. На первый взгляд код кажется логичным, но система упорно отказывается выполнять запись и выдает ошибку. В этой статье мы подробно разберем природу данной ошибки, выясним, почему она возникает, проанализируем реальный пример с форума и научимся писать качественный, быстрый и оптимизированный код для пакетного изменения данных.
Для того чтобы понять, почему платформа блокирует запись данных, нам необходимо обратиться к архитектуре хранения и обработки данных в 1С:Предприятии. В системе существует строгое разделение типов данных на ссылочные и объектные. Рассмотрим их принципиальные различия:
ДокументСсылка.РеализацияТоваровУслуг). Это легковесный указатель на запись в базе данных. Ссылка содержит в себе уникальный идентификатор (GUID) и предназначена исключительно для чтения. Мы можем обращаться к реквизитам через точку от ссылки (например, Ссылка.Организация), но изменить их напрямую нельзя. Платформа делает это умышленно, чтобы защитить базу данных от несогласованных изменений. Попытка выполнить команду вида Ссылка.Склад = НовыйСклад; или обратиться к табличной части через ссылку для её изменения гарантированно приведет к ошибке «Поле объекта недоступно для записи».ДокументОбъект.РеализацияТоваровУслуг). Это копия данных конкретной записи из базы данных, полностью загруженная в оперативную память для редактирования. Только объект обладает методами записи, проведения, удаления и только у объекта можно изменять реквизиты и строки табличных частей. Получить объект из ссылки можно с помощью метода ПолучитьОбъект().Давайте наглядно представим это в виде таблицы сравнения:
Таблица сравнения Ссылки и Объекта:
| Критерий | Ссылка (Ссылка) |
Объект (Объект) |
|---|---|---|
| Назначение | Идентификация, чтение данных, отображение в отчетах и формах. | Модификация данных, добавление, удаление, проведение. |
| Расход памяти | Минимальный (содержит только GUID). | Высокий (загружает в память все реквизиты и ТЧ). |
| Доступ к реквизитам | Только для чтения (Read-Only). | Для чтения и записи (Read-Write). |
| Основные методы | ПолучитьОбъект(), Пустая(). |
Записать(), Заблокировать(), Удалить(). |
Таким образом, золотое правило разработчика 1С звучит так: «Читаем через Ссылку, изменяем только через Объект!».
Проанализируем фрагмент кода, с которым автор темы обратился на форум. В его цикле обработки результатов запроса была допущена критическая путаница в переменных. Посмотрим на этот участок кода:
Для каждого стр из РезультатЗапроса Цикл
стр = стр.ссылка.ПолучитьОбъект; // Ошибка 1: Перезапись переменной цикла
стр.Записать(РежимЗаписиДокумента.ОтменаПроведения);
для каждого стр2 из стр.Ссылка.Товары цикл // Ошибка 2: Обращение к табличной части через Ссылку
Если стр2.Склад.Код <> "000000001" тогда
стр2.склад = Справочники.Склады.НайтиПоКоду("000000001", Истина);
КонецЕсли;
КонецЦикла;
КонецЦикла;
Разберем детально, что здесь происходит не так:
стр, присваивая ей объект документа. Это считается плохим стилем программирования, так как запутывает код и затрудняет отладку.для каждого стр2 из стр.Ссылка.Товары цикл. Переменная стр на данном этапе уже содержит Объект документа. Однако автор обращается к свойству Ссылка этого объекта, возвращаясь к ссылочному типу данных, и пытается обойти табличную часть Товары, принадлежащую именно Ссылке.стр2.склад = ... пытается записать новое значение склада в строку табличной части, полученной из ссылки (то есть доступной только для чтения). Платформа мгновенно реагирует на это выбрасыванием исключения «Поле объекта недоступно для записи».Для исправления этой ошибки достаточно было убрать обращение к свойству Ссылка во внутреннем цикле, чтобы обход выполнялся по табличной части самого Объекта:
// Правильное обращение к табличной части объекта
для каждого стр2 из стр.Товары цикл
Также участники форума справедливо заметили, что после внесения изменений в табличную часть объект необходимо повторно записать в базу данных, иначе все произведенные модификации останутся только в оперативной памяти компьютера и будут утеряны при завершении работы процедуры.
Помимо очевидной путаницы между ссылочными и объектными типами, ошибка «Поле объекта недоступно для записи» в реальной практике разработки 1С может возникать по следующим причинам:
НомерСтроки управляется платформой автоматически. Попытка вручную присвоить ему значение (например, СтрокаТЧ.НомерСтроки = 5;) вызовет данную ошибку.Хотя коллеги на форуме помогли автору устранить синтаксическую проблему, исходный код содержит в себе ряд серьезных архитектурных недостатков (антипаттернов). Использование такого кода в реальной рабочей базе данных с большим объемом документов неминуемо приведет к сильному падению производительности, зависаниям интерфейса и блокировкам СУБД.
Давайте проанализируем эти антипаттерны подробнее:
НайтиПоКоду)Внутри вложенного цикла обхода строк табличной части каждого документа автор выполняет строку кода:
стр2.склад = Справочники.Склады.НайтиПоКоду("000000001", Истина);
Метод НайтиПоКоду заставляет платформу делать отдельный полноценный SQL-запрос к базе данных для поиска элемента. Представьте, что в результате запроса мы получили 100 документов, в каждом из которых в среднем по 20 строк товаров. Это означает, что цикл выполнится 2000 раз, и система сделает 2000 лишних и абсолютно одинаковых запросов к базе данных! Это колоссальная и бессмысленная нагрузка на сервер.
Решение: Склад необходимо найти один раз в самом начале процедуры, записать его в переменную и затем использовать эту готовую переменную во всех циклах.
Использование фиксированной строки "000000001" для поиска склада крайне опасно. Коды справочников в процессе работы пользователей могут перенастраиваться (например, при перенумерации или обмене данными). Если в рабочей базе код склада изменится или этот склад будет удален, код перестанет работать. Более того, при переносе обработки в другую базу данных (например, из копии в рабочую) коды могут не совпадать.
Решение: Передавать целевой склад в качестве параметра обработки, использовать предопределенные элементы или настройки (константы, функциональные опции).
Каждый вызов метода Записать() без явного объявления транзакции инициирует открытие и закрытие транзакции на уровне СУБД. При пакетной обработке сотен документов это приводит к сильному замедлению работы, так как диск сервера тратит время на постоянные операции записи логов транзакций.
Решение: Оборачивать весь процесс обработки выборки документов в единую транзакцию с помощью методов НачатьТранзакцию() и ЗафиксироватьТранзакцию().
Автор кода безусловно выполняет отмену проведения для всех документов, попавших в выборку, даже если в табличной части документа нет ни одной строки, требующей изменения склада. Это приводит к лишней нагрузке на систему проведения и нарушает оперативную отчетность. Изменять и перезаписывать нужно только те документы, которые действительно требуют корректировки данных.
Давайте перепишем код автора темы, устранив все архитектурные ошибки и оптимизировав его под стандарты разработки фирмы «1С». Мы добавим транзакционность, исключим запросы в циклах, избавимся от хардкода и сделаем код максимально безопасным и производительным.
Рассмотрим по шагам реализацию оптимизированного алгоритма:
// Шаг 1. Находим целевой склад ОДИН раз до начала выполнения всех циклов
КодЦелевогоСклада = "000000001";
СкладПолучатель = Справочники.Склады.НайтиПоКоду(КодЦелевогоСклада, Истина);
Если СкладПолучатель.Пустая() Тогда
ВызватьИсключение "Критическая ошибка: Целевой склад с кодом """ + КодЦелевогоСклада + """ не найден в справочнике!";
КонецЕсли;
// Шаг 2. Формируем запрос для получения списка документов, требующих корректировки.
// Используем ключевое слово РАЗЛИЧНЫЕ, чтобы исключить дублирование ссылок в выборке.
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ РАЗЛИЧНЫЕ
| РеализацияТоваровУслуг.Ссылка КАК Ссылка
|ИЗ
| Документ.РеализацияТоваровУслуг.Товары КАК РеализацияТоваровУслугТовары
| ВНУТРЕННЕЕ СОЕДИНЕНИЕ Документ.РеализацияТоваровУслуг КАК РеализацияТоваровУслуг
| ПО РеализацияТоваровУслугТовары.Ссылка = РеализацияТоваровУслуг.Ссылка
|ГДЕ
| РеализацияТоваровУслуг.Проведен = ИСТИНА
| И РеализацияТоваровУслуг.ПометкаУдаления = ЛОЖЬ
| И РеализацияТоваровУслугТовары.Склад <> &СкладИсключение";
// Передаем в качестве параметра склад, который нужно исключить (заменить)
Запрос.УстановитьПараметр("СкладИсключение", Склад);
ВыборкаДокументов = Запрос.Выполнить().Выбрать();
// Шаг 3. Открываем явную транзакцию для ускорения записи и защиты целостности данных
НачатьТранзакцию();
Попытка
КоличествоОбработанных = 0;
Пока ВыборкаДокументов.Следующий() Цикл
// Получаем объект документа для редактирования
ДокументОбъект = ВыборкаДокументов.Ссылка.ПолучитьОбъект();
// Вводим флаг модифицированности, чтобы записывать только измененные документы
ДокументМодифицирован = Ложь;
// Обходим строки табличной части непосредственно у ОБЪЕКТА документа
Для каждого СтрокаТовары Из ДокументОбъект.Товары Цикл
Если СтрокаТовары.Склад <> СкладПолучатель Тогда
СтрокаТовары.Склад = СкладПолучатель;
ДокументМодифицирован = Истина;
КонецЕсли;
КонецЦикла;
// Записываем и проводим документ только при наличии реальных изменений
Если ДокументМодифицирован Тогда
// Если документ был проведен, проводим его заново для актуализации регистров накопления
Если ДокументОбъект.Проведен Тогда
ДокументОбъект.Записать(РежимЗаписиДокумента.Проведение);
Иначе
ДокументОбъект.Записать(РежимЗаписиДокумента.Запись);
КонецЕсли;
КоличествоОбработанных = КоличествоОбработанных + 1;
КонецЕсли;
КонецЦикла;
// Фиксируем транзакцию — все изменения успешно сохранены в базе данных
ЗафиксироватьТранзакцию();
Сообщить("Обработка документов успешно завершена! Изменено документов: " + КоличествоОбработанных);
Исключение
// В случае возникновения непредвиденной ошибки откатываем транзакцию полностью
ОтменитьТранзакцию();
// Записываем ошибку в системный журнал регистрации для последующего анализа администратором
ЗаписьЖурналаРегистрации(
"ОбработкаГрупповогоИзмененияСкладов",
УровеньЖурналаРегистрации.Ошибка,
,
,
ПодробноеПредставлениеОшибки(ИнформацияОбОшибке())
);
// Информируем пользователя
ВызватьИсключение "Произошла критическая ошибка во время выполнения транзакции. Все изменения были отменены! Подробности: "
+ КраткоеПредставлениеОшибки(ИнформацияОбОшибке());
КонецПопытки;
Мы детально разобрали ошибку «Поле объекта недоступно для записи» и сопутствующие проблемы производительности кода. Подведем краткие итоги, которые помогут вам избегать подобных ситуаций в будущем:
ПолучитьОбъект() только тогда, когда вам действительно необходимо перезаписать данные. Для простого чтения и вывода на экран пользуйтесь исключительно ссылками — это экономит оперативную память сервера.НайтиПоКоду(), НайтиПоНаименованию(), ПолучитьОбъект()) внутри циклов. Выносите эти действия за рамки циклов, собирайте уникальные идентификаторы в массивы или таблицы значений и делайте пакетные запросы.Записать(). Лишние перезаписи нагружают систему проведения и увеличивают вероятность блокировок таблиц СУБД.Применяя эти простые правила на практике, вы не только навсегда избавитесь от ошибок записи, но и сделаете ваши прикладные решения в среде 1С:Предприятие быстрыми, надежными и масштабируемыми.