Почему после перехода с 1С 8.1 на 8.2 внешняя обработка стала выбирать не тот склад, и как это исправить?

Программист 1С v8.3 (Обычные формы) 1С:Управление торговлей Управленческий учет Торговля и дистрибуция
← На главную

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

В этой статье мы подробно разберем, почему возникает эта проблема, проанализируем внутренние механизмы работы платформы 1С и СУБД, а также напишем надежное и быстрое решение, которое будет корректно работать на любой современной версии платформы.

В чем суть проблемы: почему «само работало» в 8.1 и сломалось в 8.2?

Для начала проанализируем поведение системы. В конфигурации «Управление торговлей 10.3» используется внешняя обработка автоподбора складов. На складе №3 и на складе №7 лежит один и тот же товар. Логика компании требует списания в первую очередь со склада №3. На платформе 1С 8.1 товар стабильно списывался со склада №3, но после обновления на 8.2 система начала упорно выбирать склад №7.

Обратимся к исходному коду обработки, который выполнялся на 8.1:


Процедура Инициализировать(Объект, ИмяТабличнойЧасти, ТабличноеПолеОбъекта) Экспорт 
    СкладРезерва = Справочники.Склады.НайтиПоКоду("00024"); 
    Если ИмяТабличнойЧасти = "Товары" Тогда 
        Для каждого СтрокаТовары Из Объект.Товары Цикл 
            Отбор = Новый Структура("Номенклатура", СтрокаТовары.Номенклатура); 
            ТаблицаСкладов = РегистрыНакопления.ТоварыНаСкладах.Остатки(, Отбор, "Склад", "Количество"); 
            Если ТаблицаСкладов.Количество <> 0 Тогда 
                Если ТаблицаСкладов[0].Склад <> СкладРезерва Тогда 
                    СтрокаТовары.Склад = ТаблицаСкладов[0].Склад; 
                КонецЕсли; 
            КонецЕсли; 
        КонецЦикла; 
    КонецЕсли; 
КонецПроцедуры

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

Согласно фундаментальным правилам реляционных баз данных (SQL), без явного указания инструкции сортировки (ORDER BY) порядок возвращаемых СУБД строк является случайным (недетерминированным). Почему же в 8.1 все работало «как надо»?

Дело в оптимизации планов запросов:

  1. Оптимизация трансляции запросов к виртуальным таблицам: В платформе 8.2 были существенно переработаны и оптимизированы алгоритмы обращения к виртуальным таблицам остатков. При получении остатков без указания конкретной границы времени 1С 8.2 генерирует прямой SQL-запрос к таблице итогов без создания лишних вложенных подзапросов и группировок, которые присутствовали в 8.1.
  2. Изменение поведения оптимизатора СУБД: Смена структуры SQL-запроса вынудила оптимизатор базы данных (например, MS SQL Server) выбрать другой план выполнения. Если раньше данные физически считывались в порядке кластерного индекса (который случайно совпадал с кодом или внутренним идентификатором нужного склада), то теперь СУБД возвращает данные в том порядке, который наиболее оптимален для её текущего кэша и структуры дисковой памяти. Как результат — первой строкой стал возвращаться склад №7 вместо склада №3.

Ловушка сортировки по умолчанию: почему Сортировать("Склад") не работает?

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

ТаблицаСкладов.Сортировать("Склад Возр");

В результате выполнения такого кода система либо выдает ошибку, либо сортирует склады в абсолютно хаотичном порядке. Выясним причину этого поведения.

Колонка Склад содержит тип данных СправочникСсылка.Склады. Когда мы вызываем метод Сортировать() по ссылочной колонке, платформа 1С выполняет сортировку по внутреннему уникальному идентификатору ссылки (UUID/GUID), а не по наименованию или коду, которые видит пользователь. UUID генерируется платформой автоматически при создании элемента справочника и никак не связан с его алфавитным наименованием или числовым кодом. Для пользователя результат такой сортировки всегда будет выглядеть непредсказуемым.

Чтобы отсортировать таблицу значений стандартными методами, нам пришлось бы сначала добавить в неё колонку с кодом или наименованием склада, заполнить её в цикле и только потом выполнять сортировку. Но это крайне неэффективный путь, нагружающий сервер приложений.

Почему объектный метод .Остатки() — это плохая практика?

Использование конструкции РегистрыНакопления.ТоварыНаСкладах.Остатки() в цикле является грубым нарушением стандартов разработки 1С по следующим причинам:

Правильное решение: Переход на запросы с явным упорядочиванием

Для того чтобы система работала быстро, предсказуемо и не зависела от версии платформы или используемой СУБД, нам необходимо полностью отказаться от объектного метода и переписать код на использование объекта Запрос.

