В процессе разработки внешних обработок для модификации данных в 1С программисты часто сталкиваются с ситуацией, когда алгоритм расчета отрабатывает корректно, данные визуализируются, но при попытке их записи в справочник ничего не происходит. В этой статье мы подробно разберем, почему возникает такая проблема, проанализируем ошибки в логике работы с выборками и ветвлением условий, а также рассмотрим профессиональные подходы к оптимизации кода для работы на реальных базах данных.
Рассмотрим сценарий: разработчик создал внешнюю обработку для перерасчета расценок в справочнике Работы. Логика обработки разделена на два этапа: просмотр расхождений и непосредственная запись измененных данных. На этапе записи данные упорно отказывались сохраняться.
В ходе коллективного разбора ситуации на форуме были выявлены две критические ошибки в исходном коде алгоритма записи:
Выборка.ПолучитьОбъект(). Однако у объекта типа ВыборкаИзРезультатаЗапроса нет такого метода. Метод ПолучитьОбъект() доступен только для ссылочных типов данных (СправочникСсылка, ДокументСсылка) или для выборки самого справочника, полученной через метод Выбрать().Записать() был ошибочно помещен внутрь одного из подчиненных блоков ИначеЕсли. В результате транзакция записи срабатывала только для элементов, соответствующих условию "шероховка", тогда как элементы по условию "вулканизац" рассчитывались, но никогда не отправлялись в базу данных.Выясним причину первой ошибки подробнее. Когда мы используем стандартный метод справочника Справочники.Работы.Выбрать(), система возвращает нам объект типа СправочникВыборка. У этого объекта действительно реализован метод ПолучитьОбъект(). Но когда мы переходим к использованию оптимальных механизмов — запросов, выборка из результата запроса возвращает нам строки, содержащие простые значения или ссылки, но не сами элементы справочника. Для модификации элемента нам сначала необходимо получить его ссылку, а уже затем вызвать метод ПолучитьОбъект().
Правильный синтаксис для получения объекта модификации выглядит следующим образом:
ВыборкаОбъект = Выборка.Ссылка.ПолучитьОбъект();
ВыборкаОбъект.Расценка = Формула;
ВыборкаОбъект.Записать();
Вторая проблема — логическое ветвление — была решена путем дублирования или вынесения блока записи на уровень основного условия, где происходит определение необходимости изменения данных. Разберем итоговый рабочий код, к которому пришли участники обсуждения.
Ниже представлен исправленный вариант обработки, в котором учтены особенности извлечения ссылок из запроса и исправлена структура ветвления условий:
РасценкиЗапись = Новый Запрос ("ВЫБРАТЬ
| Работы.Ссылка как Ссылка,
| Работы.Код,
| Работы.Наименование,
| Работы.Расценка,
| Работы.НормаЧасов,
| Работы.Родитель
|ИЗ
| Справочник.Работы КАК Работы
|ГДЕ
| Работы.ЭтоГруппа = Ложь И
| Работы.ПометкаУдаления = Ложь");
Выборка = РасценкиЗапись.Выполнить().Выбрать();
Пока Выборка.Следующий() Цикл
// Исключаем ненужные элементы по ключевым словам в наименовании
Если Найти(НРег(Выборка.Наименование), "набор арматуры") ИЛИ Найти(НРег(Выборка.Наименование), "обрыв перемычки") Тогда
Продолжить;
КонецЕсли;
// Проверяем вхождение в группу "праздничные" через анализ родителей
Если Найти(НРег(Выборка.Родитель.Родитель.Наименование), "праздничные") ИЛИ Найти(НРег(Выборка.Родитель.Родитель.Родитель.Наименование), "праздничные") Тогда
Если Найти(Выборка.Наименование, "вулканизац") Тогда
Отбор = Новый Структура("Разряд", Справочники.ТарифныеСетки.НайтиПоНаименованию("5-разряд (Сде-щики СОВ)"));
Расчет = РегистрыСведений.РазрядыТарифныхСеток.СрезПоследних(ТекущаяДата, Отбор);
Формула = Окр(2 * Выборка.НормаЧасов * Расчет[0].Ставка, 2);
// Получаем объект справочника и записываем расценку
ВыборкаОбъект = Выборка.Ссылка.ПолучитьОбъект();
ВыборкаОбъект.Расценка = Формула;
ВыборкаОбъект.Записать();
ИначеЕсли Найти(Выборка.Наименование, "шероховка") Тогда
Отбор = Новый Структура("Разряд", Справочники.ТарифныеСетки.НайтиПоНаименованию("2-разряд (Сде-щики СН"));
Расчет = РегистрыСведений.РазрядыТарифныхСеток.SrezPoslednih(ТекущаяДата, Отбор);
Формула = Окр(2 * Выборка.НормаЧасов * Расчет[0].Ставка, 2);
// Получаем объект справочника и записываем расценку
ВыборкаОбъект = Выборка.Ссылка.ПолучитьОбъект();
ВыборкаОбъект.Расценка = Формула;
ВыборкаОбъект.Записать();
КонецЕсли;
КонецЕсли;
КонецЦикла;
Этот код успешно решает задачу автора темы: данные теперь гарантированно записываются в справочник. Однако с точки зрения профессиональной разработки в системе 1С:Предприятие, данный алгоритм содержит ряд критических недостатков, которые на реальной базе данных с большим объемом информации приведут к зависаниям программы, блокировкам таблиц СУБД и даже к аварийному завершению сеансов пользователей.
Давайте проведем глубокий технический аудит этого решения и перепишем его по стандартам enterprise-разработки.
Проанализируем архитектурные проблемы представленного кода, которые необходимо устранить для обеспечения быстродействия и стабильности системы.
Посмотрим на строки внутри цикла Пока Выборка.Следующий():
Справочники.ТарифныеСетки.НайтиПоНаименованию(...)РегистрыСведений.РазрядыТарифныхСеток.СрезПоследних(...)Каждая из этих функций под капотом обращается к базе данных. Если в справочнике Работы содержится 5 000 записей, система выполнит 10 000 дополнительных запросов к СУБД! Это классический антипаттерн "N+1 запрос", который убивает производительность любой информационной системы.
Решение: Нам необходимо получить все тарифные ставки заранее, одним запросом, соединив таблицу работ со срезом последних регистра сведений. В цикле должны остаться только чистые математические вычисления без единого обращения к базе данных.
В текущей реализации система выполняет ВыборкаОбъект.Записать() для каждого элемента, прошедшего условия, независимо от того, изменилась ли расценка на самом деле. Если новая расценка совпала со старой, транзакция записи все равно будет выполнена. Это приводит к лишней блокировке таблиц, обновлению индексов СУБД и ненужному вызову подписок на события и обработчиков ПередЗаписью и ПриЗаписи в модуле справочника.
Решение: Перед выполнением записи добавим простую, но крайне эффективную проверку на равенство старого и нового значений:
Если ВыборкаОбъект.Расценка <> Формула Тогда
ВыборкаОбъект.Расценка = Формула;
ВыборкаОбъект.Записать();
КонецЕсли;
Метод Записать() для каждого элемента справочника инициирует открытие и закрытие отдельной неявной транзакции базы данных. Запись 100 элементов по отдельности может выполняться в десятки раз медленнее, чем запись тех же 100 элементов в рамках одной общей транзакции.
Решение: Обернем процесс изменения данных в явную транзакцию с помощью операторов НачатьТранзакцию() и ЗафиксироватьТранзакцию(). В случае возникновения непредвиденной ошибки мы сможем безопасно откатить все изменения через ОтменитьТранзакцию().
Конструкция Выборка.Родитель.Родитель.Наименование является крайне неоптимальной. Во-первых, обращение через точку к реквизитам ссылочного типа в цикле порождает скрытые неявные запросы к базе данных (разыменование ссылок). Во-вторых, код жестко привязан к конкретному уровню вложенности (ровно три уровня папок). Если структуру справочника изменят, код перестанет работать.
Решение: Перенесем проверку на принадлежность группе "праздничные" непосредственно в текст исходного запроса с помощью оператора В ИЕРАРХИИ. Это позволит СУБД самостоятельно и мгновенно определить принадлежность элемента к нужной группе независимо от уровня вложенности.
Использование строк вида "вулканизац", "шероховка" и конкретных наименований тарифных сеток делает код хрупким. При любом переименовании элементов пользователем в режиме "1С:Предприятие" обработка молча перестанет работать.
Решение: Параметризуем запрос. Ссылки на группы справочников и элементы тарифных сеток будем передавать в качестве параметров запроса, а не искать их по наименованию внутри цикла.
Применим все вышеописанные правила оптимизации и напишем профессиональный, быстрый и отказоустойчивый вариант нашей обработки. Мы объединим получение данных о работах и актуальных тарифах в одном запросе, избавимся от поиска в цикле и внедрим механизм транзакционной записи.
// Шаг 1. Подготовим параметры для запроса.
// В реальной практике эти параметры должны выбираться пользователем на форме обработки.
ПапкаПраздничные = Справочники.Работы.НайтиПоНаименованию("праздничные");
РазрядВулканизация = Справочники.ТарифныеСетки.НайтиПоНаименованию("5-разряд (Сде-щики СОВ)");
РазрядШероховка = Справочники.ТарифныеСетки.НайтиПоНаименованию("2-разряд (Сде-щики СН");
Если ПапкаПраздничные.Пустая() Или РазрядВулканизация.Пустая() Или РазрядШероховка.Пустая() Тогда
ВызватьИсключение "Не найдены необходимые настройки или элементы справочников! Проверьте заполнение данных.";
КонецЕсли;
// Шаг 2. Формируем единый запрос, решающий все проблемы неоптимального чтения
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| Работы.Ссылка КАК Ссылка,
| Работы.Наименование КАК Наименование,
| Работы.Расценка КАК ТекущаяРасценка,
| Работы.НормаЧасов КАК НормаЧасов,
| ВЫБОР
| КОГДА Работы.Наименование ПОДОБНО ""%вулканизац%"" ТОГДА &РазрядВулканизация
| КОГДА Работы.Наименование ПОДОБНО ""%шероховка%"" ТОГДА &РазрядШероховка
| ИНАЧЕ НЕОПРЕДЕЛЕНО
| КОНЕЦ КАК РазрядДляРасчета
|ИЗ
| Справочник.Работы КАК Работы
|ГДЕ
| Работы.ЭтоГруппа = ЛОЖЬ
| И Работы.ПометкаУдаления = ЛОЖЬ
| И Работы.Ссылка В ИЕРАРХИИ(&ПапкаПраздничные)
| И НЕ (Работы.Наименование ПОДОБНО ""%набор арматуры%""
| ИЛИ Работы.Наименование ПОДОБНО ""%обрыв перемычки%""
| ИЛИ Работы.Наименование ПОДОБНО ""%мат 1000х1000 (вулкнизация)%"")";
Запрос.УстановитьПараметр("ПапкаПраздничные", ПапкаПраздничные);
Запрос.УстановитьПараметр("РазрядВулканизация", РазрядВулканизация);
Запрос.УстановитьПараметр("РазрядШероховка", РазрядШероховка);
ТаблицаРабот = Запрос.Выполнить().Выгрузить();
// Добавим в таблицу колонку для хранения ставки, чтобы рассчитать ее за один раз
ТаблицаРабот.Колонки.Добавить("Ставка", Новый ОписаниеТипов("Число", Новый КвалификаторыЧисла(15, 2)));
// Получим уникальные разряды, которые нам действительно нужны для расчетов
ТаблицаРазрядов = ТаблицаРабот.Скопировать();
ТаблицаРазрядов.Свернуть("РазрядДляРасчета");
МассивРазрядов = Новый Массив;
Для Каждого СтрокаРазряда Из ТаблицаРазрядов Цикл
Если ЗначениеЗаполнено(СтрокаРазряда.РазрядДляРасчета) Тогда
МассивРазрядов.Добавить(СтрокаРазряда.РазрядДляРасчета);
КонецЕсли;
КонецЦикла;
// Получаем срез последних ставок для всех необходимых разрядов ОДНИМ запросом
Если МассивРазрядов.Количество() > 0 Тогда
ЗапросСтавок = Новый Запрос;
ЗапросСтавок.Текст =
"ВЫБРАТЬ
| РазрядыТарифныхСетокСрезПоследних.Разряд КАК Разряд,
| РазрядыТарифныхСетокСрезПоследних.Ставка КАК Ставка
|ИЗ
| РегистрСведений.РазрядыТарифныхСеток.СрезПоследних(&ТекущаяДата, Разряд В (&МассивРазрядов)) КАК РазрядыТарифныхСетокСрезПоследних";
ЗапросСтавок.УстановитьПараметр("ТекущаяДата", ТекущаяДата());
ЗапросСтавок.УстановитьПараметр("МассивРазрядов", МассивРазрядов);
ТаблицаСтавок = ЗапросСтавок.Выполнить().Выгрузить();
ТаблицаСтавок.Индексы.Добавить("Разряд");
// Заполняем ставки в нашей основной таблице работ
Для Каждого СтрокаРаботы Из ТаблицаРабот Цикл
Если Не ЗначениеЗаполнено(СтрокаРаботы.РазрядДляРасчета) Тогда
Продолжить;
КонецЕсли;
СтрокаСтавки = ТаблицаСтавок.Найти(СтрокаРаботы.РазрядДляРасчета, "Разряд");
Если СтрокаСтавки <> Неопределено Тогда
СтрокаРаботы.Ставка = СтрокаСтавки.Ставка;
КонецЕсли;
КонецЦикла;
КонецЕсли;
// Шаг 3. Запись результатов в транзакции с проверкой изменений
НачатьТранзакцию();
Попытка
Для Каждого СтрокаТаблицы Из ТаблицаРабот Цикл
// Если ставка не найдена или разряд не определен, пропускаем элемент
Если СтрокаТаблицы.Ставка = 0 Или Не ЗначениеЗаполнено(СтрокаТаблицы.РазрядДляРасчета) Тогда
Продолжить;
КонецЕсли;
// Вычисляем новую расценку по формуле
НоваяРасценка = Окр(2 * СтрокаТаблицы.НормаЧасов * СтрокаТаблицы.Ставка, 2);
// Проверяем, изменилось ли значение
Если СтрокаТаблицы.ТекущаяРасценка <> НоваяРасценка Тогда
// Получаем объект только при реальной необходимости записи!
ОбъектРаботы = СтрокаТаблицы.Ссылка.ПолучитьОбъект();
ОбъектРаботы.Расценка = НоваяРасценка;
// Записываем данные в базу данных
ОбъектРаботы.Записать();
КонецЕсли;
КонецЦикла;
// Фиксируем все изменения, если не произошло никаких ошибок
ЗафиксироватьТранзакцию();
Сообщить("Расчет и запись расценок успешно завершены!");
Исключение
// В случае сбоя откатываем базу данных к исходному состоянию
ОтменитьТранзакцию();
Сообщить("Произошла ошибка при записи данных! Изменения не сохранены. Описание ошибки: " + КраткоеПредставлениеОшибки(ИнформацияОбОшибке()), СтатусСообщения.Важное);
КонецПопытки;
Проанализируем, почему переписанный вариант работает эффективнее исходного кода решения:
ПолучитьОбъект() исключительно в том случае, если расчетное значение расценки действительно изменилось.Применяйте рассмотренные нами практики оптимального программирования, избегайте запросов в цикле и всегда проверяйте необходимость записи объектов перед отправкой их в базу данных — это сделает ваши разработки быстрыми, надежными и готовыми к работе в высоконагруженных системах 1С!