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