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

Программист 1С v8.3 (Обычные формы) 1C:Бухгалтерия Бухгалтерский учет IT и автоматизация бизнеса
← На главную

Каждый разработчик 1С хотя бы раз сталкивался с ситуацией, когда написанный код «зацикливается», намертво вешая сеанс пользователя или перегружая сервер СУБД. В такой ситуации первая мысль часто падает на ошибку в конструкции самого цикла Пока ... Цикл. Однако в среде 1С:Предприятие корни этой проблемы почти всегда уходят гораздо глубже — в неоптимально написанный запрос или некорректно выстроенную логику обхода выборки.

В этой статье мы подробно разберем реальный кейс, когда обработка по автоматическому созданию и перезаписи документов Приходный кассовый ордер (ПКО) на основании Отчетов о розничных продажах (ОРП) уходит в бесконечный цикл. Мы проанализируем неочевидные ошибки исходного кода, исправим опасные антипаттерны производительности и напишем чистый, быстрый и отказоустойчивый алгоритм.

В чем суть проблемы: почему цикл «крутит» без остановки?

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


Пока ВыборкаДетальныеЗаписи.Следующий() Цикл            
    // Логика поиска, изменения и записи ПКО...
КонецЦикла;

Почему же он не останавливается? Проблема кроется в самом запросе, а именно — в получении аналитики (субконто) из табличной части документа. Давайте детально разберем каждую ошибку исходного кода и найдем пути их решения.

---

Анализ и устранение критических ошибок в коде

Ошибка 1. Декартово произведение (Cross Join) в запросе

Самая главная ошибка, из-за которой выборка запроса раздулась до гигантских масштабов, находится в секции ИЗ исходного запроса:


ИЗ
    Документ.ПриходныйКассовыйОрдер КАК ПриходныйКассовыйОрдер
        ЛЕВОЕ СОЕДИНЕНИЕ Документ.ОтчетОРозничныхПродажах КАК ОтчетОРозничныхПродажах
        ПО ПриходныйКассовыйОрдер.ДокументОснование = ОтчетОРозничныхПродажах.Ссылка,
    Документ.ОтчетОРозничныхПродажах.Товары КАК ОтчетОРозничныхПродажахТовары

Обратите внимание на запятую перед таблицей табличной части Документ.ОтчетОРозничныхПродажах.Товары. Перечисление таблиц через запятую без указания явного условия соединения (ПО...) заставляет СУБД выполнять декартово произведение (Cross Join).

В результате каждая строка таблицы ПКО умножается на абсолютно все строки табличной части товаров всех ОРП, присутствующих в базе данных. Если в базе было 1 000 документов ПКО и всего 10 000 строк в товарах, результатом запроса станут 10 000 000 строк! Программа не зависает в бесконечном цикле — она просто добросовестно пытается обработать миллионы фиктивных дублирующихся записей.

Решение: Связывать табличную часть необходимо строго через оператор ЛЕВОЕ СОЕДИНЕНИЕ по ключу связи Ссылка:


ЛЕВОЕ СОЕДИНЕНИЕ Документ.ОтчетОРозничныхПродажах.Товары КАК ОтчетОРозничныхПродажахТовары
    ПО ОтчетОРозничныхПродажахТовары.Ссылка = ОтчетОРозничныхПродажах.Ссылка

Ошибка 2. Неправильное направление Левого соединения (Left Join)

В первоначальном запросе автора соединение начинается от таблицы ПКО к ОРП:


Документ.ПриходныйКассовыйОрдер КАК ПриходныйКассовыйОрдер
    ЛЕВОЕ СОЕДИНЕНИЕ Документ.ОтчетОРозничныхПродажах КАК ОтчетОРозничныхПродажах

При такой структуре левого соединения мы получим только те ОРП, для которых уже созданы документы ПКО. Но цель обработки — найти ОРП, для которых ПКО еще нет, и создать их! Новые документы никогда не создадутся, так как левое соединение от ПКО отсечет все отчеты о продажах без связанных кассовых ордеров.

