Разработка механизмов автоматического заполнения цен при выборе номенклатуры в табличной части — это одна из самых частых задач при программировании в «1С:Предприятие 8». Несмотря на кажущуюся простоту, начинающие разработчики регулярно сталкиваются с ситуацией, когда написанный запрос не возвращает данные, хотя нужные записи гарантированно присутствуют в базе данных. В этой статье мы детально проанализируем, почему возникает подобная проблема, разберем типичные ошибки позиционирования выборки, оптимизируем работу с виртуальными таблицами и рассмотрим альтернативные, более изящные способы получения цен.
Давайте посмотрим на фрагмент кода, который изначально вызывал затруднения у автора темы на форуме:
ВыборкаДетальныеЗаписи = РезультатЗапроса.Выбрать();
Сообщить(""+ВыборкаДетальныеЗаписи.Цена);
Сообщить(""+ВыборкаДетальныеЗаписи.НоменклатураЦенаПокупки);
Сообщить(""+ВыборкаДетальныеЗаписи.НоменклатураЦенаПродажи);
Если ВыборкаДетальныеЗаписи.Следующий() Тогда
Возврат(ВыборкаДетальныеЗаписи.Цена);
КонецЕсли;
Выясним причину, почему вывод сообщений через функцию Сообщить() до оператора условия возвращал пустые строки. Дело в том, что метод Выбрать() возвращает объект типа ВыборкаИзРезультатаЗапроса, указатель (курсор) которого изначально позиционирован перед первой записью полученного набора данных. На этой позиции выборка еще ничего не «знает» о значениях полей конкретной строки.
Для того чтобы считать данные из первой строки, нам необходимо передвинуть указатель вперед. За это отвечает метод Следующий(). Он выполняет сразу две функции:
Истина, если переход на новую строку прошел успешно, и Ложь, если записи в выборке закончились или отсутствовали изначально.Правило разработки: Обращаться к полям выборки (например, ВыборкаДетальныеЗаписи.Цена) допускается строго после успешного вызова метода Следующий(), который обычно помещают в конструкцию условия Если ... Тогда или цикла Пока ... Цикл.
Исправленный вариант вывода отладочных сообщений должен выглядеть следующим образом:
ВыборкаДетальныеЗаписи = РезультатЗапроса.Выбрать();
// Сначала позиционируем выборку на первую запись
Если ВыборкаДетальныеЗаписи.Следующий() Тогда
// Теперь данные доступны для чтения
Сообщить("Цена: " + ВыборкаДетальныеЗаписи.Price);
Сообщить("Покупка: " + ВыборкаДетальныеЗаписи.НоменклатураЦенаПокупки);
Сообщить("Продажа: " + ВыборкаДетальныеЗаписи.НоменклатураЦенаПродажи);
Возврат ВыборкаДетальныеЗаписи.Цена;
Иначе
Сообщить("Записи по заданным критериям не найдены!");
Возврат 0;
КонецЕсли;
Проанализируем первый вариант текста запроса, который был предложен автором темы:
ВЫБРАТЬ
ЦеныПоставщиковСрезПоследних.Цена КАК Цена
ИЗ
РегистрСведений.ЦеныПоставщиков.СрезПоследних КАК ЦеныПоставщиковСрезПоследних
ГДЕ
ЦеныПоставщиковСрезПоследних.Контрагент = &Контрагент
И ЦеныПоставщиковСрезПоследних.Номенклатура = &Номенклатура
Этот запрос является крайне неоптимальным с точки зрения производительности СУБД. Проблема кроется в использовании секции ГДЕ для фильтрации данных виртуальной таблицы СрезПоследних.
Когда платформа 1С обрабатывает подобный запрос, она сначала строит временную таблицу среза последних записей для абсолютно всех элементов, существующих в данном регистре сведений, и только после этого накладывает фильтр по конкретному контрагенту и номенклатуре. Если в вашей базе данных хранятся десятки тысяч записей о ценах, это вызовет колоссальную и неоправданную нагрузку на сервер.
Правильное решение: Условия отбора по измерениям регистра необходимо передавать непосредственно в параметры виртуальной таблицы. В этом случае СУБД на уровне базы данных сразу выберет и обработает только одну единственную нужную строку.
Давайте посмотрим на то, как должен выглядеть оптимизированный и правильный текст запроса:
ВЫБРАТЬ
ЦеныПоставщиковСрезПоследних.Цена КАК Цена,
ЦеныПоставщиковСрезПоследних.Номенклатура.ЦенаПокупки КАК НоменклатураЦенаПокупки,
ЦеныПоставщиковСрезПоследних.Номенклатура.ЦенаПродажи КАК НоменклатураЦенаПродажи
ИЗ
РегистрСведений.ЦеныПоставщиков.СрезПоследних(
&Период,
Номенклатура = &Номенклатура И Контрагент = &Контрагент
) КАК ЦеныПоставщиковСрезПоследних
Во втором варианте кода автор попытался перенести отбор в параметры виртуальной таблицы, но столкнулся с тем, что запрос перестал работать вообще:
РегистрСведений.ЦеныПоставщиков.СрезПоследних(&Дата, Номенклатура = &Номенклатура)
Здесь допущены сразу две ошибки:
ЦеныПоставщиков обязательно присутствует контрагент, поиск цены без указания поставщика вернет некорректный срез последних цен (например, цену совершенно другого контрагента, которая оказалась новее по дате).&Дата (или &Период). Если в тексте запроса указан параметр &Дата, но в коде программы не написано Запрос.УстановитьПараметр("Дата", ПолучаемаяДата);, платформа 1С прервет выполнение кода с системной ошибкой об отсутствии установленного значения параметра.Обратим внимание на объявление функции получения цены на сервере:
&НаСервере
Функция ТоварыНоменклатураПриИзмененииНаСервере(Номенклатура)
Использование директивы &НаСервере заставляет платформу передавать с клиента на сервер весь контекст формы (все реквизиты, элементы формы, данные других строк табличной части и т.д.). При выборе номенклатуры в каждой строке это может приводить к заметным задержкам интерфейса у пользователей, особенно при работе через интернет или на слабых компьютерах.
Для оптимизации процесса следует использовать директиву &НаСервереБезКонтекста. При ее использовании на сервер передаются только те параметры, которые мы явно передаем в саму функцию. Все необходимые для запроса данные (номенклатуру, контрагента и дату документа) мы передадим в качестве параметров.
С учетом всех разобранных выше ошибок и правил оптимизации, давайте соберем надежный и быстродействующий код.
Код процедуры на клиенте:
&НаКлиенте
Процедура ТоварыНоменклатураПриИзменении(Элемент)
СтрокаТаблицы = Элементы.Товары.ТекущиеДанные;
Если СтрокаТаблицы = Неопределено Тогда
Возврат;
КонецЕсли;
// Передаем на сервер без контекста только необходимые для расчета данные
СтрокаТаблицы.Цена = ПолучитьЦенуНоменклатурыНаСервере(
СтрокаТаблицы.Номенклатура,
Объект.Контрагент,
Объект.Дата
);
КонецПроцедуры
Оптимизированная функция на сервере без контекста:
&НаСервереБезКонтекста
Функция ПолучитьЦенуНоменклатурыНаСервере(Знач Номенклатура, Знач Контрагент, Знач Период)
// Если ключевые параметры не заполнены, возвращаем 0
Если Не ЗначениеЗаполнено(Номенклатура) Или Не ЗначениеЗаполнено(Контрагент) Тогда
Возврат 0;
КонецЕсли;
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| ЦеныПоставщиковСрезПоследних.Цена КАК Цена
|ИЗ
| РегистрСведений.ЦеныПоставщиков.СрезПоследних(
| &Период,
| Номенклатура = &Номенклатура И Контрагент = &Контрагент
| ) КАК ЦеныПоставщиковСрезПоследних";
Запрос.УстановитьПараметр("Номенклатура", Номенклатура);
Запрос.УстановитьПараметр("Контрагент", Контрагент);
// Приводим дату к концу дня или используем переданную дату документа
Запрос.УстановитьПараметр("Период", ?(ЗначениеЗаполнено(Период), Период, ТекущаяДата()));
РезультатЗапроса = Запрос.Выполнить();
Выборка = РезультатЗапроса.Выбрать();
Если Выборка.Следующий() Тогда
Возврат Выборка.Цена;
КонецЕсли;
Возврат 0;
КонецФункции
Если нам требуется получить только одно конкретное значение ресурса регистра сведений по точным измерениям, то писать громоздкий запрос в коде вовсе не обязательно. Платформа 1С предоставляет удобный встроенный инструмент — метод менеджера регистра сведений ПолучитьПоследнее().
Этот метод работает чрезвычайно быстро, автоматически формирует оптимальный SQL-запрос к базе данных и значительно снижает риск совершения синтаксических ошибок в тексте запроса. Давайте посмотрим на альтернативный вариант серверной функции:
&НаСервереБезКонтекста
Функция ПолучитьЦенуНоменклатурыАльтернативный(Знач Номенклатура, Знач Контрагент, Знач Период)
Если Не ЗначениеЗаполнено(Номенклатура) Или Не ЗначениеЗаполнено(Контрагент) Тогда
Возврат 0;
КонецЕсли;
// Формируем структуру отбора по ключевым измерениям регистра
СтруктураОтбора = Новый Структура;
СтруктураОтбора.Вставить("Номенклатура", Номенклатура);
СтруктураОтбора.Вставить("Контрагент", Контрагент);
// Получаем структуру ресурсов на указанный период времени
РабочаяДата = ?(ЗначениеЗаполнено(Период), Период, ТекущаяДата());
СтруктураРесурсов = РегистрыСведений.ЦеныПоставщиков.ПолучитьПоследнее(РабочаяДата, СтруктураОтбора);
// Проверяем наличие ресурса "Цена" в возвращенной структуре
Если СтруктураРесурсов.Свойство("Цена") Тогда
Возврат СтруктураРесурсов.Цена;
КонецЕсли;
Возврат 0;
КонецФункции
В ходе обсуждения на форуме автор отметил, что отладчик показывал результат выполнения запроса как значение Неопределено. Разберем этот важный нюанс работы с отладчиком в среде 1С.
При пошаговой отладке программы (клавиша F10) точка останова срабатывает и задерживает выполнение кода на строке до момента её фактического выполнения. То есть, когда маркер отладчика находится на строке:
РезультатЗапроса = Запрос.Выполнить();
сама операция выполнения запроса еще не произошла. Поэтому переменная РезультатЗапроса на данном шаге действительно будет равна значению Неопределено.
Для корректного анализа результатов в отладчике сделайте следующее:
Выполнить() полностью отработала.РезультатЗапроса, нажмите сочетание клавиш Shift + F9 (окно Табло/Вычислить выражение).РезультатЗапроса.Выгрузить().Количество(). Если возвращается значение больше нуля, значит, ваш запрос успешно извлекает нужные цены из базы данных, и проблема кроется исключительно в логике последующего обхода выборки.Используя предложенные нами рекомендации и готовые примеры кода, вы сможете избежать зависаний системы, оптимизировать нагрузку на базу данных и гарантировать точное и быстрое заполнение цен в документах конфигураций 1С.