В практике разработки на платформе 1С часто встречается ситуация, когда при проведении документа (например, Заказ покупателя) требуется проверить корректность данных в табличной части. Одной из самых распространенных ошибок начинающих разработчиков является использование запроса внутри цикла Для Каждого. Давайте разберем, почему это плохо, и как реализовать проверку правильно, используя возможности языка запросов и механизмы платформы.
Представим стандартную задачу: при проведении документа нужно проверить, чтобы цена товара в табличной части была не ниже минимально допустимой цены из регистра сведений. Рассмотрим типичный "неправильный" код:
Для Каждого Стр ИЗ Товары Цикл
Результат = ОбщийМодуль.Проверка(Дата, Стр.Номенклатура, Стр.Цена);
Если Результат Тогда
Отказ = Истина;
КонецЕсли;
КонецЦикла;
В этом примере на каждую строку документа система делает отдельное обращение к базе данных. Если в документе 100 строк, сервер выполнит 100 запросов. Это создает колоссальную нагрузку на SQL-сервер и значительно замедляет проведение документа. Проанализируем ситуацию: наша цель — получить все данные одним "выстрелом" и обработать их разом.
Правильный подход заключается в том, чтобы соединить данные табличной части документа с данными регистра цен прямо в запросе. Рассмотрим, как это реализовать. Мы передадим в запрос ссылку на текущий документ и получим все расхождения по ценам за один раз.
Процедура ПроверкаЦен(Отказ)
Если Отказ Тогда Возврат; КонецЕсли;
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| ДокТовары.НомерСтроки КАК НомерСтроки,
| ДокТовары.Номенклатура КАК Номенклатура,
| ДокТовары.Цена КАК ЦенаДокумента,
| ЦеныРегистра.Цена КАК ЦенаРегистра
|ИЗ
| Документ.ЗаказПокупателя.Товары КАК ДокТовары
| ЛЕВОЕ СОЕДИНЕНИЕ РегистрСведений.ЦеныНоменклатуры.СрезПоследних(&Период, ТипЦен = &ТипЦен) КАК ЦеныРегистра
| ПО ДокТовары.Номенклатура = ЦеныРегистра.Номенклатура
|ГДЕ
| ДокТовары.Ссылка = &Ссылка
| И ДокТовары.Цена < ЕСТЬNULL(ЦеныРегистра.Цена, 0)";
Запрос.УстановитьПараметр("Ссылка", Ссылка);
Запрос.УстановитьПараметр("Период", Дата);
Запрос.УстановитьПараметр("ТипЦен", ТипЦен);
РезультатЗапроса = Запрос.Выполнить();
Если Не РезультатЗапроса.Пустой() Тогда
Отказ = Истина;
Выборка = РезультатЗапроса.Выбрать();
Пока Выборка.Следующий() Цикл
Сообщение = Новый СообщениеПользователю;
Сообщение.Текст = "В строке №" + Выборка.НомерСтроки + " цена ниже допустимой! " +
"Товар: " + Выборка.Номенклатура +
", Цена в док: " + Выборка.ЦенаДокумента +
", Мин. цена: " + Выборка.ЦенаРегистра;
Сообщение.Сообщить();
КонецЦикла;
КонецЕсли;
КонецПроцедуры;
Разберем, что мы сделали. Во-первых, мы использовали ЛЕВОЕ СОЕДИНЕНИЕ, чтобы сопоставить каждую строку документа с ценой из регистра. Во-вторых, в секции ГДЕ мы сразу отфильтровали только те строки, которые нарушают наше условие (Цена < ЦенаРегистра). Таким образом, выборка будет содержать только "проблемные" строки.
Если документ еще не записан в базу данных или вы хотите передать данные из ТЧ, которые были изменены программно перед проведением, эффективнее использовать временные таблицы (ВТ). Это позволяет SQL-серверу более эффективно строить план запроса.
Рассмотрим пошагово, как работать с временными таблицами в 1С:
Номенклатура.
ТаблицаТоваров = Товары.Выгрузить();
МенеджерВТ = Новый МенеджерВременных Таблиц;
Запрос = Новый Запрос;
Запрос.МенеджерВременныхТаблиц = МенеджерВТ;
// Помещаем данные во временную таблицу
Запрос.Текст =
"ВЫБРАТЬ * ПОМЕСТИТЬ ВТ_Товары ИЗ &ТЗ КАК ТЗ";
Запрос.УстановитьПараметр("ТЗ", ТаблицаТоваров);
Запрос.Выполнить();
// Основной запрос с использованием ВТ
Запрос.Текст =
"ВЫБРАТЬ
| Т.НомерСтроки,
| Т.Номенклатура,
| Т.Цена,
| Цены.Цена КАК ЦенаРегистра
|ИЗ
| ВТ_Товары КАК Т
| ЛЕВОЕ СОЕДИНЕНИЕ РегистрСведений.ЦеныНоменклатуры.СрезПоследних(&Период, ТипЦен = &ТипЦен) КАК Цены
| ПО Т.Номенклатура = Цены.Номенклатура
|ГДЕ
| Т.Цена < ЕСТЬNULL(Цены.Цена, 0)";
Результат = Запрос.Выполнить().Выбрать();
// Далее обработка результата...
При реализации проведения важно помнить о нескольких правилах, которые отделяют профессиональный код от "костылей":
1. Проверка свойства ОбменДанными.Загрузка. Если ваш документ загружается из другой базы (через конвертацию данных или API), проверки при проведении могут только мешать. Всегда добавляйте в начало процедур проверки условие:
Если ОбменДанными.Загрузка Тогда Возврат; КонецЕсли;
2. Параметры виртуальных таблиц. Обратите внимание, что условие по ТипЦен мы передали в параметры виртуальной таблицы СрезПоследних, а не в секцию ГДЕ. Проанализируем ситуацию: если передать условие в ГДЕ, система сначала получит цены для всех типов цен на указанную дату, и только потом отфильтрует их. Это крайне неэффективно для больших регистров.
3. Блокировки данных. Поскольку проверка выполняется внутри транзакции проведения, чтение данных из регистра происходит с установкой разделяемой блокировки. Если запросы будут выполняться слишком долго, это может привести к конфликтам блокировок при одновременной работе многих пользователей. Использование одного компактного запроса сводит время удержания блокировки к минимуму.
4. Принцип сбора всех ошибок. Не стоит прерывать проведение на первой же ошибке. Пользователю гораздо удобнее получить сразу весь список строк с некорректными ценами, чем исправлять их по одной и каждый раз нажимать "Провести". Используйте объект СообщениеПользователю с привязкой к полю Цена и конкретному номеру строки — тогда пользователь сможет кликнуть на ошибку и сразу перейти к нужной ячейке.
5. Разделение логики. Выносите тяжелые проверки в Общий модуль с признаком "Сервер". Это позволит вам переиспользовать этот же код, например, в форме документа при нажатии кнопки "Проверить цены" или перед записью документа.
Мы выяснили, что основной враг производительности в 1С — это обращение к базе данных внутри циклов. Для оптимизации проведения документа Заказ покупателя мы рассмотрели:
ЛЕВОЕ СОЕДИНЕНИЕ для сопоставления данных.Следуя этим правилам, вы обеспечите высокую скорость работы системы даже при очень больших объемах данных в документах и справочниках.