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