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