Как исправить ошибку «Поле объекта не обнаружено» при изменении табличной части справочника в цикле?

Программист 1С v8.3 (Управляемые формы) IT и автоматизация бизнеса
← На главную

При программной обработке данных конфигураций 1С начинающие разработчики часто сталкиваются с ошибкой времени выполнения: «Поле объекта не обнаружено (ИмяРеквизита)» при попытке обновить реквизит табличной части справочника или документа в цикле запроса. Разберем природу этой ошибки, выясним причину ее возникновения и подробно рассмотрим правильные способы решения задачи с примерами кода и оптимизацией производительности.

В чем причина ошибки

Проанализируем ситуацию на типовом примере. Разработчик пытается установить цену в табличной части Состав объекта справочника следующим образом:


НоменклатураОбъект = ВыборкаДетальныеЗаписи.Ссылка.ПолучитьОбъект();
НоменклатураОбъект.Состав.Цена = ВыборкаДетальныеЗаписи.ЦенаРегистра;
НоменклатураОбъект.Записать();

В этой конструкции обращение НоменклатураОбъект.Состав возвращает объект типа ТабличнаяЧасть — то есть всю коллекцию строк целиком, а не отдельную строку. У самой коллекции табличной части нет реквизита Цена (она содержит методы вроде Добавить(), Найти(), Очистить() и т. д.). Реквизит Цена принадлежит отдельному элементу этой коллекции — строке табличной части (СтрокаТабличнойЧасти).

Чтобы изменить реквизит, необходимо сначала получить конкретную строку табличной части, изменить ее значение и только после этого записать объект. Рассмотрим различные подходы к реализации этой задачи.

Способ 1. Поиск строки с помощью метода Найти()

Если в строках табличной части ключевой реквизит (например, наименование или номенклатура материала) уникален, самым простым и наглядным решением будет использование метода Найти() коллекции строк. Метод возвращает первую найденную строку или значение Неопределено.

Разберем реализацию этого способа по шагам:

  1. Получаем объект конфигурации через ПолучитьОбъект().
  2. Выполняем поиск строки по нужному значению реквизита.
  3. Если строка найдена, обновляем целевые реквизиты.
  4. Обязательно пересчитываем зависимые суммы, так как события формы при программной записи не срабатывают.
  5. Записываем объект.

Пока ВыборкаДетальныеЗаписи.Следующий() Цикл
    НоменклатураОбъект = ВыборкаДетальныеЗаписи.Ссылка.ПолучитьОбъект();
    
    // Ищем строку по значению реквизита "Наименование"
    НайденнаяСтрока = НоменклатураОбъект.Состав.Найти(ВыборкаДетальныеЗаписи.Наименование, "Наименование");
    
    Если НайденнаяСтрока <> Неопределено Тогда
        НайденнаяСтрока.Цена = ВыборкаДетальныеЗаписи.ЦенаРегистра;
        // Программный пересчет зависимых реквизитов
        НайденнаяСтрока.Стоимость = НайденнаяСтрока.Количество * НайденнаяСтрока.Цена;
        
        НоменклатураОбъект.Записать();
    КонецЕсли;
КонецЦикла;

Способ 2. Поиск строк с помощью метода НайтиСтроки()

Если в табличной части по одному критерию может присутствовать несколько строк либо отбор необходимо выполнять по комбинации нескольких реквизитов (например, по типу цен и номенклатуре), метод НайтиСтроки() подходит лучше всего. Он принимает в качестве параметра Структура и возвращает массив найденных строк.

Посмотрим на пример кода с перебором массива найденных строк:


Пока ВыборкаДетальныеЗаписи.Следующий() Цикл
    НоменклатураОбъект = ВыборкаДетальныеЗаписи.Ссылка.ПолучитьОбъект();
    
    Отбор = Новый Структура;
    Отбор.Вставить("Наименование", ВыборкаДетальныеЗаписи.Наименование);
    
    НайденныеСтроки = НоменклатураОбъект.Состав.НайтиСтроки(Отбор);
    
    Если НайденныеСтроки.Количество() > 0 Тогда
        Для Каждого Стр Из НайденныеСтроки Цикл
            Стр.Цена = ВыборкаДетальныеЗаписи.ЦенаРегистра;
            Стр.Стоимость = Стр.Количество * Стр.Цена;
        КонецЦикла;
        
        НоменклатураОбъект.Записать();
    КонецЕсли;
КонецЦикла;

Способ 3. Прямой доступ по индексу строки

