Почему запрос к регистру сведений возвращает пустое значение и как правильно получить цену номенклатуры в 1С

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

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

Разбор ошибки №1: Позиционирование выборки и метод Следующий()

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


ВыборкаДетальныеЗаписи = РезультатЗапроса.Выбрать();

Сообщить(""+ВыборкаДетальныеЗаписи.Цена);
Сообщить(""+ВыборкаДетальныеЗаписи.НоменклатураЦенаПокупки);
Сообщить(""+ВыборкаДетальныеЗаписи.НоменклатураЦенаПродажи);

Если ВыборкаДетальныеЗаписи.Следующий() Тогда
    Возврат(ВыборкаДетальныеЗаписи.Цена);
КонецЕсли;

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

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

  1. Сдвигает указатель на следующую (в данном случае — на первую) строку выборки.
  2. Возвращает булево значение: Истина, если переход на новую строку прошел успешно, и Ложь, если записи в выборке закончились или отсутствовали изначально.

Правило разработки: Обращаться к полям выборки (например, ВыборкаДетальныеЗаписи.Цена) допускается строго после успешного вызова метода Следующий(), который обычно помещают в конструкцию условия Если ... Тогда или цикла Пока ... Цикл.

Исправленный вариант вывода отладочных сообщений должен выглядеть следующим образом:


ВыборкаДетальныеЗаписи = РезультатЗапроса.Выбрать();

// Сначала позиционируем выборку на первую запись
Если ВыборкаДетальныеЗаписи.Следующий() Тогда
    // Теперь данные доступны для чтения
    Сообщить("Цена: " + ВыборкаДетальныеЗаписи.Price);
    Сообщить("Покупка: " + ВыборкаДетальныеЗаписи.НоменклатураЦенаПокупки);
    Сообщить("Продажа: " + ВыборкаДетальныеЗаписи.НоменклатураЦенаПродажи);
    
    Возврат ВыборкаДетальныеЗаписи.Цена;
Иначе
    Сообщить("Записи по заданным критериям не найдены!");
    Возврат 0;
КонецЕсли;

Разбор ошибки №2: Фильтрация в ГДЕ вместо параметров виртуальной таблицы

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


ВЫБРАТЬ
    ЦеныПоставщиковСрезПоследних.Цена КАК Цена
ИЗ
    РегистрСведений.ЦеныПоставщиков.СрезПоследних КАК ЦеныПоставщиковСрезПоследних
ГДЕ
    ЦеныПоставщиковСрезПоследних.Контрагент = &Контрагент
    И ЦеныПоставщиковСрезПоследних.Номенклатура = &Номенклатура

Этот запрос является крайне неоптимальным с точки зрения производительности СУБД. Проблема кроется в использовании секции ГДЕ для фильтрации данных виртуальной таблицы СрезПоследних.

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

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

Давайте посмотрим на то, как должен выглядеть оптимизированный и правильный текст запроса:


ВЫБРАТЬ
    ЦеныПоставщиковСрезПоследних.Цена КАК Цена,
    ЦеныПоставщиковСрезПоследних.Номенклатура.ЦенаПокупки КАК НоменклатураЦенаПокупки,
    ЦеныПоставщиковСрезПоследних.Номенклатура.ЦенаПродажи КАК НоменклатураЦенаПродажи
ИЗ
    РегистрСведений.ЦеныПоставщиков.СрезПоследних(
        &Период, 
        Номенклатура = &Номенклатура И Контрагент = &Контрагент
    ) КАК ЦеныПоставщиковСрезПоследних

Разбор ошибки №3: Забытые параметры запроса во втором варианте

Во втором варианте кода автор попытался перенести отбор в параметры виртуальной таблицы, но столкнулся с тем, что запрос перестал работать вообще:


РегистрСведений.ЦеныПоставщиков.СрезПоследних(&Дата, Номенклатура = &Номенклатура)

Здесь допущены сразу две ошибки:

  1. Пропущен обязательный отбор по контрагенту. Поскольку в измерении регистра ЦеныПоставщиков обязательно присутствует контрагент, поиск цены без указания поставщика вернет некорректный срез последних цен (например, цену совершенно другого контрагента, которая оказалась новее по дате).
  2. Параметр даты не был передан в запрос. В коде функции отсутствовала строчка установки параметра &Дата (или &Период). Если в тексте запроса указан параметр &Дата, но в коде программы не написано Запрос.УстановитьПараметр("Дата", ПолучаемаяДата);, платформа 1С прервет выполнение кода с системной ошибкой об отсутствии установленного значения параметра.

Разбор ошибки №4: Лишний контекст и оптимизация клиент-серверного взаимодействия

Обратим внимание на объявление функции получения цены на сервере:


&НаСервере
Функция ТоварыНоменклатураПриИзмененииНаСервере(Номенклатура)

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

Для оптимизации процесса следует использовать директиву &НаСервереБезКонтекста. При ее использовании на сервер передаются только те параметры, которые мы явно передаем в саму функцию. Все необходимые для запроса данные (номенклатуру, контрагента и дату документа) мы передадим в качестве параметров.

Итоговый эталонный вариант реализации через Запрос

С учетом всех разобранных выше ошибок и правил оптимизации, давайте соберем надежный и быстродействующий код.

Код процедуры на клиенте:


&НаКлиенте
Процедура ТоварыНоменклатураПриИзменении(Элемент)
    
    СтрокаТаблицы = Элементы.Товары.ТекущиеДанные;
    Если СтрокаТаблицы = Неопределено Тогда
        Возврат;
    КонецЕсли;
    
    // Передаем на сервер без контекста только необходимые для расчета данные
    СтрокаТаблицы.Цена = ПолучитьЦенуНоменклатурыНаСервере(
        СтрокаТаблицы.Номенклатура, 
        Объект.Контрагент, 
        Объект.Дата
    );
    
КонецПроцедуры

Оптимизированная функция на сервере без контекста:


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

Альтернативный способ: Использование объектной модели (без написания запроса)

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

Этот метод работает чрезвычайно быстро, автоматически формирует оптимальный SQL-запрос к базе данных и значительно снижает риск совершения синтаксических ошибок в тексте запроса. Давайте посмотрим на альтернативный вариант серверной функции:


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

Как не запутаться в отладчике в 1С

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

При пошаговой отладке программы (клавиша F10) точка останова срабатывает и задерживает выполнение кода на строке до момента её фактического выполнения. То есть, когда маркер отладчика находится на строке:

РезультатЗапроса = Запрос.Выполнить();

сама операция выполнения запроса еще не произошла. Поэтому переменная РезультатЗапроса на данном шаге действительно будет равна значению Неопределено.

Для корректного анализа результатов в отладчике сделайте следующее:

  1. Сдвиньте маркер отладки на один шаг вперед (нажмите клавишу F10), чтобы строка с вызовом метода Выполнить() полностью отработала.
  2. Выделите переменную РезультатЗапроса, нажмите сочетание клавиш Shift + F9 (окно Табло/Вычислить выражение).
  3. Для проверки количества полученных строк введите выражение РезультатЗапроса.Выгрузить().Количество(). Если возвращается значение больше нуля, значит, ваш запрос успешно извлекает нужные цены из базы данных, и проблема кроется исключительно в логике последующего обхода выборки.

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

← На главную