Как правильно обойти табличную часть на управляемой форме в 1С 8.3 и избежать ошибок производительности

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

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

Анализ ошибки «Итератор для значения не определен»

Рассмотрим типичную ситуацию, с которой часто начинается разработка обработчиков на управляемой форме. Нам требуется обойти строки табличной части (например, ДанныеОВремени) и заполнить в каждой строке определенный реквизит. Попытка написать код «напрямую» через обращение к элементам формы часто выглядит следующим образом:


// Ошибочный код, вызывающий сбой платформы
Для каждого стр из элементы.ДанныеОВремени.ТекущиеДанные цикл
    стр.Должность = Должность(стр.сотрудник);
конеццикла;

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

Правильный способ обхода табличной части на форме

Для того чтобы обойти все строки табличной части, нам необходимо обращаться не к визуальному элементу формы (коллекции Элементы), а непосредственно к источнику данных этой формы — реквизиту объекта. На управляемой форме все данные документа или справочника проецируются в специальный рабочий реквизит — Объект.

Правильное обращение к коллекции строк табличной части выполняется через конструкцию Объект.ИмяТабличнойЧасти, которая на клиенте имеет тип ДанныеФормыКоллекция. Данный тип данных прекрасно поддерживает итерирование. Рассмотрим рабочий пример правильного цикла:


// Исправленный рабочий вариант обхода ТЧ
Для каждого стр из Объект.ДанныеОВремени цикл
    // Здесь выполняется обработка каждой строки
    стр.Должность = ПолучитьДолжностьСотрудника(стр.Сотрудник);
конеццикла;

Помните важное правило разделения зон ответственности в управляемом приложении: через коллекцию Элементы мы управляем исключительно отображением (видимостью, доступностью, цветом, поведением колонок), а через свойство Объект — непосредственно данными, хранящимися в этих колонках.

Опасная ловушка: Запрос в цикле (Антипаттерн)

Решив проблему с итератором, разработчики часто наступают на другие, более опасные «грабли» — выполняют запросы к базе данных внутри цикла обхода строк. Проанализируем пример неоптимального кода, который приводит к долгому открытию или зависанию формы:


// ВНИМАНИЕ: Данный код является антипаттерном! Так писать нельзя!
Для каждого стр из Объект.ДанныеОВремени цикл
    // На каждой итерации цикла происходит серверный вызов и запрос к БД
    стр.Должность = Должность(стр.Сотрудник); 
конеццикла;

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

Представьте, что в вашем документе (например, в «Табеле учета рабочего времени» в конфигурации ЗУП 3.1) содержится 150 или 200 сотрудников. При открытии такой формы система выполнит 150–200 отдельных, последовательных SQL-запросов к СУБД. Это создает огромные накладные расходы на сетевой трафик, компиляцию планов запросов сервером СУБД и блокировку таблиц. Пользователь будет вынужден секунд 10–15 смотреть на «песочные часы» просто при открытии формы документа. Кроме того, в данном примере функции есть критическая уязвимость: если по сотруднику не будет найден проведенный документ приема на работу, функция вернет пустое значение, но в коде автора темы переменная не была проинициализирована заранее, что могло привести к ошибке выполнения «Переменная не определена».

Оптимальное решение проблемы с помощью пакетной выборки и «Соответствия»

Чтобы кардинально повысить скорость работы алгоритма, мы должны применить классический паттерн оптимизации в 1С: получить все необходимые данные одним запросом, поместить их во временное хранилище в оперативной памяти (объект Соответствие) и затем мгновенно распределить по строкам табличной части без повторных обращений к базе данных.

Разберем этот высокопроизводительный метод по шагам:

  1. Сначала мы соберем все уникальные идентификаторы сотрудников из табличной части в массив. Это позволит нам передать в запрос только тех людей, которые реально присутствуют в документе.
  2. Выполним один-единственный запрос к базе данных, передав этот массив в качестве параметра.
  3. Результаты выполнения запроса выгрузим в объект Соответствие, где ключом будет ссылка на сотрудника, а значением — его должность. Поиск в объекте Соответствие происходит в памяти практически мгновенно (сложность алгоритма O(1)).
  4. В финальном цикле мы просто обходим строки табличной части и считываем данные из нашего соответствия.

