Как автоматически установить время первого документа в начало дня, а последующих — на секунду позже?

Программист 1С v8.3 (Управляемые формы) IT и автоматизация бизнеса
← На главную

В практике автоматизации на платформе 1С:Предприятие нередко возникает задача жесткого упорядочивания документов на временной оси в пределах одного дня. Например, это требуется для того, чтобы определенные обеспечивающие документы всегда гарантированно проводились раньше документов расхода. Типичный сценарий: первый документ за дату должен записываться строго на начало суток (00:00:00), а каждому следующему документу этого же вида должно присваиваться время на одну секунду больше предыдущего.

В рамках этой статьи мы разберем программное решение данной задачи в обработчиках событий, рассмотрим оптимизацию запросов к базе данных для повышения быстродействия, а также предотвратим возможные проблемы при параллельной работе пользователей.

Базовая реализация через обработчик ПередЗаписью

Для реализации логики автоматического сдвига времени отлично подходит обработчик события ПередЗаписью в модуле объекта документа. Назначение времени должно происходить на стороне сервера непосредственно перед сохранением транзакции в базу данных.

Рассмотрим базовый вариант программного кода, который определяет, является ли создаваемый документ новым, находит в базе последний документ за текущие сутки и увеличивает его время на одну секунду:


Процедура ПередЗаписью(Отказ, РежимЗаписи, РежимПроведения)
    
    Если ЭтоНовый() Тогда
        
        ДатаНачалоДня = НачалоДня(Дата);
        
        Запрос = Новый Запрос;
        Запрос.УстановитьПараметр("ДатаНачалоДня", ДатаНачалоДня);
        
        Запрос.Текст = "ВЫБРАТЬ ПЕРВЫЕ 1
        |    ЗП_ОтпускаОрганизаций.Ссылка КАК Ссылка,
        |    ЗП_ОтпускаОрганизаций.Дата КАК Дата
        |ИЗ
        |    Документ.ЗП_ОтпускаОрганизаций КАК ЗП_ОтпускаОрганизаций
        |ГДЕ
        |    НАЧАЛОПЕРИОДА(ЗП_ОтпускаОрганизаций.Дата, ДЕНЬ) = &ДатаНачалоДня
        |
        |УПОРЯДОЧИТЬ ПО
        |    ЗП_ОтпускаОрганизаций.МоментВремени УБЫВ";
        
        РезультатЗапроса = Запрос.Выполнить();
        Выборка = РезультатЗапроса.Выбрать();
        
        Если Выборка.Следующий() Тогда
            Дата = Выборка.Дата + 1;
        Иначе 
            Дата = ДатаНачалоДня;
        КонецЕсли;
        
    КонецЕсли;
    
КонецПроцедуры

Данный алгоритм успешно справляется со своей задачей в простых файловых базах при монопольной работе. Однако при переносе этого решения в реальную многопользовательскую среду или на базы данных большого объема мы неизбежно столкнемся с критическими проблемами производительности и параллельного доступа. Проанализируем эти проблемы и оптимизируем наш код.

Оптимизация поискового запроса к СУБД

Обратим внимание на условие фильтрации в исходном запросе:

НАЧАЛОПЕРИОДА(ЗП_ОтпускаОрганизаций.Дата, ДЕНЬ) = &ДатаНачалоДня

Важно помнить: применение любых платформенных функций (таких как НАЧАЛОПЕРИОДА) непосредственно к индексируемому полю таблицы базы данных (в данном случае к реквизиту Дата) делает это условие не-SARGable. Это означает, что СУБД не сможет использовать индекс по полю Дата для быстрого позиционирования. Вместо быстрого поиска СУБД будет вынуждена выполнить полное сканирование таблицы (Table Scan), что приведет к катастрофическому падению производительности при росте базы данных.

Для исправления ситуации перепишем запрос с использованием явного диапазона дат, передавая в параметры границы дня. СУБД в таком случае мгновенно отработает по индексу (Index Seek):


ДатаНачалаДня = НачалоДня(Дата);
ДатаКонцаДня  = КонецДня(Дата);

Запрос = Новый Запрос;
Запрос.УстановитьПараметр("ДатаНачалаДня", ДатаНачалаДня);
Запрос.УстановитьПараметр("ДатаКонцаДня",  ДатаКонцаДня);

