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