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