Время бесценно
Особенно при высокой стоимости часа специалиста, который будет исправлять ошибки конфигурации в работе с датами и временем.
На эту тему уже многое написано как здесь на ИС, так и за его пределами. Фирма "1С" снабдила нас исчерпывающими инструкциями по работе с датами и временем, включила некоторые положения в стандарты разработки, а также реализовала удобный функционал в БСП. Не смотря на это, многие продолжают "пилить" костыли в обслуживаемых информационных базах, уверенные что у них все хорошо.
Сегодня мы пробежимся по основам работы со временем из кода встроенного языка и запросов, рассмотрим основные возможности платформы и дополнительный функционал в БСП. Также подробней разберем особенности при работе в разных часовых поясах с примерами на распределенном кластере серверов 1С, а также некоторые другие особенности.
Статья может быть интересна:
- Начинающим разработчикам 1С.
- Любым разработчикам 1С, которые везде используют функцию "ТекущаяДата()".
- Кого интересует работа с разными часовыми поясами и функционал БСП работы со временем.
- Тем, кого интересует как будет работать кластер 1С, если сервера в разных часовых поясах (о ужас!).
- Кто хочет заглянуть как платформа 1С хранит дату и время в базе, как работает момент времени, какие проблемы могут быть.
Все нижесказанное актуально для версии платформы 8.3.13.1690. Вы все еще здесь? Тогда добро пожаловать в путешествие во времени (или по времени, или через....).
Основные основы основ
Язык программирования платформы 1С и синтаксис языка запросов имеют все необходимое для работы с датами и временем, чтобы решить большинство возникающих задач. Ниже приведен листинг кода встроенного языка, где мы применили все возможные операции с датами.
Операции с датами из встроенного языка
Ничего нового. Практически каждый разработчик сталкивался с этими функциями.
// Инициализация даты
// 1. Константой
Дата = '20190308143015'; // 08.03.2019 14:30:15
// 2. Строкой
Дата = Дата("20190308143015"); // 08.03.2019 14:30:15
// 3. Частями
Дата = Дата(2019, 3, 8, 14, 30, 15); // 08.03.2019 14:30:15
// Получение частей даты в виде числа
Год = Год(Дата); // 2019
Месяц = Месяц(Дата); // 3
День = День(Дата); // 8
Час = Час(Дата); // 14
Минута = Минута(Дата); // 30
Секунда = Секунда(Дата); // 15
// Основные операции с датами
// 1. Добавить одну секунду
Дата = Дата + 1; // 08.03.2019 14:30:16
// 2. Добавить 2 месяца
Дата = ДобавитьМесяц(Дата, 2); // 08.05.2019 14:30:16
// 3. Отнять 3 месяца
Дата = ДобавитьМесяц(Дата, -3); // 08.02.2019 14:30:16
// 4. Получить порядковый номер дня в году
НомерДняВГоду = ДеньГода(Дата); // 39
// 5. Получить порядковый номер дня в неделе (начиная с понедельника)
НомерДняВНеделе = ДеньНедели(Дата); // 5
// 6. Получить порядковый номер недели в году
НомерНеделиВГоду = НеделяГода(Дата); // 6
// 7. Начало предопределенных периодов
НачалоГода = НачалоГода(Дата); // 01.01.2019 00:00:00
НачалоКвартала = НачалоКвартала(Дата); // 01.01.2019 00:00:00
НачалоМесяца = НачалоМесяца(Дата); // 01.02.2019 00:00:00
НачалоНедели = НачалоНедели(Дата); // 04.02.2019 00:00:00
НачалоДня = НачалоДня(Дата); // 08.02.2019 00:00:00
НачалоЧаса = НачалоЧаса(Дата); // 08.02.2019 14:00:00
НачалоМинуты = НачалоМинуты(Дата); // 08.02.2019 14:30:00
// 8. Конец предопределенных периодов
КонецГода = КонецГода(Дата); // 31.12.2019 23:59:59
КонецКвартала = КонецКвартала(Дата); // 31.03.2019 23:59:59
КонецМесяца = КонецМесяца(Дата); // 28.02.2019 23:59:59
КонецНедели = КонецНедели(Дата); // 10.02.2019 23:59:59
КонецДня = КонецДня(Дата); // 08.02.2019 23:59:59
КонецЧаса = КонецЧаса(Дата); // 08.02.2019 14:59:59
КонецМинуты = КонецМинуты(Дата); // 08.02.2019 14:30:59
// Арифметические операции с датами
Дата1 = Дата(2019, 3, 8, 14, 30, 15); // 08.03.2019 14:30:15
Дата2 = Дата1 - Дата(2018, 1, 1, 0, 0, 0); // 37 290 615
Дата3 = Дата(2018, 1, 1, 0, 0, 0) + Дата2; // 08.03.2019 14:30:15
КоличествоСекунд = Дата1 - Дата3; // 0
Или есть все же что-то еще? :)
Синтаксис языка запросов также позволяет выполнять некоторые манипуляции с датами на уроне СУБД. Конечно, количество доступных операций меньше, чем возможности самой СУБД (будь это SQL Server или PostgreSQL), но многие задачи все же позволяет решать.
Операции с датами из языка запросов
Язык запросов платформы также поддерживает различные операции над датами.
ВЫБРАТЬ
// Инициализация даты в запросе
ДАТАВРЕМЯ(2019, 3, 8, 14, 30, 50) КАК ДатаЧастями,
&ДатаПараметром КАК ДатаПараметром,
// Части даты
ГОД(&ДатаПараметром) КАК Год,
КВАРТАЛ(&ДатаПараметром) КАК Квартал,
МЕСЯЦ(&ДатаПараметром) КАК Месяц,
ДЕНЬГОДА(&ДатаПараметром) КАК ДеньГода,
ДЕНЬ(&ДатаПараметром) КАК День,
НЕДЕЛЯ(&ДатаПараметром) КАК Неделя,
ДЕНЬНЕДЕЛИ(&ДатаПараметром) КАК ДеньНедели,
ЧАС(&ДатаПараметром) КАК Час,
МИНУТА(&ДатаПараметром) КАК Минута,
СЕКУНДА(&ДатаПараметром) КАК Секунда,
// Преобразование даты к указанному периоду
// Возможные типы периодов:
// - Минута
// - Час
// - День
// - Неделя
// - Месяц
// - Квартал
// - Год
// - Декада
// - Полугодие
НАЧАЛОПЕРИОДА(&ДатаПараметром, ДЕНЬ) КАК НачалоПериода,
КОНЕЦПЕРИОДА(&ДатаПараметром, ДЕНЬ) КАК КонецПериода,
ДОБАВИТЬКДАТЕ(&ДатаПараметром, МЕСЯЦ, 6) КАК ДобавитьКДате,
// Разность дат для указанного периода
// Из 2 параметра вычитается первый.
РАЗНОСТЬДАТ(&ДатаПараметром, &ТекущаяДата, ДЕНЬ) КАК РазностьДат
Функционал работы с датами в запросах похож на тот, что мы видели в коде встроенного языка платформы. Исключение - это функции преобразования даты к началу и концу периода, а также разность дат и добавление периода к дате. Но не смотря на различия, все эти действия можно выполнять как в коде, так и в запросах.
Даты нельзя можно преобразовать к строке и наоборот. Во встроенном языке преобразование доступно, например, с помощью функции "Строка()". По замечанию philya, исправил информацию, что дату нельзя преобразовать к строке в запросе.
ВЫБРАТЬ
ПРЕДСТАВЛЕНИЕ(ДАТАВРЕМЯ(2019, 1, 1)) КАК ДатаСтрокой,
ПРЕДСТАВЛЕНИЕ(&ТекущаяДата) КАК ПредставлениеПараметраДатаСтрокой
Еще одной особенностью работы с датами в запросах является использование выражения "МЕЖДУ", с помощью которого в краткой форме можно установить фильтр по диапазону дат. Вот пример.
ВЫБРАТЬ
НекотороеПоле
ИЗ
КакойТоТамТаблицы
ГДЕ
ПолеПериод МЕЖДУ &НачалоПериода И &КонецПериода
Этот запрос будет аналогичен следующему.
ВЫБРАТЬ
НекотороеПоле
ИЗ
КакойТоТамТаблицы
ГДЕ
ПолеПериод >= &НачалоПериода И ПолеПериод <= &КонецПериода
В отличии от встроенного языка, в запросе есть ряд ограничений:
- Нельзя выполнить инициализацию даты, задавая параметры с помощью других выражений. Например, такой запрос не пройдет синтаксический контроль.
ВЫБРАТЬ
ДАТАВРЕМЯ(ГОД(&ТекущаяДата), 3, 8, 14, 30, 50) КАК ДатаЧастями
- Недоступны операции сложения или вычитания с датами.
ВЫБРАТЬ
&ТекущаяДата - &ДругаяДата КАК ВыражениеСДатами,
&ТекущаяДата + ДАТАВРЕМЯ(2019, 3, 8, 14, 30, 50) КАК ДругоеВыражениеСДатами
Большинство ограничений обусловлены именно особенностями платформы, а не возможностями СУБД. По крайней мере SQL Server и PostgreSQL позволяют выполнять SQL-запросы с подобным синтаксисом.
Для того, чтобы понять является ли дата пустым значением, необходимо:
- Для встроенного языка сравнить значение с датой "01.01.01 00:00:00" или использовать функцию "ЗначениеЗаполнено()".
- В запросах необходимо сравнить со значением "01.01.01 00:00:00", например вот так:
ВЫБРАТЬ
&ТекущаяДата = ДатаВремя(1,1,1) ЭтоПустаяДата
На этом основные возможности работы с датами исчерпаны. Есть чем дополнить? Комментарии ждут Вас!
Именно эти возможности мы, разработчики, чаще всего используем при работе со временем в алгоритмах, запросах и т.д. Казалось бы, что еще можно придумать?
Может дальше будет интересней
Сквозь время и часовые пояса
На самом деле не все так просто! Если "копать" дальше, то первая тема, которая может усложнить жизнь разработчиков - это часовые пояса. Пока Вы работаете с файловой или клиент-серверной базой, в которой все пользователи находятся в одном часовом поясе, то проблем нет. Но как только бизнес вырос и стал распределенным между регионами в разных часовых поясах, вот тут то веселье и начинается!
Для начала рассмотрим какие инструменты предлагает платформа 1С для таких ситуаций.
Что есть в платформе 1С для работы в разных часовых поясах
Платформа поддерживает все стандартные часовые пояса. Для получения списка часовых поясов из встроенного языка есть системные функции.
// Список доступных часовых поясов
МассивЧасовыхПоясов = ПолучитьДопустимыеЧасовыеПояса();
// Пример вывода
// ...
// - Europe/Amsterdam
// - Europe/Andorra
// - Europe/Astrakhan
// - Europe/Athens
// - Europe/Belfast
// - Europe/Belgrade
// - Europe/Berlin
// - Europe/Bratislava
// - Europe/Brussels
// - Europe/Bucharest
// - Europe/Budapest
// - Europe/Chisinau
// - Europe/Copenhagen
// ...
// Получение локализованного представления часового пояса
ПредставлениеЧасовогоПояса = ПредставлениеЧасовогоПояса("Europe/Astrakhan");
// Пример вывода
// - "GMT+04:00"
В этом примере, представление часового пояса - это локализованное значение для стандартного имени этого пояса. Вот небольшой пример.
Часовой пояс |
Представление (United States) |
Представление (Россия) |
Asia/Oral |
Indochina Time |
Индокитайское стандартное время |
Asia/Novosibirsk |
Omsk Time |
GMT+06:00 |
Europe/Moscow |
Eastern European Time |
Восточноевропейское время |
Полезная функция при локализации прикладных решений.
Отлично, у нас есть список часовых поясов и их локализованное представление. Но что с этим можно сделать?
Начнем с того, что у прикладных решений есть несколько стандартных настроек для часовых поясов:
- Часовой пояс информационной базы - определяет в каком часовом поясе находится информационная база. По умолчанию не определен, при этом часовым поясом базы считается часовой пояс сервера.
- Часовой пояс сеанса - указывает часовой пояс запущенного сеанса. Если не задан явно, то используется часовой пояс информационной базы или, если он не заполнен, часовой пояс сервера.
Таким образом, если у Вас пользователи базы распределены по разным часовым поясам, то настройка этих параметров строго обязательна, чтобы избавиться от проблем работы с датами и временем. Ниже мы на примере подробно рассмотрим на что каждый из этих параметров влияет, а пока продолжим рассматривать остальные функции работы с часовыми поясами.
Параметры часового пояса базы и сеанса
Начнем с примера работы с настройками информационной базы.
// Для установки часового поиска информационной базы
// нужно использовать монопольный режим
УстановитьМонопольныйРежим(Истина);
// Получаем текущий часовой пояс базы
ЧасовойПоясБазы = ПолучитьЧасовойПоясИнформационнойБазы();
Сообщить("До: " + ЧасовойПоясБазы);
// Устанавливаем новый часовой пояс
НовыйЧасовойПоясБазы = "Europe/Moscow";
УстановитьЧасовойПоясИнформационнойБазы(НовыйЧасовойПоясБазы);
// В этом случае функция вернет имя только что установленного
// часового пояса
ЧасовойПоясБазы = ПолучитьЧасовойПоясИнформационнойБазы();
Сообщить("После: " + ЧасовойПоясБазы);
УстановитьМонопольныйРежим(Ложь);
В примере мы считали текущий пояс ИБ, а после установили новое значение "Europe/Moscow". Значение устанавливаемого часового пояса должно быть одним из тех, которое возвращает функция "ПолучитьДопустимыеЧасовыеПояса()".
Для работы с часовым поясом сеанса используются следующие функции.
// Получаем текущий часовой пояс сеанса
ЧасовойПоясСеанса = ЧасовойПоясСеанса();
Сообщить("До: " + ЧасовойПоясСеанса);
// Устанавливаем новый часовой пояс текущего сеанса
НовыйЧасовойПоясСеанса = "Asia/Oral";
УстановитьЧасовойПоясСеанса(НовыйЧасовойПоясСеанса);
// Получаем новое значение часового пояса
ЧасовойПоясСеанса = ЧасовойПоясСеанса();
Сообщить("После: " + ЧасовойПоясСеанса);
Работа с часовым поясом сеанса очень похожа на работу с часовым поясом самой информационной базы, но есть существенные различия:
- Часовой пояс информационной базы сохраняется в базе данных, а часовой пояс сеанса должен устанавливаться каждый раз при его старте:
- В первую очередь выполняется попытка получить его из часового пояса информационной базы.
- Если для ИБ часовой пояс не установлен, то берется часовой пояс сервера.
- Если при старте сеанса часовой пояс сеанса устанавливается явно в коде встроенного языка, то в дальнейшем используется именно это значение.
- Установить часовой пояс базы можно только в монопольном режиме.
Для получения текущего часового пояса компьютера используется функция, внезапно, "ЧасовойПояс()".
Итого, для управления настройками часовых поясов всей базы или отдельного сеанса используется всего лишь несколько методов:
- ПолучитьЧасовойПоясИнформационнойБазы()
- УстановитьЧасовойПоясИнформационнойБазы("ИмяЧасовогоПояса")
- ЧасовойПояс()
- ЧасовойПоясСеанса()
- УстановитьЧасовойПоясСеанса()
// Получение текущего часового пояса компьютера
Сообщить(ЧасовойПояс());
Все эти функции не работают в тонком или веб-клиенте, поэтому чаще всего их вызовы нужно делать на сервере или в толстом клиенте, если он у Вас используется.
Окей, мы знаем как смотреть и настраивать часовые пояса. Теперь рассмотрим основные функции для их использования при работе с датой и временем.
Работы с датами в разных часовых поясах
Выше уже был пример использования функции "ТекущаяДата()", которая возвращает текущую дату и время для компьютера. Если функция вызвана на клиенте, то она вернет дату и время клиентского компьютера. Если ее вызвать на сервере, то будет возвращена дата и время сервера. Но есть и другие функции получения текущей даты.
&НаКлиенте
Процедура ПроверкаТекущейДаты(Команда)
// Текущая дата на клиенте
Сообщить("Текущая дата на клиенте: " + ТекущаяДата());
ПроверкаТекущейДатыНаСервере();
КонецПроцедуры
&НаСервере
Процедура ПроверкаТекущейДатыНаСервере()
// Текущая дата на сервере
Сообщить("Текущая дата на сервере: " + ТекущаяДата());
// Текущая универсальная дата (UTC)
Сообщить("Текущая универсальная дата на сервере: " + ТекущаяУниверсальнаяДата());
// Текущая универсальная дата в миллисекундах, начиная от 01.01.0001
Сообщить("Текущая универсальная дата в миллисекундах на сервере: " + ТекущаяУниверсальнаяДатаВМиллисекундах());
КонецПроцедуры
Из всех функций получения текущей даты и времени только одна может использоваться в тонком клиенте - это "ТекущаяДата()". Далее мы рассмотрим работу этих функций более подробно на примере.
Есть и другая более интересная функция получения текущей даты - это получение текущей даты сеанса.
// Информация от сервера. Для Windows настройки часового пояса
// и текущего времени задаются через "Панель управления -> Язык и региональные стандарты"
// и "Дата и время"
// Примечание: для разных версий Windows названия могут отличаться.
// В дистрибутивах Linux настройки также могут отличаться.
Сообщить("Часовой пояс сервера: " + ЧасовойПояс()); // Asia/Novosibirsk
Сообщить("Текущее время сервера: " + ТекущаяДата()); // 27.03.2019 22:35:28
// Ранее мы установили часовой пояс информационой базы "Europe/Moscow"
ЧасовойПоясБазы = ПолучитьЧасовойПоясИнформационнойБазы();
Сообщить("Часовой пояс информационной базы: " + ЧасовойПоясБазы); // Europe/Moscow
// До этого момента часовой пояс сеанса не был настроен.
// Т.к. мы установили пояс информационной базы, то часовой пояс
// сеанса теперь по умолчанию имеет такое же значение.
// Примечание: если часовой пояс базы не был бы настроен,
// то по умолчанию устанавливался бы часовой пояс сервера.
Сообщить("Часовой пояс сеанса: " + ЧасовойПоясСеанса()); // Europe/Moscow
Сообщить("Текущее время сеанса: " + ТекущаяДатаСеанса()); // 27.03.2019 18:35:28
// Явно установим часовой пояс сеанса
УстановитьЧасовойПоясСеанса("Asia/Oral");
Сообщить("Часовой пояс сеанса: " + ЧасовойПоясСеанса()); // Asia/Oral
Сообщить("Текущее время сеанса: " + ТекущаяДатаСеанса()); // 27.03.2019 20:35:28
// И поменяем его еще раз!
УстановитьЧасовойПоясСеанса("Europe/Amsterdam");
Сообщить("Часовой пояс сеанса: " + ЧасовойПоясСеанса()); // Europe/Amsterdam
Сообщить("Текущее время сеанса: " + ТекущаяДатаСеанса()); // 27.03.2019 16:35:28
Текущая дата сеанса - очень важное понятие для информационных баз, где работа ведется в разных часовых поясах. Фактически это дата сервера, приведенная к часовому поясу сеанса. По стандартам разработки именно ее следует использовать в прикладном коде, вместо функции "ТекущаяДата()", за исключением редких случаев. Например, при создании новых документов используется именно текущая дата сеанса, позволяющая поддерживать единую последовательность документов для часовых поясов.
Всегда использовали текущую дату на сервере и никогда не испытывали проблем? Вам повезло! Значит Ваша база использует единый часовой пояс, который совпадает с часовым поясом сервера.
Также важный момент - это использование функции "ТекущаяДата()" на клиенте. В абсолютном большинстве случаев это будет ошибкой, т.к. привязываться к текущей дате клиента очень ненадежно, даже если его часовой пояс совпадает с часовым поясом информационной базы. Причин этому несколько:
- Часовой пояс клиента может быть любым. Это может быть пользователь, зашедший в базу часового пояса "Europe\Moscow", но находящийся в часовом поясе "America/New_York". Фактически мы никак не можем использовать текущую дату клиента, т.к. она сильно отвязана от часового пояса системы.
- Даже если часовые пояса клиента и информационной базы совпадают, то клиентскую дату и время все равно нельзя использовать, потому что она может отличаться от серверной даты. Например, если на клиенте часы ошибочно идут "назад" на 10 минут, то при использовании этой даты для проверки остатков или другой информации может произойти ошибка в учете. Пользователь продаст товар, который 10 минут назад уже распродали!
Но что же делать, если на клиенте нужна текущая дата? Самым правильным подходом будет использование текущей даты сеанса, которую нужно получить с сервера на клиент. В некоторых случаях можно использовать дату документа, но нужно смотреть по конкретной задаче.
И напоследок рассмотрим еще несколько функций, которые платформа предоставляет для работы с часовыми поясами.
ЧасовойПоясСервера = ЧасовойПояс(); // Asia/Novosibirsk
ТекущаяДатаСервера = ТекущаяДата(); // 28.03.2019 1:27:06
ЧасовойПоясИнформационнойБазы = ПолучитьЧасовойПоясИнформационнойБазы(); // Europe/Moscow
УниверсальнаяДата = УниверсальноеВремя(ТекущаяДатаСервера, ЧасовойПоясСервера); // 27.03.2019 18:27:06
ТекущаяДатаИнформационнойБазы = МестноеВремя(УниверсальнаяДата, ЧасовойПоясИнформационнойБазы); // 27.03.2019 21:27:06
// Часовой пояс сеанса в этом случае аналогичен
// часовому поясу информационной базы
ЧасовойПоясСеанса = ЧасовойПоясСеанса(); // Europe/Moscow
// Текущая дата сеанса, получаемая платформенной функцией,
// такая же как и "ТекущаяДатаИнформационнойБазы", которую мы получили выше
ТекущаяДатаСеанса = ТекущаяДатаСеанса(); // 27.03.2019 21:27:06
// Смещение времени в секундах между указанным часовым поясом и универсальным временем (UTC)
СмещениеОтСтандартногоВремени = СмещениеСтандартногоВремени(ЧасовойПоясИнформационнойБазы, УниверсальнаяДата); // 10 800 секунд (3 часа)
// Смещение летнего времени в секундах для указанного часового пояса
СмещениеЛетнегоВремени = СмещениеЛетнегоВремени(ЧасовойПоясИнформационнойБазы, УниверсальнаяДата); // 0 секунд
Это все основные возможности платформы 1С при работе с часовыми поясами.
Теперь Вы точно должны знать и понимать, что платформа 1С поддерживает работу с разными часовыми поясами в одной информационной базе, а также имеет обширный функционал для работы с датой и временем в таком режиме. Многие разработчики не сталкивались с особенностями работы в нескольких часовых поясах и не видят смысла в использовании функции"ТекущаяДатаСеанса()" вместо привычной "ТекущаяДата()". С этим трудно поспорить, т.к. это действительно привычней, да и использование текущей даты сеанса в большинстве конфигураций имеет то же самое поведение, что и использование текущей даты сервера.
Однако, теперь Вы понимаете, к чему это может привести:
- При изменении часового пояса в настройках сервера это повлияет на поведение функции "ТекущаяДата()", "ЧасовойПояс()" и др. А что если пояс поменяли ошибочно или база просто переехала в облако, но часовой пояс не должен был измениться?
- При необходимости организовать работу в базе в разных часовых поясах потребуются дополнительные усилия по доработке конфигурации. Иногда значительные и с .... непредсказуемыми последствиями.
Часовые пояса могут использоваться и в других случаях, не только когда пользователи разделены географически:
- Необходимость знать локальное время географически распределенных подразделений и филиалов, чтобы ограничить различные email / sms-рассылки дневным временем.
- Для корректной работы интеграции с различными сервисами и службами.
- И др.
В этих случаях в прикладном решении создаются настройки часовых поясов для объектов, а далее рассчитывается дата и время в зависимости от этих параметров. Подробнее на этом останавливаться не будем.
Как понять, есть ли необходимость разбивать работу сеансов на разные часовые пояса? Можно выделить две основные причины:
- В информационной базе ведется учет по нескольким обособленным управленческим единицам (организациям, филиалам), которые находятся в разных часовых поясах и ведут относительно обособленный учет. В этом случае использование часовых поясов избавит от множества проблем в учете, ведь то же закрытие месяца должно проходить с учетом локального времени.
- В базе ведется учет по одной организации, но с распределенными филиалами, складами и т.д. То есть организация одна, но ее подразделения могут быть в разных часовых поясах. Например:
- Управленческий офис в Москве
- Центральный склад в Тюмени
- Производство в Новосибирске и Владивостоке
Для корректного учета Новосибирск и Владивосток работают по местному времени. Чтобы учет на центральном складе был корректным мы не можем использовать местное время производств, поэтому выходом будет использовать время управленческого офиса. Итого, будут использоваться три часовых пояса (Москва, Новосибирск, Владивосток).
Это лишь общие примеры. На практике все либо гораздо проще, когда организации полностью обособлены друг от друга, а вся информация "сливается" в единую базу без изменения времени. Либо все гораздо сложнее, когда нужно и даты преобразовывать при интеграции, да еще и отчетность как-то при этом собирать. Здесь нет готовых рецептов, т.к. все зависит от конкретной задачи.
Подробнее про работу в разных часовых поясах Вы можете прочитать в официальных источниках:
А теперь перейдем к небольшому примеру.
Очень простой пример
Рассмотрим простой пример, который можно встретить в реальном бизнесе. Компания работает по всей стране. Центральный офис и центральный склад находятся в Москве. Два дополнительных обособленных офиса открыты в Тюмени и Новосибирске. Информационная база одна для всех.
Так как учет в удаленных подразделениях обособленный, то работа там ведется по местному времени. Для этого сделаны следующие настройки:
- Для информационной базы установлен часовой пояс "Europe/Moscow"
- Для пользовательских сеансов в Новосибирске при запуске устанавливается часовой пояс "Asia/Novosibirsk"
- Для сеансов из Тюмени при запуске устанавливается часовой пояс "Asia/Oral"
Сравним результаты вызова основных функций работы со временем на сервере.
Метод платформы (вызов на сервере) |
Пользователи в Москве |
Пользователи в Новосибирске |
Пользователи в Тюмени |
ЧасовойПояс() |
Europe/Moscow |
Europe/Moscow |
Europe/Moscow |
ПолучитьЧасовойПоясИнформационнойБазы() |
Europe/Moscow |
Europe/Moscow |
Europe/Moscow |
ЧасовойПоясСеанса() |
Europe/Moscow |
Asia/Novosibirsk |
Asia/Oral |
ТекущаяДата() |
30.03.2019 23:01:01 |
30.03.2019 23:01:01 |
30.03.2019 23:01:01 |
ТекущаяДатаСеанса() |
30.03.2019 23:01:01 |
31.03.2019 1:01:01 |
31.03.2019 3:01:01 |
ТекущаяУниверсальнаяДата() |
30.03.2019 20:01:01 |
30.03.2019 20:01:01 |
30.03.2019 20:01:01 |
ТекущаяУниверсальнаяДатаВМиллисекундах() |
63 689 572 861 383 |
63 689 572 861 383 |
63 689 572 861 383 |
Поскольку сервер один, то функции "ЧасовойПояс()" и "ТекущаяДата()" возвращают один и тот же результат для всех сеансов. Часовой пояс информационной базы тоже один для всех, ведь база то одна. Универсальная дата, в т.ч. и в миллисекундах, тоже одинаковое, т.к. время в UTC не может различаться. А вот часовые пояса сеансов и, что логично, текущие даты сеансов различаются. Все из-за того, что при запуске приложения в Тюмени и Новосибирске, пользователям программно устанавливаются часовые пояса, отличные от часового пояса информационной базы. Вот так наглядно можно проверить работу этого механизма.
Часовые пояса сеансов позволили организовать обособленный учет, при этом вся компания работает в единой информационной базе. Теперь Вы представляете для чего нужен часовой пояс информационной базы и часовые пояса сеансов. На простом примере мы рассмотрели работу с ними, а ранее пробежались практически по всем механизмам платформы для работы со временем. Теперь рассмотрим более сложный пример, а после перейдем к другим особенностям.
Нереальная инфраструктура
А что если немного усложнить пример, совсем чуть-чуть. Вместо одного сервера приложений у нас будет кластер, состоящий из 3 серверов! При этом в базе позабыли настроить часовой пояс информационной базы, а про часовые пояса сеансов вообще никто не слышал! Но и это еще не все! Каждый сервер кластера будет находиться в своем часовом поясе!
Это ужас, но мы ведь говорим лишь об искусственном примере. Вряд ли кому-то придет в голову использовать такое на боевой инфраструктуре.
Вернемся к примеру. Поскольку сервера находятся в разных часовых поясах, а настройки информационной базы и сеансов не выполнены в части часовых поясов, то мы получим вот такую странную картину.
Метод платформы (вызов на сервере) |
Сервер №1 |
Сервер №2 |
Сервер №3 |
ЧасовойПояс() |
Europe/Moscow |
Asia/Oral |
America/New_York |
ПолучитьЧасовойПоясИнформационнойБазы() |
Неопределено |
Неопределено |
Неопределено |
ЧасовойПоясСеанса() |
Europe/Moscow |
Asia/Oral |
America/New_York |
ТекущаяДата() |
30.03.2019 23:01:05 |
31.03.2019 1:01:05 |
30.03.2019 16:01:05 |
ТекущаяДатаСеанса() |
30.03.2019 23:01:06 |
31.03.2019 1:01:06 |
30.03.2019 16:01:06 |
ТекущаяУниверсальнаяДата() |
30.03.2019 21:00:54 |
30.03.2019 21:00:54 |
30.03.2019 21:00:54 |
ТекущаяУниверсальнаяДатаВМиллисекундах() |
63 689 576 454 039 |
63 689 576 454 039 |
63 689 576 454 039
|
Поскольку в кластере три сервера 1С, а часовой пояс информационной базы не настроен, то настройка часового пояса получается непосредственно с сервера. Но у нас их 3! И все они находятся в разных поясах. Допустим, сервер №1 - это центральный сервер кластера, а сервера №2 и №3 - это рабочие сервера. Кластер распределяет нагрузку и перенаправляет сеансы 1С то на 1, то на 2 сервер, а кому-то повезло и сеанс запустился на 3 сервере.
Вот и представьте, какие странные даты они будут получать как с помощью "ТекущаяДата()", так и с помощью "ТекущаяДатаСеанса()". Все это приведет к большому количеству ошибок в системе, ведь последовательность ввода документов не будет поддаваться никаким правилам. Фактически, документы будут вводиться в хаотичном порядке.
Если бы у нас был настроен хотя бы часовой пояс информационной базы, то таких проблем бы уже удалось избежать.
В мире БСП
Возможности платформы 1С при работе со временем покрывают большинство потребностей, но в некоторых случаях использовать их не очень удобно или неэффективно. На помощь в этом случае приходит библиотека стандартных подсистем (БСП). Далее рассмотрим некоторые особенности работы с датой и временем в БСП, в т.ч. и с часовыми поясами. Информация для различных версий БСП может отличаться, но основные моменты практически всегда остаются неизменными. С учетом того, что БСП применяется для большинства типовых конфигураций, эта информация может быть полезна для разработчиков, особенно если Вы не любите изобретать велосипеды.
Начнем с простого. Выше мы уже говорили, что использовать функцию "ТекущаяДата()" на клиенте неприемлемо и правильнее использовать дату сервера, приведенную к часовому поясу сеанса. Конечно, можно это делать каждый раз вызывая сервер, писать свои функции возврата этой даты и т.д. Но если у Вас встроена в конфигурацию БСП, то лучше использовать функцию "ОбщегоНазначенияКлиент.ДатаСеанса()" и "ОбщегоНазначенияКлиент.ДатаУниверсальная(), которые получат необходимые значения с оптимизацией клиент-серверных вызовов.
ТекущаяДатаСеанса = ОбщегоНазначенияКлиент.ДатаСеанса();
ТекущаяДатаУниверсальная = ОбщегоНазначенияКлиент.ДатаУниверсальная();
Но как им удается получить эти значения без вызова сервера?
// Возвращает текущую дату, приведенную к часовому поясу сеанса.
//
// Функция возвращает время, близкое к результату функции ТекущаяДатаСеанса() в серверном контексте.
// Погрешность обусловлена временем выполнения серверного вызова.
// Предназначена для использования вместо функции ТекущаяДата().
//
Функция ДатаСеанса() Экспорт
Возврат ТекущаяДата() + СтандартныеПодсистемыКлиентПовтИсп.ПараметрыРаботыКлиента().ПоправкаКВремениСеанса;
КонецФункции
// Возвращает универсальную дату сеанса, получаемую из текущей даты сеанса.
//
// Функция возвращает время, близкое к результату функции УниверсальноеВремя() в серверном контексте.
// Погрешность обусловлена временем выполнения серверного вызова.
// Предназначена для использования вместо функции УниверсальноеВремя().
//
Функция ДатаУниверсальная() Экспорт
ПараметрыРаботыКлиента = СтандартныеПодсистемыКлиентПовтИсп.ПараметрыРаботыКлиента();
ДатаСеанса = ТекущаяДата() + ПараметрыРаботыКлиента.ПоправкаКВремениСеанса;
Возврат ДатаСеанса + ПараметрыРаботыКлиента.ПоправкаКУниверсальномуВремени;
КонецФункции
Как мы видим, обе функции получают кэш параметров работы клиента, который инициализируется при старте сеанса. Эти параметры содержат поправку в секундах к времени сеанса, а также поправку к универсальному времени. Используя функцию "Текущаядата()" (да, да, в этом случае ее использование обосновано) мы можем вычислить необходимую дату и время, но с некоторой приемлемой погрешностью из-за клиент-серверного вызова.
Что касается часовых поясов, то основной пояс информационной базы настраивается в панели администрирования. О работе с этим параметром из кода встроенного языка мы уже говорили ранее.
Также в БСП содержится функционал работы с часовыми поясами в модели сервиса, но его мы не будем рассматривать, т.к. на практике с ним приходится редко сталкиваться, если, конечно, вы не работаете в базах модели сервиса.
Более тонких настроек при работе с датой и временем в БСП трудно найти. Обычно, если возникают задачи с управлением часовыми поясами, то проще адаптировать конфигурацию под свои нужды, т.к. готовых решений для сложных задач с часовыми поясами нет ни в БСП, ни в большинстве поставляемых типовых решениях.
На стороне базы данных
В базе данных даты могут храниться в некотором измененном виде. Например, если Вы используйте SQL Server, то при стандартной настройке используется смещение в 2000 лет, которое позволяет обойти ограничение СУБД на минимальную дату 01.01.1753. Для платформы 1С это критично, т.к. не позволит сохранить пустую дату 01.01.0001 в базе. Вот так, например, выглядят даты в таблице.
Если же речь идет о PostgreSQL, то смещение дат в этом случае использовать не обязательно, т.к. СУБД позволяет хранить дату 01.01.0001 без лишних телодвижений.
Настройка смещения дат задается при создании информационной базы и доступна только для SQL Server.
В самой базе настройка смещения хранится в таблице "_YearOffset". Изменять настройку вручную не рекомендуется, т.к. может привести к непредсказуемым последствиям (в основном не очень хороших). При получении дат из базы платформа уже преобразовывает их к нужному виду с учетом параметра смещения.
Для хранения даты и времени в таблицах базы данных используется тип "datetime2(0)", который позволяет хранить большой диапазон дат, и при этом более высокую точность в долях секунд. Кстати, платформа 1С эту высокую точность не использует и всегда сохраняет значения с точностью до секунды, миллисекунды не используются! На первый взгляд в этом случае логичнее было бы использовать тип "datetime", но на самом деле нет. По рекомендациям Microsoft тип "datetime2" является более предпочтительным из-за расширенных возможностей.
Теперь Вы знаете как платформа 1С хранит дату и время на стороне базы данных и не испугаетесь, увидев четвертое тысячелетие в таблице.
Скрытая особенность
В одной из прошлых статей "Баг или фича? Неожиданное поведение платформы" было описание интересной особенности при работе с датами в коде встроенного языка. Речь идет о недокументированной фиче (или баге, кто знает), заключающейся в поддержке миллисекунд в датах.
ДатаОбычная = Дата(2019,2,1);
ДатаСтранная = ДатаОбычная + 0.001;
СравнениеДат1 = ДатаСтранная = ДатаОбычная;
Сообщить("Вариант с датами 1: " + СравнениеДат1); // ЛОЖЬ - даты не равны
ДатаСтранная = ДатаСтранная - 0.001;
СравнениеДат2 = ДатаСтранная = ДатаОбычная;
Сообщить("Вариант с датами 2: " + СравнениеДат2); // ИСТИНА - даты равны
При этом в отладке даже сразу и не определить, что дата содержит миллисекунды. Но можно использовать такой способ.
ДатаОбычная = Дата(2019,2,1);
ДатаСтранная = ДатаОбычная + 0.068;
МиллисекундВДате = ДатаСтранная - ДатаОбычная;
Если ЗначениеЗаполнено(МиллисекундВДате) Тогда
Сообщить("В дате содержатся миллисекунды: " + МиллисекундВДате);
Иначе
Сообщить("Миллисекунд в дате нет!");
КонецЕсли;
Как это относится к решению практических задач? Например, в коде к дате случайно может быть прибавлено дробное число. После этого мы можем получить случаи, когда при сравнении дат или при указании фильтров поиска (поиск в таблице значений, по ключу в соответствии и др.) мы не получим ожидаемый результат, ведь даты то будут разные. При этом сразу не удастся разобраться что вообще происходит.
Будьте бдительны! Миллисекунды ждут, когда Вы ими воспользуетесь!
Момент, пожалуйста!
Есть еще одна сущность, относящаяся к работе с датой и временем в платформе 1С - это момент времени! Его использование может быть критичным при получении остатков, если в одну секунду в системе были введены сразу несколько документов. То есть, момент времени позволяет различать позицию документов на временной оси, даже если дата и время у них одинаковая. Достигается это за счет того, что момент содержит не только дату, но и ссылку на конкретный документ.
Но и это еще не все! Есть также такая сущность как "Граница", которая позволяет хранить границы некоторого интервала значений. При этом также дает возможность указать признак включения / исключения граничного значения интервала.
Рассмотрим несколько примеров на некотором регистре накопления вида "Остатки", где применим возможные способы использования даты, момента времени и границы.
Классический случай, хоть и может быть не совсем корректным с прикладной точки зрения - в качестве параметра "Период" в виртуальную таблицу остатков передается обычная дата.
ДокументОбъект = Документы.ПростойДокумент.НайтиПоНомеру("000000001");
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| КакойТоТамРегистрОстатковОстатки.Номенклатура КАК Номенклатура,
| КакойТоТамРегистрОстатковОстатки.КоличествоОстаток КАК КоличествоОстаток
|ИЗ
| РегистрНакопления.КакойТоТамРегистрОстатков.Остатки(
| &Период,
| Номенклатура = &Номенклатура)
| КАК КакойТоТамРегистрОстатковОстатки";
Запрос.УстановитьПараметр("Номенклатура", Справочники.Номенклатура.Товар1);
Запрос.УстановитьПараметр("Период", ДокументОбъект.Дата);
РезультатЗапроса = Запрос.Выполнить();
В этом случае платформа выполнит такой запрос на стороне базы данных.
-- Запрос с использованием даты
SELECT
T1.Fld27RRef,
T1.Fld28Balance_
FROM (
SELECT
T2.Fld27RRef AS Fld27RRef,
CAST(SUM(T2.Fld28Balance_) AS NUMERIC(33, 2)) AS Fld28Balance_
FROM (
-- Получение данных из текущих итогов
SELECT
T3._Fld27RRef AS Fld27RRef,
CAST(SUM(T3._Fld28) AS NUMERIC(27, 2)) AS Fld28Balance_
FROM dbo._AccumRgT29 T3
-- Фильтр накладывается только по дате текущих итогов (3999-11-01 00:00:00 - дата конца света по календарю 1С)
WHERE T3._Period = @P1 AND ((T3._Fld27RRef = @P2)) AND (T3._Fld28 <> @P3) AND (T3._Fld28 <> @P4)
GROUP BY T3._Fld27RRef
HAVING (CAST(SUM(T3._Fld28) AS NUMERIC(27, 2))) <> 0.0
UNION ALL
-- Получение данных из основной таблицы движений (без использования итогов)
SELECT
T4._Fld27RRef AS Fld27RRef,
CAST(CAST(SUM(CASE WHEN T4._RecordKind = 0.0 THEN -T4._Fld28 ELSE T4._Fld28 END) AS NUMERIC(21, 2)) AS NUMERIC(27, 2)) AS Fld28Balance_
FROM dbo._AccumRg26 T4
-- Фильтр накладывается по периоду, с учетом переданного параметра в запрос
-- Обычно здесь указывается период таким образом, чтобы получить записи, для которых итоги еще не рассчитаны,
-- но вникать в работу виртуальных таблиц сейчас не будем.
WHERE T4._Period >= @P5 AND T4._Period < @P6 AND T4._Active = 0x01 AND ((T4._Fld27RRef = @P7))
GROUP BY T4._Fld27RRef
HAVING (CAST(CAST(SUM(CASE WHEN T4._RecordKind = 0.0 THEN -T4._Fld28 ELSE T4._Fld28 END) AS NUMERIC(21, 2)) AS NUMERIC(27, 2))) <> 0.0
) T2
GROUP BY T2.Fld27RRef
HAVING (CAST(SUM(T2.Fld28Balance_) AS NUMERIC(33, 2))) <> 0.0
) T1
Ничего особенного, обычные фильтры по датам. Обратите внимание, что при получении данных из основной таблицы движений, условие по периоду сделано таким образом, что не учитывает последнюю секунду переданного параметра "Период".
WHERE
T4._Period >= @P5
AND
T4._Period < @P6 -- Вот оно!
Это одна из самых популярных особенностей виртуальных таблиц остатков, которую должны знать разработчики. Иначе можете потерять остатки в отчетах и других запросах.
Остатки на момент времени
Теперь попробуем вместо даты передать момент времени.
ДокументОбъект = Документы.ПростойДокумент.НайтиПоНомеру("000000001");
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| КакойТоТамРегистрОстатковОстатки.Номенклатура КАК Номенклатура,
| КакойТоТамРегистрОстатковОстатки.КоличествоОстаток КАК КоличествоОстаток
|ИЗ
| РегистрНакопления.КакойТоТамРегистрОстатков.Остатки(
| &Период,
| Номенклатура = &Номенклатура)
| КАК КакойТоТамРегистрОстатковОстатки";
Запрос.УстановитьПараметр("Номенклатура", Справочники.Номенклатура.Товар1);
Запрос.УстановитьПараметр("Период", ДокументОбъект.МоментВремени());
РезультатЗапроса = Запрос.Выполнить();
В этом случае запрос на стороне базы данных уже будет несколько отличаться.
-- Запрос с использованием момента времени
SELECT
T1.Fld27RRef,
T1.Fld28Balance_
FROM (
SELECT
T2.Fld27RRef AS Fld27RRef,
CAST(SUM(T2.Fld28Balance_) AS NUMERIC(33, 2)) AS Fld28Balance_
FROM (
-- Получение данных из текущих итогов
SELECT
T3._Fld27RRef AS Fld27RRef,
CAST(SUM(T3._Fld28) AS NUMERIC(27, 2)) AS Fld28Balance_
FROM dbo._AccumRgT29 T3
-- Фильтр накладывается только по дате текущих итогов (3999-11-01 00:00:00 - дата конца света по календарю 1С)
WHERE T3._Period = @P1 AND ((T3._Fld27RRef = @P2)) AND (T3._Fld28 <> @P3) AND (T3._Fld28 <> @P4)
GROUP BY T3._Fld27RRef
HAVING (CAST(SUM(T3._Fld28) AS NUMERIC(27, 2))) <> 0.0
UNION ALL
-- Получение данных из основной таблицы движений (без использования итогов)
SELECT
T4._Fld27RRef AS Fld27RRef,
CAST(CAST(SUM(CASE WHEN T4._RecordKind = 0.0 THEN -T4._Fld28 ELSE T4._Fld28 END) AS NUMERIC(21, 2)) AS NUMERIC(27, 2)) AS Fld28Balance_
FROM dbo._AccumRg26 T4
-- Фильтр накладывается по периоду, с учетом переданного параметра в запрос,
-- а также добавилось условие, что ссылка на регистратор больше ИЛИ равен ссылке, которая находилась в переданном моменте времени
WHERE (T4._Period > @P5 OR T4._Period = @P6 AND T4._RecorderRRef >= @P7) AND T4._Period < @P8
-- В этом случае параметр @P5 указывает с какой даты получать движения, для которых еще нет итогов
-- Параметр @P6 - это дата из момента времени
-- А параметр @P8 - это дата, на которую хранятся текущие итоги
-- Опять же, углубляться в работу виртуальной таблицы не будем :)
AND T4._Active = 0x01 AND ((T4._Fld27RRef = @P9))
GROUP BY T4._Fld27RRef
HAVING (CAST(CAST(SUM(CASE WHEN T4._RecordKind = 0.0 THEN -T4._Fld28 ELSE T4._Fld28 END) AS NUMERIC(21, 2)) AS NUMERIC(27, 2))) <> 0.0
) T2
GROUP BY T2.Fld27RRef
HAVING (CAST(SUM(T2.Fld28Balance_) AS NUMERIC(33, 2))) <> 0.0
) T1
По сравнению с примером, где была передана обычная дата, тут уже интереснее.
Во-первых, теперь изменилась логика отбора:
- Дата движения должна быть в периоде, где движения еще не рассчитаны
- ИЛИ дата движения должна равняться дате из момента времени. При этом ссылка регистратора может быть больше или равна ссылке из момента времени.
Во-вторых, теперь у нас могут быть потенциальные проблемы с производительностью из-за использования условия "OR" (ИЛИ) в запросе. Ведь всем известно, что при использовании "ИЛИ" в фильтрах запросов индексы не могут быть использованы.
Но давайте посмотрим, что будет, если использовать границу.
Остатки на границу с видом "Исключая"
Для начала попробуем использовать границу с видом "Исключая".
ДокументОбъект = Документы.ПростойДокумент.НайтиПоНомеру("000000001");
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| КакойТоТамРегистрОстатковОстатки.Номенклатура КАК Номенклатура,
| КакойТоТамРегистрОстатковОстатки.КоличествоОстаток КАК КоличествоОстаток
|ИЗ
| РегистрНакопления.КакойТоТамРегистрОстатков.Остатки(
| &Период,
| Номенклатура = &Номенклатура)
| КАК КакойТоТамРегистрОстатковОстатки";
Запрос.УстановитьПараметр("Номенклатура", Справочники.Номенклатура.Товар1);
ГраницаПолученияОстатков = Новый Граница(ДокументОбъект.МоментВремени(), ВидГраницы.Исключая);
Запрос.УстановитьПараметр("Период", ГраницаПолученияОстатков);
РезультатЗапроса = Запрос.Выполнить();
Что же на стороне СУБД?
-- Запрос с использованием границы (вид границы = Исключая)
SELECT
T1.Fld27RRef,
T1.Fld28Balance_
FROM (
SELECT
T2.Fld27RRef AS Fld27RRef,
CAST(SUM(T2.Fld28Balance_) AS NUMERIC(33, 2)) AS Fld28Balance_
FROM (
-- Получение данных из текущих итогов
SELECT
T3._Fld27RRef AS Fld27RRef,
CAST(SUM(T3._Fld28) AS NUMERIC(27, 2)) AS Fld28Balance_
FROM dbo._AccumRgT29 T3
-- Фильтр накладывается только по дате текущих итогов (3999-11-01 00:00:00 - дата конца света по календарю 1С)
WHERE T3._Period = @P1 AND ((T3._Fld27RRef = @P2)) AND (T3._Fld28 <> @P3) AND (T3._Fld28 <> @P4)
GROUP BY T3._Fld27RRef
HAVING (CAST(SUM(T3._Fld28) AS NUMERIC(27, 2))) <> 0.0
UNION ALL
-- Получение данных из основной таблицы движений (без использования итогов)
SELECT
T4._Fld27RRef AS Fld27RRef,
CAST(CAST(SUM(CASE WHEN T4._RecordKind = 0.0 THEN -T4._Fld28 ELSE T4._Fld28 END) AS NUMERIC(21, 2)) AS NUMERIC(27, 2)) AS Fld28Balance_
FROM dbo._AccumRg26 T4
-- Фильтр накладывается по периоду, с учетом переданного параметра в запрос,
-- а также добавилось условие, что ссылка на регистратор больше ссылки, которая находилась в переданном моменте времени
WHERE (T4._Period > @P5 OR T4._Period = @P6 AND T4._RecorderRRef >= @P7) AND T4._Period < @P8 AND T4._Active = 0x01 AND ((T4._Fld27RRef = @P9))
-- В этом случае параметр @P5 указывает с какой даты получать движения, для которых еще нет итогов
-- Параметр @P6 - это дата из момента времени
-- А параметр @P8 - это дата, на которую хранятся текущие итоги
GROUP BY T4._Fld27RRef
HAVING (CAST(CAST(SUM(CASE WHEN T4._RecordKind = 0.0 THEN -T4._Fld28 ELSE T4._Fld28 END) AS NUMERIC(21, 2)) AS NUMERIC(27, 2))) <> 0.0
) T2
GROUP BY T2.Fld27RRef
HAVING (CAST(SUM(T2.Fld28Balance_) AS NUMERIC(33, 2))) <> 0.0
) T1
Если посмотреть внимательно, то никаких различий от передачи обычного момента времени Вы не найдете. Фактически, варианты фильтра по моменту времени и границе вида "Исключая" идентичны.
Остатки на границу с видом "Включая"
И последний пример - граница с видом "Включая".
ДокументОбъект = Документы.ПростойДокумент.НайтиПоНомеру("000000001");
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| КакойТоТамРегистрОстатковОстатки.Номенклатура КАК Номенклатура,
| КакойТоТамРегистрОстатковОстатки.КоличествоОстаток КАК КоличествоОстаток
|ИЗ
| РегистрНакопления.КакойТоТамРегистрОстатков.Остатки(
| &Период,
| Номенклатура = &Номенклатура)
| КАК КакойТоТамРегистрОстатковОстатки";
Запрос.УстановитьПараметр("Номенклатура", Справочники.Номенклатура.Товар1);
ГраницаПолученияОстатков = Новый Граница(ДокументОбъект.МоментВремени(), ВидГраницы.Включая);
Запрос.УстановитьПараметр("Период", ГраницаПолученияОстатков);
РезультатЗапроса = Запрос.Выполнить();
В этот раз в SQL-запрос будет небольшое отличие.
-- Запрос с использованием границы (вид границы = Включая)
SELECT
T1.Fld27RRef,
T1.Fld28Balance_
FROM (
SELECT
T2.Fld27RRef AS Fld27RRef,
CAST(SUM(T2.Fld28Balance_) AS NUMERIC(33, 2)) AS Fld28Balance_
FROM (
-- Получение данных из текущих итогов
SELECT
T3._Fld27RRef AS Fld27RRef,
CAST(SUM(T3._Fld28) AS NUMERIC(27, 2)) AS Fld28Balance_
FROM dbo._AccumRgT29 T3
-- Фильтр накладывается только по дате текущих итогов (3999-11-01 00:00:00 - дата конца света по календарю 1С)
WHERE T3._Period = @P1 AND ((T3._Fld27RRef = @P2)) AND (T3._Fld28 <> @P3) AND (T3._Fld28 <> @P4)
GROUP BY T3._Fld27RRef
HAVING (CAST(SUM(T3._Fld28) AS NUMERIC(27, 2))) <> 0.0
UNION ALL
-- Получение данных из основной таблицы движений (без использования итогов)
SELECT
T4._Fld27RRef AS Fld27RRef,
CAST(CAST(SUM(CASE WHEN T4._RecordKind = 0.0 THEN -T4._Fld28 ELSE T4._Fld28 END) AS NUMERIC(21, 2)) AS NUMERIC(27, 2)) AS Fld28Balance_
FROM dbo._AccumRg26 T4
-- Фильтр накладывается по периоду, с учетом переданного параметра в запрос,
-- а также добавилось условие, что ссылка на регистратор больше ссылки, которая находилась в переданном моменте времени
WHERE (T4._Period > @P5 OR T4._Period = @P6
AND T4._RecorderRRef > @P7) -- Вот тут!!! Сравнение теперь "Больше", вместо "Больше или равно"
AND T4._Period < @P8 AND T4._Active = 0x01 AND ((T4._Fld27RRef = @P9))
-- В этом случае параметр @P5 указывает с какой даты получать движения, для которых еще нет итогов
-- Параметр @P6 - это дата из момента времени
-- А параметр @P8 - это дата, на которую хранятся текущие итоги
GROUP BY T4._Fld27RRef
HAVING (CAST(CAST(SUM(CASE WHEN T4._RecordKind = 0.0 THEN -T4._Fld28 ELSE T4._Fld28 END) AS NUMERIC(21, 2)) AS NUMERIC(27, 2))) <> 0.0
) T2
GROUP BY T2.Fld27RRef
HAVING (CAST(SUM(T2.Fld28Balance_) AS NUMERIC(33, 2))) <> 0.0
) T1
Одно маленькое различие для текста запроса, но большое различие для логики работы запроса! Сравнение с ссылкой из момента времени теперь выполняется по условию "Больше", а не "Больше или равно" как это было ранее.
Такое условие позволяет получить данные из текущих итогов, при этом исключить запись из таблицы движений для документа из момента времени. Да, да! Вид границы "Включая" исключает запись из таблицы движений и учитывает только запись в итогах.
Варианты SQL-запросов могут изменяться в зависимости от настроек итогов и структуры регистра, а также дополнительных фильтров, но логика работы с моментом времени и границей сохраняются.
Также стоит отметить, что потенциальные проблемы с производительностью здесь такие же, как и при использовании момента времени, т.к. условие "ИЛИ" также присутствует и индекс будет использоваться неэффективно.
Теперь Вы знаете что такое момент времени, граница и как это используется платформой. Кстати, из всех примеров выше только один вернет результат для текущего документа - это последний пример с границей вида "Включая". Вы же понимаете почему?
К сожалению, есть еще один момент, который стоит упомянуть о моменте времени (извините за тавтологию). Сравнение ссылок это хорошо, но оно не гарантирует всегда ожидаемый результат. Момент времени будет работать, если исходить из того, что ссылки генерируются последовательно, но это не всегда так!
Порядок ссылок в базе может быть случайным, если:
- У вас используется УРБД, ведь ссылки в разных базах и на разных серверах, в разных часовых поясах могут генерироваться не так как Вы ожидаете. В итоге - документы в единой базе будут также иметь последовательность на временной оси, которая не совсем может подходить для решения учетных задач.
- Если это таблица, объединяющая несколько типов метаданных (журналы документов, последовательность и т.д.), ведь для разных типов идентификаторы объектов могут сильно отличаться.
- При использовании явной установки ссылки для объектов также не гарантируется последовательность генерации GUID'ов. Речь идет о таком методе явной установки ссылки.
ИдентификаторСсылка = Новый УникальныйИдентификатор;
НоваяСсылка = Документы.ПростойДокумент.ПолучитьСсылку(ИдентификаторСсылка);
НовыйОбъект = Документы.ПростойДокумент.СоздатьДокумент();
НовыйОбъект.УстановитьСсылкуНового(НоваяСсылка);
Таким образом, момент времени и граница вроде бы подходят для "ювелирной" работы с объектами на оси времени, а с другой стороны результат их работы и не гарантируется в некоторых ситуациях. Тут можно дать лишь один совет - анализировать ситуацию с созданием ссылок и обменами, оценить потенциальные проблемы с использованием этих механизмов.
Ранее на ИС уже поднимались темы, связанные с моментом времени. Вот ссылки:
Спасибо их авторам за хороший материал!
Бегите, глупцы!
В статье мы рассмотрели многие вопросы по работе с датой и временем в платформе 1С, начиная от основ и заканчивая тонкостями работы самой платформы с датами и временем, часовыми поясами и особенностями хранения даты в базе данных.
Надеюсь, эта информация будет кому-то да полезна. Кого-то заставит переосмыслить использование функции "ТекущаяДата()" и момента времени. Кто-то перестанет бояться часовых поясов и/или обратит внимание на настройки часовых поясов в сопровождаемой информационной базе.
Если у Вас есть интересные кейсы по работе с часовыми поясами в платформе или по другим затронутым темам, то прошу в комментарии. Это очень интересное направление при разработке, жаль что не сильно востребованное.
Другие ссылки