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

Программист IT и автоматизация бизнеса
← На главную

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

Анализ исходной проблемы

Рассмотрим ситуацию на примере кода, предоставленного автором темы. Разработчик пытается выгрузить информацию о нескольких документах реализации (X, Y, Z) в текстовый файл. Однако вместо трех разных строк в файле появляются три одинаковые записи, содержащие данные документа Z.

Посмотрим на фрагмент проблемного кода:


Процедура ВыгрузитьНаСервереНал(ссылкаДок) 
    ДокОбъект = ссылкаДок.ПолучитьОбъект(); // Объект получен ОДИН раз до начала цикла
    Текст = Новый ТекстовыйДокумент; 
    Для Каждого Стр Из Таблица Цикл 
        Если (докОбъект.Выгружен = Ложь) Тогда 
            // Здесь данные всегда берутся из одного и того же ДокОбъект
            ХСтрока = "$Тип&Шапка$ДатаДокумента&" + Формат(докОбъект.ДатаДоставки,"ДЛФ=Д") + ...;
            Текст.ДобавитьСтроку(ХСтрока); 
        КонецЕсли; 
    КонецЦикла; 
    // ... запись файла
КонецПроцедуры

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

Решение №1: Исправление логики обращения к данным внутри цикла

Чтобы код работал корректно, нам необходимо обращаться к данным той строки, которую мы в данный момент обходим. Если в колонке Документ вашей таблицы хранится ссылка на соответствующий документ, мы должны извлекать данные именно из этой ссылки.

Рассмотрим исправленный вариант кода:


Процедура ВыгрузитьНаСервереНал() 
    Текст = Новый ТекстовыйДокумент; 
    
    Для Каждого Стр Из Таблица Цикл 
        // Обращаемся к данным ССЫЛКИ, которая содержится в текущей строке таблицы
        ТекущийДокумент = Стр.Документ; 
        
        // Проверяем признак выгрузки непосредственно у документа
        Если Не ТекущийДокумент.Выгружен Тогда 
            ХСтрока = "$Тип&Шапка" 
                    + "$ДатаДокумента&" + Формат(ТекущийДокумент.ДатаДоставки, "ДЛФ=Д") 
                    + "$НомерДок&" + Формат(ТекущийДокумент.Номер, "ЧГ=0") 
                    + "$Контр&" + ТекущийДокумент.Контрагент.Наименование;
            
            Текст.ДобавитьСтроку(ХСтрока); 
        КонецЕсли; 
    КонецЦикла; 

    Если Текст.КоличествоСтрок() > 0 Тогда 
        Текст.Записать("D:\1CBase\SaleN.txt", "windows-1251"); 
    КонецЕсли; 
КонецПроцедуры

В этом примере мы используем переменную Стр, которая является текущим элементом коллекции. Теперь при каждой итерации программа берет данные нового документа.

Решение №2: Использование запроса (Оптимальный метод)

Профессиональная разработка на платформе 1С 8.3 подразумевает отказ от обращения к данным через "точку" (например, Стр.Документ.Контрагент.Наименование) внутри циклов. Каждое такое обращение порождает отдельный запрос к базе данных, что при большом количестве строк (например, более 100) существенно замедляет работу системы.

Рассмотрим "нормальный алгоритм", предложенный опытными участниками форума:

  1. Сбор ссылок: Сначала соберем все ссылки на документы из таблицы, которые еще не были выгрузаны.
  2. Выполнение запроса: Одним запросом получим все необходимые поля (Дата, Номер, Наименование контрагента) для всех найденных документов.
  3. Обход результата: Сформируем текстовый файл на основании выборки из запроса.

Пример реализации через запрос:


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

// Передаем таблицу значений в запрос
Запрос.УстановитьПараметр("ТаблицаДокументов", Таблица.Выгрузить());
РезультатЗапроса = Запрос.Выполнить();
Выборка = РезультатЗапроса.Выбрать();

Текст = Новый ТекстовыйДокумент;
Пока Выборка.Следующий() Цикл
    ХСтрока = "$Тип&Шапка" 
            + "$ДатаДокумента&" + Формат(Выборка.ДатаДоставки, "ДЛФ=Д") 
            + "$НомерДок&" + Формат(Выборка.Номер, "ЧГ=0") 
            + "$Контр&" + Выборка.КонтрагентНаименование;
    Текст.ДобавитьСтроку(ХСтрока);
КонецЦикла;

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

Решение №3: Изменение реквизитов документа (Запись признака выгрузки)

Часто требуется не только выгрузить данные, но и пометить документ в базе как "Выгруженный". Для этого действительно необходимо получать ДокументОбъект, но делать это нужно внутри цикла и только для того документа, который мы обрабатываем в данный момент.

Проанализируем правильную последовательность действий:


Пока Выборка.Следующий() Цикл
    // 1. Формируем строку для файла (как в примерах выше)
    // ...
    
    // 2. Устанавливаем признак выгрузки в базе данных
    ОбъектДок = Выборка.Ссылка.ПолучитьОбъект();
    ОбъектДок.Выгружен = Истина;
    
    Попытка
        ОбъектДок.Записать(РежимЗаписиДокумента.Запись);
    Исключение
        Сообщить("Не удалось записать документ: " + Выборка.Ссылка);
    КонецПопытки;
КонецЦикла;

Важное замечание: Если ваша процедура выполняется на сервере управляемой формы, получение ДокументОбъект не обновит данные на экране пользователя автоматически. Для обновления формы после работы серверного кода используйте метод ОповеститьОбИзменении(Ссылка) или перечитайте данные на клиенте.

Почему разработчик считал, что в 8.2 это работало?

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

Резюмируем ключевые правила:

  1. Никогда не используйте переменную объекта, инициализированную до цикла, для обращения к реквизитам внутри цикла по коллекции.
  2. Внутри цикла всегда обращайтесь к свойствам итератора (переменной, стоящей после Для Каждого).
  3. Для получения данных из связанных справочников или документов внутри цикла используйте запросы к базе данных, чтобы избежать проблем с производительностью.
  4. Если нужно изменить реквизит документа, получайте объект по ссылке из текущей строки прямо в теле цикла, меняйте значение и вызывайте метод Записать().
← На главную