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