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