Запрос.Текст = "ВЫБРАТЬ ПЕРВЫЕ 1
|    ЗП_ОтпускаОрганизаций.Дата КАК Дата
|ИЗ
|    Документ.ЗП_ОтпускаОрганизаций КАК ЗП_ОтпускаОрганизаций
|ГДЕ
|    ЗП_ОтпускаОрганизаций.Дата МЕЖДУ &ДатаНачалаДня И &ДатаКонцаДня
|
|УПОРЯДОЧИТЬ ПО
|    ЗП_ОтпускаОрганизаций.МоментВремени УБЫВ";

Предотвращение коллизий при параллельной работе (Состояние гонки)

В многопользовательских базах данных предложенный алгоритм подвержен проблеме «состояния гонки» (race condition). Если два или более пользователя одновременно создают и записывают документы на одну и ту же дату, их транзакции выполняются параллельно. На момент выполнения запроса ни один из документов еще физически не сохранен в СУБД. В результате запросы вернут одинаковое «последнее» время из уже записанных ранее документов, и система присвоит обоим новым документам абсолютно одинаковое время, что нарушит уникальную последовательность.

Для решения этой проблемы необходимо применить механизм управляемых блокировок перед выполнением запроса. Мы должны заблокировать диапазон дат текущего дня для данного типа документа с помощью объекта БлокировкаДанных. Это выстроит параллельные транзакции записи в очередь:


Блокировка = Новый БлокировкаДанных;
ЭлементБлокировки = Блокировка.Добавить("Документ.ЗП_ОтпускаОрганизаций");
ЭлементБлокировки.Режим = РежимБлокировкиДанных.Исключительный;
ЭлементБлокировки.УстановитьПараметр("Дата", Новый Диапазон(ДатаНачалаДня, ДатаКонцаДня));
Блокировка.Заблокировать();

Блокировка накладывается только на диапазон дат одного дня текущего документа, поэтому общая пропускная способность системы пострадает незначительно, а целостность данных будет полностью сохранена.

Корректная обработка изменения даты существующих документов

Конструкция Если ЭтоНовый() Тогда покрывает исключительно сценарий создания документа с нуля. Однако на практике пользователи могут открыть уже созданный ранее документ и изменить его дату (перенести на другой день). В таком случае проверка ЭтоНовый() вернет значение Ложь, алгоритм не сработает, и документ не получит последовательного времени на новый день.

Посмотрим, как усовершенствовать условие, чтобы отслеживать изменение суток документа:


НужноПересчитатьВремя = Ложь;

Если ЭтоНовый() Тогда
    НужноПересчитатьВремя = Истина;
Иначе
    // Проверим, изменился ли день у существующего документа
    СтараяДата = Ссылка.Дата;
    Если НачалоДня(СтараяДата) <> НачалоДня(Дата) Тогда
        НужноПересчитатьВремя = Истина;
    КонецЕсли;
КонецЕсли;

Если НужноПересчитатьВремя Тогда
    // Здесь выполняем блокировку данных, запрос и расчет времени...
КонецЕсли;

Альтернативный платформенный способ позиционирования

Не стоит забывать, что платформа 1С:Предприятие имеет встроенный оптимизированный механизм для автоматического управления временем документов. Вместо ручного вычисления секунд через запросы к базе данных мы можем задействовать метод объекта УстановитьВремя().

Если нам требуется гарантированно записать документ в конец текущего дня, мы можем вызвать следующую команду в обработчике ПередЗаписью:

ЭтотОбъект.УстановитьВремя(РежимАвтоВремя.Последним, Ложь);

При использовании этого метода система сама определит крайнее положение документа на оси времени текущего дня, используя оптимизированные внутренние механизмы платформы.

Ограничение при оперативном проведении

Разберем важный нюанс: если в метаданных документа разрешено оперативное проведение и пользователь проводит его оперативно (РежимПроведения.Оперативный), попытка принудительно установить время в начало дня (то есть в «прошлое» относительно текущего времени сеанса) приведет к ошибке платформы. Оперативное проведение требует строго актуального времени. Если бизнес-логика вашей конфигурации требует записи документов строго в начало дня, вам понадобится либо переключить режим проведения документа на неоперативный в его свойствах, либо программно изменять режим проведения в обработчиках.

← На главную