Как исправить ошибку расчета цен в цикле по табличной части в 1С и оптимизировать вызовы сервера

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

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

Выясняем причину ошибки: почему цены одинаковые?

Рассмотрим исходный код, который вызвал проблему у автора темы:


&НаСервереБезКонтекста
Функция ТЧРНоменклатураПриИзмененииНаСервере(Дата, Номенклатура)
    Возврат РегистрыСведений.Цены.ПолучитьПоследнее(Дата, Новый Структура("Номенклатура", Номенклатура)).Цена;
КонецФункции

&НаКлиенте
Процедура ПроцентПриИзменении(Элемент)    
    Для Каждого Строка Из Объект.ТЧРасход Цикл
        ТД = Элементы.ТЧРасход.ТекущиеДанные;
        А = ТЧРНоменклатураПриИзмененииНаСервере(Объект.Дата, ТД.Номенклатура);
        Строка.Цена = А - А /100 * Объект.Процент;    
    КонецЦикла;
КонецПроцедуры

Проанализируем этот фрагмент. В цикле Для Каждого Строка Из Объект.ТЧРасход Цикл происходит обход строк табличной части. Однако для получения номенклатуры разработчик обращается к конструкции Элементы.ТЧРасход.ТекущиеДанные (переменная ТД).

Свойство ТекущиеДанные возвращает данные активной строки, выделенной пользователем в интерфейсе в данный момент. Пока выполняется цикл на клиенте, активная строка в таблице физически не меняется. В результате на каждой итерации цикла функция ТЧРНоменклатураПриИзмененииНаСервере получает одну и ту же номенклатуру из выделенной строки. Рассчитанная для нее цена затем присваивается всем строкам подряд.

Быстрое решение проблемы: чтобы исправить эту ошибку, необходимо брать номенклатуру непосредственно из текущего итератора цикла — переменной Строка. Вместо кода ТД.Номенклатура следует использовать Строка.Номенклатура.

Антипаттерн: вызов сервера в цикле на клиенте

Хотя замена ТД.Номенклатура на Строка.Номенклатура решает функциональную проблему, такой код категорически не рекомендуется использовать в реальных задачах. В сообщении 5 участники форума справедливо отмечают высокую неоптимальность данного решения.

Каждое обращение к серверу через директиву &НаСервереБезКонтекста сопровождается следующими накладными расходами:

  1. Сетевые задержки (RTT): Передача пакетов по сети между клиентом и сервером приложений занимает время. Если в таблице 100 строк, клиент выполнит 100 последовательных сетевых запросов. На медленных каналах связи (например, при работе через VPN или мобильный интернет) это приведет к зависанию интерфейса на десятки секунд.
  2. Сериализация данных: Платформа тратит ресурсы процессора на упаковку параметров в XML/JSON на клиенте и их распаковку на сервере, а затем на обратную отправку результата.
  3. Нагрузка на СУБД: Вместо одного пакетного запроса СУБД вынуждена выполнять множество одиночных мелких запросов, что снижает общую производительность системы.

Рассмотрим два правильных паттерна проектирования для решения этой задачи.

Решение 1. Пакетный запрос цен на сервере + расчет на клиенте (Рекомендуемый гибридный подход)

Этот способ позволяет получить все необходимые цены за один единственный вызов сервера, минимизируя объем передаваемых данных. Мы соберем массив уникальной номенклатуры на клиенте, передадим его на сервер, получим цены в виде соответствия и быстро пересчитаем их на клиенте.

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


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

&НаКлиенте
Процедура ПроцентПриИзменении(Элемент)
    
    // Шаг 1. Собираем только уникальные ссылки на номенклатуру, чтобы не передавать дубли
    МассивНоменклатуры = Новый Массив;
    Для Каждого Строка Из Объект.ТЧРасход Цикл
        Если ЗначениеЗаполнено(Строка.Номенклатура) Тогда
            // Проверяем, чтобы избежать повторного добавления
            Если МассивНоменклатуры.Найти(Строка.Номенклатура) = Неопределено Тогда
                МассивНоменклатуры.Добавить(Строка.Номенклатура);
            КонецЕсли;
        КонецЕсли;
    КонецЦикла;
    
    Если МассивНоменклатуры.Количество() = 0 Тогда
        Возврат;
    КонецЕсли;
    
    // Шаг 2. Один раз обращаемся к серверу для получения всех цен сразу
    СоответствиеЦен = ПолучитьЦеныНоменклатурыНаСервере(Объект.Дата, МассивНоменклатуры);
    
    // Шаг 3. Быстро рассчитываем цены в цикле на клиенте без обращения к серверу
    Для Каждого Строка Из Объект.ТЧРасход Цикл
        Если ЗначениеЗаполнено(Строка.Номенклатура) Тогда
            БазоваяЦена = 0;
            // Ищем цену в полученном соответствии
            Если СоответствиеЦен.Свойство(Строка.Номенклатура, БазоваяЦена) Тогда
                Строка.Цена = БазоваяЦена - (БазоваяЦена / 100 * Объект.Процент);
            Иначе
                Строка.Цена = 0; // Если цена в регистре не найдена
            EndIf;
        КонецЕсли;
    КонецЦикла;
    
