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