Решение: Ведущей (левой) таблицей в запросе должен выступать документ-основание — Документ.ОтчетОРозничныхПродажах, а ПКО должен присоединяться к нему справа.

Ошибка 3. Антипаттерн «Запрос в цикле» (НайтиПоРеквизиту)

Внутри цикла обхода выборки мы видим следующую строку:


ЕстьПКО = Документы.ПриходныйКассовыйОрдер.НайтиПоРеквизиту("ДокументОснование", ВыборкаДетальныеЗаписи.Ссылка);

Использование метода НайтиПоРеквизиту внутри цикла — это грубейшая ошибка производительности. На каждом шаге цикла система выполняет отдельный неявный запрос к базе данных. Если документов в выборке много, обработка будет работать крайне медленно.

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

Ошибка 4. Потенциальный краш при получении объекта от NULL

В коде автора вызывается метод получения объекта:


СуществующийДокумент = ЕстьПКО.ПолучитьОбъект();

Если ПКО не будет найден, метод НайтиПоРеквизиту вернет пустую ссылку. Попытка получить объект от пустой ссылки приведет к значению Неопределено. На следующем шаге при вызове СуществующийДокумент.Заполнить(...) программа аварийно завершит работу с ошибкой: «Значение не является значением объектного типа».

Решение: Получать объект только после проверки заполненности ссылки с помощью функции ЗначениеЗаполнено(), либо опираться на данные, заранее отобранные запросом.

Ошибка 5. Дублирование записей при соединении с табличной частью

Даже если мы правильно свяжем табличную часть Товары по ссылке, у одного ОРП может быть много строк в табличной части. Это приведет к тому, что документ ОРП продублируется в выборке столько раз, сколько товаров в нем указано. Цикл будет обрабатывать и записывать один и тот же ПКО многократно.

Решение: Чтобы избежать дублирования документов, запрос необходимо обязательно сгруппировать по ссылке документа ОРП, а для получения субконто использовать агрегатную функцию (например, МАКСИМУМ или МИНИМУМ).

---

Пошаговое руководство по исправлению кода

Разберем по шагам, как построить надежный алгоритм без лишней нагрузки на систему.

Шаг 1. Проектирование правильного запроса

Спроектируем запрос так, чтобы он сразу решал все наши задачи: отдавал только уникальные документы ОРП за период, показывал связь с уже существующими ПКО (если они есть) и возвращал нужное значение аналитики (субконто) без дублирования строк.

Используем группировку СГРУППИРОВАТЬ ПО и агрегатную функцию МАКСИМУМ для поля Субконто:


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

СГРУППИРОВАТЬ ПО
    ОтчетОРозничныхПродажах.Ссылка,
    ОтчетОРозничныхПродажах.Дата,
    ПриходныйКассовыйОрдер.Ссылка,
    ПриходныйКассовыйОрдер.ПометкаУдаления

Шаг 2. Разработка безопасного алгоритма обработки выборки

В самом цикле нам больше не нужно выполнять поиск. Мы просто проверяем заполненность поля СсылкаПКО, полученного из запроса:

  1. Если СсылкаПКО заполнена — документ уже существует. Мы либо перезаписываем его (если включен соответствующий флаг на форме), либо пропускаем.
  2. Если СсылкаПКО пустая — создаем новый документ ПКО, заполняем его на основании ОРП и проводим.

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

---

Итоговый эталонный вариант кода обработки

Проанализируем готовое оптимизированное решение, которое можно перенести в свою обработку:


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

Резюме: Мы полностью устранили декартово произведение, избавились от тяжелых запросов в цикле с использованием метода НайтиПоРеквизиту, добавили безопасные проверки заполненности объектов СУБД и предотвратили дублирование обработки документов ОРП. Теперь алгоритм работает стабильно, предсказуемо и не вызывает зависаний системы.

← На главную