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