Как исправить ошибки в коде поиска товаров и аналогов: избавляемся от "запросов в цикле" и несуществующих методов

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

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

Разберем конкретный пример из практики: попытку реализовать подбор лекарств и их аналогов с получением цен и остатков. Мы проанализируем, почему код, написанный "с помощью ИИ", не работает, и напишем правильное решение.

Анализ ошибок: почему код не работает?

В исходном коде автора можно выделить три критические проблемы. Рассмотрим их подробно, чтобы вы могли избегать их в будущем.

  1. Галлюцинации синтаксиса (Несуществующие методы).

    В коде встречается конструкция:

    
    Остатки = РегистрыНакопления.Остатки.ОстаткиПоСрезамПоследних(...);
    

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

    • У регистров сведений (периодических) есть виртуальная таблица СрезПоследних.
    • У регистров накопления есть виртуальная таблица Остатки.

    Метода ОстаткиПоСрезамПоследних не существует. Программа просто упадет с ошибкой компиляции или исполнения.

  2. Проблема производительности "N+1" (Запрос в цикле).

    Это самая распространенная архитектурная ошибка. Посмотрите на структуру алгоритма:

    
    // 1. Сначала выполняется запрос для поиска лекарств
    Пока Выборка.Следующий Цикл
        // 2. ВНУТРИ цикла вызывается метод получения остатков (обращение к БД)
        // 3. Затем вызывается процедура ДобавитьАналоги
            // 4. В которой ВНУТРИ цикла снова делается Запрос к регистру цен
            // 5. И снова обращение к регистру остатков
    КонецЦикла;
    

    Если поиск вернет 100 лекарств, платформа сделает сотни обращений к базе данных. Это работает крайне медленно. Правильный подход — получать все данные (и товары, и цены, и остатки) одним запросом.

  3. Ошибка логики: изменение коллекции при итерации.

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

    
    Для Каждого СтрокаТаблицы Из Таблица Цикл
        ДобавитьАналоги(Таблица, СтрокаТаблицы.Наименование);
    КонецЦикла;
    

    Если аналог добавляется в конец таблицы, цикл может пойти по новому кругу, обрабатывая только что добавленный аналог, снова искать его аналоги и так до переполнения памяти (если будут перекрестные ссылки). Аналоги нужно собирать отдельно или получать сразу в запросе.

Правильное архитектурное решение

Чтобы решить задачу грамотно, нам нужно отказаться от перебора в коде в пользу языка запросов. Мы должны соединить таблицы справочников и регистров.

Алгоритм действий будет следующим:

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

Реализация решения

Рассмотрим, как должен выглядеть правильный код серверной функции. Вместо десятков разрозненных вызовов мы напишем один пакетный запрос.

Шаг 1. Подготовка запроса для основных товаров

Мы будем использовать ЛЕВОЕ СОЕДИНЕНИЕ, чтобы не потерять товары, у которых нет остатка или не установлена цена (если нам нужно показывать и отсутствующие товары). Если нужны только товары в наличии, используется ВНУТРЕННЕЕ СОЕДИНЕНИЕ.


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

Однако задача усложняется требованием вывода аналогов. В исходной задаче аналоги хранятся в табличной части справочника Лекарства.

Шаг 2. Как получить аналоги без запроса в цикле?

Есть два путя. Первый — сложный запрос с объединением. Второй, более понятный для обучения — пост-обработка результата, но без запросов внутри цикла.

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

Рассмотрим вариант решения, предложенный опытными разработчиками: получить таблицу остатков и цен отдельно, а потом программно сопоставить данные. Но самый "чистый" вариант — сделать это через пакетный запрос или ОБЪЕДИНИТЬ ВСЕ.

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


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

Разбор кода решения

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

  1. Временная таблица ВТ_Найденные: Мы один раз фильтруем справочник по условиям пользователя (Название, ДВ, Назначение).
  2. Объединение (UNION ALL): Мы собираем в одну таблицу ВТ_Итоговая и сами найденные лекарства, и все их аналоги (взятые из табличной части). При этом мы не делаем лишних обращений к базе.
  3. Получение остатков и цен: Мы присоединяем виртуальные таблицы Остатки и ЦенаЗакупкиСрезПоследних к уже готовому полному списку товаров. Обратите внимание, что параметры виртуальных таблиц лучше заполнять (отбор по массиву товаров), чтобы 1С не рассчитывала остатки по всей базе данных, а только по нужным позициям.

Рекомендации по заполнению формы

Серверная функция не должна возвращать ТаблицаЗначений напрямую, если вы хотите оптимальной работы. Лучше передавать данные через ДанныеФормыКоллекция или массив структур.

Пример обработки результата запроса:


Результат = Запрос.Выполнить();
Если Не Результат.Пустой() Тогда
    ТаблицаДанных = Результат.Выгрузить();
    // Если на форме есть таблица НайденныеПрепараты (ДанныеФормыКоллекция)
    ЗначениеВДанныеФормы(ТаблицаДанных, НайденныеПрепараты);
КонецЕсли;

Важно: Имена колонок в запросе (Наименование, Остаток, Стоимость) должны точно совпадать с именами колонок таблицы на форме. Тогда метод ЗначениеВДанныеФормы автоматически заполнит таблицу.

Резюме

Никогда не используйте запросы внутри цикла Пока Выборка.Следующий(). Это убивает производительность. Всегда старайтесь собрать все необходимые данные с помощью соединений (JOIN) или объединений (UNION) в одном большом запросе. И будьте осторожны с кодом от нейросетей — всегда проверяйте названия методов в Синтаксис-помощнике 1С.

← На главную