Еще один вариант — передать в запросе системное поле НомерСтроки и обращаться напрямую к элементу коллекции. Однако здесь важно учитывать критическую особенность платформы 1С: поле НомерСтроки нумеруется с единицы, в то время как доступ по индексу через квадратные скобки у коллекции строк табличной части нумеруется с нуля.

Рассмотрим, как правильно обращаться по индексу без вылета за границы массива:


Пока ВыборкаДетальныеЗаписи.Следующий() Цикл
    НоменклатураОбъект = ВыборкаДетальныеЗаписи.Ссылка.ПолучитьОбъект();
    
    // НомерСтроки уменьшаем на 1 для корректного индекса
    ИндексСтроки = ВыборкаДетальныеЗаписи.НомерСтроки - 1;
    
    Если ИндексСтроки >= 0 И ИндексСтроки < НоменклатураОбъект.Состав.Количество() Тогда
        СтрокаТЧ = НоменклатураОбъект.Состав[ИндексСтроки];
        СтрокаТЧ.Цена = ВыборкаДетальныеЗаписи.ЦенаРегистра;
        СтрокаТЧ.Стоимость = СтрокаТЧ.Количество * СтрокаТЧ.Цена;
        
        НоменклатураОбъект.Записать();
    КонецЕсли;
КонецЦикла;

Оптимизация: группировка выборки и пакетная запись

Приведенные выше варианты имеют существенный недостаток производительности: вызов метода ПолучитьОбъект() и последующий Записать() выполняются на каждую итерацию цикла. Если у одного элемента справочника 20 строк состава, объект будет прочитан из базы данных и записан обратно 20 раз, создавая избыточные неявные транзакции и нагружая СУБД.

Проанализируем правильное построение запроса с обходом по группировке ссылок и пакетной модификацией:

  1. В запросе строим иерархическую выборку с помощью секции ИТОГИ ПО Ссылка.
  2. Сначала обходим выборку верхнего уровня (каждую ссылку только один раз).
  3. Получаем объект справочника единожды, затем во внутреннем цикле детальных записей модифицируем все нужные строки табличной части.
  4. Применяем свойство ОбменДанными.Загрузка = Истина, чтобы избежать выполнения ресурсоемких проверок в модуле объекта, если выполняется техническая синхронизация цен.
  5. Один раз вызываем метод Записать() для объекта.

Запрос = Новый Запрос;
Запрос.Текст = 
    "ВЫБРАТЬ
    |	НоменклатураСостав.Ссылка КАК Ссылка,
    |	НоменклатураСостав.НомерСтроки КАК НомерСтроки,
    |	НоменклатураСостав.Наименование КАК Наименование,
    |	СтоимостьМатериалаСрезПоследних.Цена КАК ЦенаРегистра
    |ИЗ
    |	Справочник.Номенклатура.Состав КАК НоменклатураСостав
    |	ЛЕВОЕ СОЕДИНЕНИЕ РегистрСведений.СтоимостьМатериала.СрезПоследних(&ПериодВыборки, Цена > 0) КАК СтоимостьМатериалаСрезПоследних
    |		ПО НоменклатураСостав.Наименование = СтоимостьМатериалаСрезПоследних.Наименование
    |ИТОГИ ПО
    |	Ссылка";

Запрос.УстановитьПараметр("ПериодВыборки", Объект.ПериодВыборки);
Результат = Запрос.Выполнить();

ВыборкаПоСсылкам = Результат.Выбрать(ОбходРезультатаЗапроса.ПоГруппировкам);

Пока ВыборкаПоСсылкам.Следующий() Цикл
    НоменклатураОбъект = ВыборкаПоСсылкам.Ссылка.ПолучитьОбъект();
    
    ВыборкаДетали = ВыборкаПоСсылкам.Выбрать();
    Пока ВыборкаДетали.Следующий() Цикл
        НайденнаяСтрока = НоменклатураОбъект.Состав.Найти(ВыборкаДетали.Наименование, "Наименование");
        Если НайденнаяСтрока <> Неопределено Тогда
            НайденнаяСтрока.Цена = ВыборкаДетали.ЦенаРегистра;
            НайденнаяСтрока.Стоимость = НайденнаяСтрока.Количество * НайденнаяСтрока.Цена;
        КонецЕсли;
    КонецЦикла;
    
    // Отключаем лишние проверки при массовом обновлении цен
    НоменклатураОбъект.ОбменДанными.Загрузка = Истина;
    НоменклатураОбъект.Записать();
КонецЦикла;

Такой подход полностью устраняет ошибку Поле объекта не обнаружено, обеспечивает корректность расчетных сумм и повышает скорость работы алгоритма в десятки раз.

← На главную