Посмотрим на пример реализации данного подхода на стороне сервера:


&НаСервере
Процедура ЗаполнитьДолжностиНаСервере()
    
    // Шаг 1: Собираем массив уникальных сотрудников из ТЧ формы
    МассивСотрудников = Новый Массив;
    Для каждого Стр Из Объект.ДанныеОВремени Цикл
        Если ЗначениеЗаполнено(Стр.Сотрудник) И МассивСотрудников.Найти(Стр.Сотрудник) = Неопределено Тогда
            МассивСотрудников.Добавить(Стр.Сотрудник);
        КонецЕсли;
    КонецЦикла;
    
    Если МассивСотрудников.Количество() = 0 Тогда
        Возврат; // Некого обрабатывать
    КонецЕсли;
    
    // Шаг 2: Выполняем ОДИН запрос ко всей выборке сотрудников
    Запрос = Новый Запрос;
    Запрос.Текст = 
        "ВЫБРАТЬ
        |	КадроваяИстория.Сотрудник КАК Сотрудник,
        |	КадроваяИстория.Должность КАК Должность
        |ИЗ
        |	РегистрСведений.КадроваяИсторияСотрудников.СрезПоследних(&Период, Сотрудник В (&МассивСотрудников)) КАК КадроваяИстория";
    
    Запрос.УстановитьПараметр("МассивСотрудников", МассивСотрудников);
    Запрос.УстановитьПараметр("Период", ?(ЗначениеЗаполнено(Объект.Дата), Объект.Дата, ТекущаяДатаСеанса()));
    
    ТаблицаДолжностей = Запрос.Выполнить().Выгрузить();
    
    // Шаг 3: Заполняем Соответствие для быстрого поиска в памяти
    СоответствиеДолжностей = Новый Соответствие;
    Для каждого СтрокаТаблицы Из ТаблицаДолжностей Цикл
        СоответствиеДолжностей.Вставить(СтрокаТаблицы.Сотрудник, СтрокаТаблицы.Должность);
    КонецЦикла;
    
    // Шаг 4: Обходим ТЧ и быстро заполняем реквизиты без обращений к СУБД
    Для каждого Стр Из Объект.ДанныеОВремени Цикл
        Если ЗначениеЗаполнено(Стр.Сотрудник) Тогда
            НайденнаяДолжность = СоответствиеДолжностей[Стр.Сотрудник];
            Если НайденнаяДолжность <> Неопределено Тогда
                Стр.Должность = НайденнаяДолжность;
            Иначе
                Стр.Должность = Справочники.Должности.ПустаяСсылка();
            КонецЕсли;
        КонецЕсли;
    КонецЦикла;
    
КонецПроцедуры

Специфика разработки кадровых отчетов в «1С:ЗУП 3.1»

Обратите внимание на важную деталь при работе с конфигурацией «Зарплата и управление персоналом 3.1». Автор исходного сообщения делал прямой запрос к таблице документа Документ.ПриемНаРаботу. С точки зрения методологии учета 1С — это грубейшая ошибка. Сотрудник за время своей работы в организации может быть временно или постоянно переведен на другую должность документом КадровыйПеревод, перемещен или уволен. Прямой запрос к первичному документу приема вернет устаревшую или неактуальную информацию.

В ЗУП 3.1 вся история изменений кадровых данных хранится в периодических регистрах сведений (таких как КадроваяИсторияСотрудников). Более того, разработчики фирмы «1С» предоставили готовый и очень удобный программный интерфейс (API) для получения актуальных кадровых сведений на любую дату. Рекомендуется использовать экспортные методы общего модуля КадровыйУчет. Функция КадровыеДанныеСотрудников возвращает готовую таблицу с актуальными должностями, подразделениями и другими параметрами сотрудников на указанную дату среза.

Применение стандартного API гарантирует стабильную работу вашего кода даже после масштабных обновлений конфигурации фирмой «1С».

← На главную