Проанализируем оптимальный алгоритм решения задачи:

  1. Мы должны получить остатки товаров по всем позициям документа за один запрос (чтобы избежать запросов в цикле).
  2. В самом запросе мы должны четко определить приоритет складов с помощью предложения УПОРЯДОЧИТЬ ПО. Сортировать склады мы будем по их коду (Склад.Код) или по наименованию (Склад.Наименование).
  3. В коде мы обойдем результат запроса и распределим склады по строкам документа.

Рассмотрим пример правильной реализации процедуры инициализации табличной части:


Процедура Инициализировать(Объект, ИмяТабличнойЧасти, ТабличноеПолеОбъекта) Экспорт

    Если ИмяТабличнойЧасти <> "Товары" Тогда
        Возврат;
    КонецЕсли;

    // Шаг 1. Подготовим массив товаров из документа, чтобы сделать отбор в запросе
    МассивТоваров = Объект.Товары.ВыгрузитьКолонку("Номенклатура");
    
    // Исключим дубли из массива для оптимизации запроса
    НовыйМассив = Новый Массив;
    СоответствиеГраниц = Новый Соответствие;
    Для каждого ТекТовар Из МассивТоваров Цикл
        Если СоответствиеГраниц[ТекТовар] = Неопределено Тогда
            НовыйМассив.Добавить(ТекТовар);
            СоответствиеГраниц.Вставить(ТекТовар, Истина);
        КонецЕсли;
    КонецЦикла;
    
    Если НовыйМассив.Количество() = 0 Тогда
        Возврат;
    КонецЕсли;

    СкладРезерва = Справочники.Склады.НайтиПоКоду("00025");

    // Шаг 2. Создаем один запрос ко всем остаткам по нужным товарам
    Запрос = Новый Запрос;
    Запрос.Текст = 
        "ВЫБРАТЬ
        |	ТоварыНаСкладахОстатки.Номенклатура КАК Номенклатура,
        |	ТоварыНаСкладахОстатки.Склад КАК Склад,
        |	ТоварыНаСкладахОстатки.КоличествоОстаток КАК КоличествоОстаток
        |ИЗ
        |	РегистрНакопления.ТоварыНаСкладах.Остатки(&Период, Номенклатура В (&МассивТоваров)) КАК ТоварыНаСкладахОстатки
        |ГДЕ
        |	ТоварыНаСкладахОстатки.КоличествоОстаток > 0
        |	И ТоварыНаСкладахОстатки.Склад <> &СкладРезерва
        |
        |УПОРЯДОЧИТЬ ПО
        |	Номенклатура,
        |	ТоварыНаСкладахОстатки.Склад.Код ВОЗР";

    Запрос.УстановитьПараметр("МассивТоваров", НовыйМассив);
    Запрос.УстановитьПараметр("Период", Новый Граница(Объект.Дата, ВидГраницы.Включая));
    Запрос.УстановитьПараметр("СкладРезерва", СкладРезерва);

    // Выгружаем результат в индексированную таблицу для быстрого поиска на сервере
    ТаблицаОстатков = Запрос.Выполнить().Выгрузить();
    ТаблицаОстатков.Индексы.Добавить("Номенклатура");

    // Шаг 3. Заполняем склады в документе на основе полученных остатков
    Для каждого СтрокаТовары Из Объект.Товары Цикл
        
        // Быстро ищем строки остатков по номенклатуре (благодаря индексу это происходит мгновенно)
        ПараметрыПоиска = Новый Структура("Номенклатура", СтрокаТовары.Номенклатура);
        НайденныеСтроки = ТаблицаОстатков.НайтиСтроки(ПараметрыПоиска);
        
        Если НайденныеСтроки.Количество() > 0 Тогда
            // Так как запрос был отсортирован по Склад.Код ВОЗР, 
            // в нулевом индексе гарантированно окажется склад с наименьшим кодом (например, склад №3, а не №7)
            СтрокаТовары.Склад = НайденныеСтроки[0].Склад;
        КонецЕсли;
        
    КонецЦикла;

КонецПроцедуры

Давайте подробно разберем, почему этот код работает стабильно и быстро:

Резюме

Переход на новые версии платформы 1С:Предприятие часто выявляет скрытые ошибки кода, которые раньше маскировались особенностями работы старых платформ. Для написания отказоустойчивых систем всегда придерживайтесь правила: никогда не полагайтесь на порядок строк по умолчанию. Всегда используйте явную сортировку в запросах с помощью оператора УПОРЯДОЧИТЬ ПО и избегайте использования устаревших методов объектного обхода остатков.

← На главную