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

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

В процессе разработки в среде «1С:Предприятие» программисты часто сталкиваются с ситуацией, когда при попытке получить актуальную цену номенклатуры или блюда из периодического регистра сведений система возвращает 0 (или пустое значение). При этом разработчик точно знает, что соответствующая запись в регистре присутствует, а документ переоценки проведен.

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

Анализ исходной ситуации и поиск ключевой ошибки

Рассмотрим исходный код, который использовал автор темы. В общем модуле была объявлена следующая функция:


Функция ПоказатьЦенуБлюда(Блюдо) Экспорт
    Отбор = Новый Структура("Блюдо");
    Отбор.Вставить("Блюдо", Блюдо);
    ПрДанные = РегистрыСведений.Переоценка.ПолучитьПоследнее(, Отбор);
    Возврат ПрДанные.Цена;
КонецФункции

А на клиенте в модуле формы выполнялся следующий вызов:


&НаКлиенте
Процедура ИмяПриИзменении(Элемент)
    Новая = ЦеныБлюд.ПоказатьЦенуБлюда(Объект.Блюдо);
    Объект.Цена = Новая;
КонецПроцедуры

При таком подходе функция стабильно возвращала 0. Давайте разберемся, почему так произошло.

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

Когда платформа выполняет метод ПолучитьПоследнее(), она пытается сопоставить переданное строковое значение с данными измерения в базе данных. Поскольку строка "Суп-пюре" и ссылка на элемент справочника с тем же наименованием — это абсолютно разные типы данных для СУБД, платформа не находит подходящих записей и возвращает структуру, заполненную дефолтными значениями (для числового ресурса Цена дефолтным значением как раз является 0).

Как правильно инициализировать структуру отбора

Прежде всего обратим внимание на инициализацию структуры отбора. Писать код в три строки, как в исходном примере, не совсем эффективно. Гораздо лаконичнее и правильнее создавать структуру сразу с параметрами:

Отбор = Новый Структура("Блюдо", Блюдо);

Этот вариант не только сокращает объем кода, но и делает его более читаемым. Однако, чтобы этот отбор сработал, в переменную Блюдо должна быть передана именно ссылка.

Важный подводный камень: Работа с Объект.Ссылка для новых элементов

В ходе обсуждения проблемы автор упомянул, что замена параметра на передачу Объект.Ссылка решила его проблему. Действительно, для уже существующих и сохраненных в базе данных элементов это сработает. Но здесь скрывается опасный подводный камень, о котором должен знать каждый разработчик.

Что произойдет, если пользователь создает абсолютно новый элемент справочника?

Если элемент еще ни разу не был записан в базу данных:

  1. Свойство Объект.Ссылка вернет пустую ссылку (Справочники.Блюда.ПустаяСсылка()).
  2. Метод ПолучитьПоследнее() попытается найти цену для этой пустой ссылки.
  3. Поскольку для пустой ссылки цен в регистре существовать не может, система вернет 0.

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


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

Использование запросов: Как избежать критических ошибок производительности

В сообщении 18 был предложен альтернативный вариант решения задачи с использованием запроса к виртуальной таблице СрезПоследних:


// ВНИМАНИЕ: Пример неоптимального запроса из форума!
Запрос.Текст = "ВЫБРАТЬ * ИЗ РегистрСведений.Переоценка.СрезПоследних(&Дата, Блюдо.Наименование = &Блюдо)";

Ни в коем случае не используйте подобную конструкцию в реальных проектах!

Давайте выясним причину, почему такое написание считается грубым нарушением стандартов разработки фирмы «1С» и приводит к серьезному снижению производительности системы:

  1. Неявное разыменование полей (обращение через точку) в параметрах виртуальной таблицы. Когда вы пишете Блюдо.Наименование = &Блюдо в условиях виртуальной таблицы, СУБД вынуждена выполнять операцию LEFT JOIN (левое соединение) физической таблицы регистра сведений с таблицей справочника непосредственно в момент построения среза.
  2. Блокировка использования индексов. Это действие полностью исключает возможность использования стандартных индексов регистра сведений СУБД. Происходит сканирование всей таблицы (Full Scan), что на крупных базах данных приведет к колоссальным задержкам и зависанию интерфейса у пользователей.

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

Рассмотрим пример правильного написания запроса:


Функция ПолучитьЦенуЗапросом(СсылкаНаБлюдо) Экспорт

    Если Не ЗначениеЗаполнено(СсылкаНаБлюдо) Тогда
        Возврат 0;
    КонецЕсли;

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

Что выбрать: ПолучитьПоследнее() или Запрос?

Давайте проанализируем, в каких ситуациях предпочтительнее использовать объектный метод ПолучитьПоследнее(), а когда — полноценный SQL-запрос через объект Запрос.

Метод ПолучитьПоследнее() предпочтителен, если:

Запрос к виртуальной таблице СрезПоследних незаменим, если:

Оптимизация клиент-серверной архитектуры

Для того чтобы ваша форма работала быстро и без задержек, обратите внимание на организацию вызовов между клиентом и сервером. Общий модуль, в котором располагается функция получения цены, должен иметь установленные флаги «Сервер» и «Вызов сервера».

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


&НаКлиенте
Процедура БлюдоПриИзменении(Элемент)
    // Передаем только ссылку на блюдо, не отправляя весь контекст формы на сервер
    Объект.Цена = ПолучитьЦенуНаСервере(Объект.Блюдо);
КонецПроцедуры

&НаСервереБезКонтекста
Функция ПолучитьЦенуНаСервере(Знач СсылкаНаБлюдо)
    Возврат ЦеныБлюд.ПоказатьЦенуБлюда(СсылкаНаБлюдо);
КонецФункции

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

← На главную