КонецПроцедуры

Важная техническая деталь: Обратите внимание на то, как наложен отбор в запросе на сервере. Мы передаем массив номенклатуры непосредственно в параметры виртуальной таблицы: РегистрСведений.Цены.СрезПоследних(&Период, Номенклатура В (&МассивНоменклатуры)). Это критически важно для производительности СУБД. Если наложить отбор через конструкцию ГДЕ после выбора таблицы, СУБД сначала построит срез последних по всей номенклатуре базы данных, а уже потом отфильтрует ее, что приведет к колоссальным нагрузкам на больших объемах данных.

Решение 2. Перенос всей логики обработки на сервер (Контекстный подход)

Если табличная часть содержит огромное количество строк (сотни или тысячи) или алгоритмы расчета цен завязаны на сложные бизнес-правила (со скидками, накопительными программами и т.д.), эффективнее выполнить всю операцию в контексте сервера. Платформа 1С оптимизирует обратную передачу измененных данных, отправляя клиенту только обновленные ячейки строк.

Рассмотрим пример контекстной обработки:


&НаСервере
Процедура ПересчитатьЦеныНаСервере()
    
    // Выгружаем ТЧ в таблицу значений для обработки в запросе
    ТаблицаТоваров = Объект.ТЧРасход.Выгрузить( , "Номенклатура, Цена");
    
    Запрос = Новый Запрос;
    Запрос.Текст = 
        "ВЫБРАТЬ
        |	ВТ_Товары.Номенклатура КАК Номенклатура
        |ПОМЕСТИТЬ ВТ_Товары
        |ИЗ
        |	&ТаблицаТоваров КАК ВТ_Товары
        |;
        |
        |////////////////////////////////////////////////////////////////////////////////
        |ВЫБРАТЬ
        |	ВТ_Товары.Номенклатура КАК Номенклатура,
        |	ЕСТЬNULL(ЦеныСрезПоследних.Цена, 0) КАК БазоваяЦена
        |ИЗ
        |	ВТ_Товары КАК ВТ_Товары
        |		ЛЕВОЕ СОЕДИНЕНИЕ РегистрСведений.Цены.СрезПоследних(&Период, Номенклатура В (ВЫБРАТЬ Номенклатура ИЗ ВТ_Товары)) КАК ЦеныСрезПоследних
        |		ПО ВТ_Товары.Номенклатура = ЦеныСрезПоследних.Номенклатура";
        
    Запрос.УстановитьПараметр("Период", Объект.Дата);
    Запрос.УстановитьПараметр("ТаблицаТоваров", ТаблицаТоваров);
    
    РезультатЗапроса = Запрос.Выполнить().Выгрузить();
    
    // Обновляем данные в исходной табличной части формы
    Для Каждого СтрокаТЧ Из Объект.ТЧРасход Цикл
        СтрокаПоиска = РезультатЗапроса.Найти(СтрокаТЧ.Номенклатура, "Номенклатура");
        Если СтрокаПоиска <> Неопределено Тогда
            БазоваяЦена = СтрокаПоиска.БазоваяЦена;
            СтрокаТЧ.Price = БазоваяЦена - (БазоваяЦена / 100 * Объект.Процент);
        КонецЕсли;
    КонецЦикла;
    
КонецПроцедуры

&НаКлиенте
Процедура ПроцентПриИзменении(Элемент)
    ПересчитатьЦеныНаСервере();
КонецПроцедуры

Подведем итоги

Для создания быстрого и качественного кода всегда следуйте трем золотым правилам работы с табличными частями в 1С:

← На главную