В процессе разработки прикладных решений на платформе 1С:Предприятие 8.3 часто возникает задача прикрепления файлов (например, фотографий сотрудников) к элементам справочников. При этом архитектурно правильным подходом считается предоставление пользователю выбора: сохранять файлы непосредственно в информационной базе (ИБ) или во внешнем каталоге на диске. Это позволяет оптимизировать размер базы данных и повысить скорость создания резервных копий.
В этой статье мы подробно разберем, почему загруженные фотографии могут «исчезать» при повторном открытии формы, как правильно управлять временем жизни файлов во временном хранилище и как спроектировать надежную систему хранения с использованием регистров сведений.
Рассмотрим классическую ошибку начинающих разработчиков при работе с временным хранилищем. В исходном коде для помещения файла в хранилище использовалась следующая конструкция:
Картинка = ПоместитьВоВременноеХранилище(Новый Картинка(Объект.ПутьКФайлу));
При таком вызове двоичные данные помещаются во временное хранилище, однако они будут автоматически удалены платформой при первом же серверном вызове (например, при переходе фокуса, изменении реквизита или выполнении любого серверного метода формы). Согласно документации 1С, если в методе ПоместитьВоВременноеХранилище не указан второй параметр (адрес или уникальный идентификатор формы), то время жизни этого значения ограничивается одним серверным вызовом.
Для того чтобы данные во временном хранилище «жили» на протяжении всей работы пользователя с формой, необходимо жестко связать их с уникальным идентификатором этой формы:
Картинка = ПоместитьВоВременноеХранилище(Новый Картинка(Объект.ПутьКФайлу), УникальныйИдентификатор);
В этом случае данные будут автоматически удалены сервером только тогда, когда пользователь закроет форму элемента.
Хранить двоичные данные большого объема (особенно изображения высокого разрешения) непосредственно в реквизитах справочника крайне не рекомендуется. Это приводит к существенному замедлению работы динамических списков, выборок и отчетов, так как SQL-сервер тратит ресурсы на чтение «тяжелых» полей.
Проанализируем оптимальные варианты организации хранения:
ФотографииСотрудников), где ключевым измерением выступает ссылка на сотрудника, а ресурсом — реквизит с типом ХранилищеЗначения. Это позволяет считывать тяжелые данные только в момент непосредственного вывода фотографии на форму.Давайте рассмотрим пошаговую реализацию переключения режимов хранения «В базе» / «На диске» с использованием регистра сведений.
Ниже представлен разбор кода, который позволяет загружать файл на клиенте и корректно переносить его в базу или на серверный диск в зависимости от выбранной настройки.
Для начала опишем клиентскую процедуру выбора файла. Мы используем асинхронные методы платформы для обеспечения стабильной работы в любых клиентах, включая веб-клиент:
&НаКлиенте
Процедура ВыбратьФотографию(Команда)
// Описываем оповещение о завершении выбора файла
Оповещение = Новый ОписаниеОповещения("ВыбратьФотографиюЗавершение", ЭтотОбъект);
// Начинаем интерактивное помещение файла во временное хранилище
НачатьПомещениеФайла(Оповещение, , , ИСТИНА, УникальныйИдентификатор);
КонецПроцедуры
&НаКлиенте
Процедура ВыбратьФотографиюЗавершение(Результат, Адрес, ВыбранноеИмяФайла, ДополнительныеПараметры) Экспорт
Если Результат Тогда
// Связываем адрес временного хранилища с реквизитом формы "Картинка"
Картинка = Адрес;
// Извлекаем расширение файла для корректного сохранения на диске в будущем
ФайлИмя = Новый Файл(ВыбранноеИмяФайла);
Объект.Расширение = ФайлИмя.Расширение;
// Устанавливаем модифицированность формы, чтобы платформа предложила сохранить изменения
Модифицированность = ИСТИНА;
КонецЕсли;
КонецПроцедуры
Теперь разберем серверную логику записи элемента. В процедуре ПередЗаписьюНаСервере нам необходимо определить, изменилось ли место хранения (например, пользователь переключил флаг «Место хранения файла» с диска в базу или наоборот). Исходя из этого, мы будем вызывать соответствующие методы:
&НаСервере
Процедура ПередЗаписьюНаСервере(Отказ, ТекущийОбъект, ПараметрыЗаписи)
// Проверяем, изменился ли способ хранения для существующего элемента
ИзменилосьМестоХранения = ЛОЖЬ;
Если НЕ ТекущийОбъект.Ссылка.Пустая() Тогда
ИзменилосьМестоХранения = (ТекущийОбъект.МестоХраненияФайла <> Объект.Ссылка.МестоХраненияФайла);
КонецЕсли;
// Выполняем сохранение в зависимости от настройки
Если ТекущийОбъект.МестоХраненияФайла = Перечисления.МестоХраненияФайла.ВБазе Тогда
СохранитьВБазе(ИзменилосьМестоХранения, Картинка, Отказ);
Иначе
СохранитьНаСервере(ИзменилосьМестоХранения, Картинка, Отказ);
КонецЕсли;
КонецПроцедуры
Рассмотрим процедуру СохранитьВБазе. В ней мы считываем файл из временного хранилища (или с диска, если способ хранения изменился), помещаем его в регистр сведений и удаляем физический файл с диска сервера, чтобы не дублировать информацию:
&НаСервере
Процедура СохранитьВБазе(ИзменилосьМестоХранения, АдресВременный, Отказ)
ДвоичныеДанные = Неопределено;
// Пытаемся получить новые двоичные данные из временного хранилища
Если ЭтоАдресВременногоХранилища(АдресВременный) Тогда
ДвоичныеДанные = ПолучитьИзВременногоХранилища(АдресВременный);
КонецЕсли;
// Если место хранения изменилось, забираем файл с диска
Если ИзменилосьМестоХранения Тогда
ПутьКФайлу = Константы.КаталогХраненияФотографий.Получить() + "\" + Объект.Ссылка.УникальныйИдентификатор + Объект.Ссылка.Расширение;
ФайлНаДиске = Новый Файл(ПутьКФайлу);
Если НЕ ФайлНаДиске.Существует() Тогда
Отказ = ИСТИНА;
Сообщить("Исходный файл на сервере отсутствует или недоступен.");
Возврат;
КонецЕсли;
Если ДвоичныеДанные = Неопределено Тогда
ДвоичныеДанные = Новый ДвоичныеДанные(ПутьКФайлу);
КонецЕсли;
// После успешного считывания удаляем ставший ненужным файл с диска
Попытка
УдалитьФайлы(ПутьКФайлу);
Исключение
// Логируем ошибку, но не прерываем транзакцию записи
ЗаписьЖурналаРегистрации("ХранениеФайлов", УровеньЖурналаРегистрации.Предупреждение, , , ОписаниеОшибки());
КонецПопытки;
КонецЕсли;
// Записываем данные в регистр сведений
Если ДвоичныеДанные <> Неопределено Тогда
МенеджерЗаписи = РегистрыСведений.ФотографииСотрудников.СоздатьМенеджерЗаписи();
МенеджерЗаписи.Сотрудник = Объект.Ссылка;
МенеджерЗаписи.Фото = Новый ХранилищеЗначения(ДвоичныеДанные);
МенеджерЗаписи.Записать();
КонецЕсли;
КонецПроцедуры
Аналогично реализуем обратную процедуру СохранитьНаСервере. Если пользователь решил хранить файл на диске, мы выгружаем двоичные данные из регистра сведений, записываем их в целевой каталог под уникальным именем (на базе УникальныйИдентификатор ссылки) и очищаем запись в регистре сведений:
&НаСервере
Процедура СохранитьНаСервере(ИзменилосьМестоХранения, АдресВременный, Отказ)
ДвоичныеДанные = Неопределено;
Если ЭтоАдресВременногоХранилища(АдресВременный) Тогда
ДвоичныеДанные = ПолучитьИзВременногоХранилища(АдресВременный);
КонецЕсли;
Если ИзменилосьМестоХранения Тогда
МенеджерЗаписи = РегистрыСведений.ФотографииСотрудников.СоздатьМенеджерЗаписи();
МенеджерЗаписи.Сотрудник = Объект.Ссылка;
Если ДвоичныеДанные = Неопределено Тогда
МенеджерЗаписи.Прочитать();
Если МенеджерЗаписи.Выбран() Тогда
ДвоичныеДанные = МенеджерЗаписи.Фото.Получить();
КонецЕсли;
КонецЕсли;
// Очищаем данные в базе
МенеджерЗаписи.Удалить();
КонецЕсли;
// Записываем двоичные данные на диск сервера
Если ДвоичныеДанные <> Неопределено Тогда
КаталогСервера = Константы.КаталогХраненияФотографий.Получить();
ПутьКФайлу = КаталогСервера + "\" + Объект.Ссылка.УникальныйИдентификатор + Объект.Ссылка.Расширение;
Попытка
ДвоичныеДанные.Записать(ПутьКФайлу);
Исключение
Отказ = ИСТИНА;
Сообщить("Не удалось сохранить файл на диске сервера: " + ОписаниеОшибки());
КонецПопытки;
КонецЕсли;
КонецПроцедуры
При создании формы на сервере нам нужно корректно инициализировать отображение картинки в зависимости от её текущего расположения:
&НаСервере
Процедура ПриСозданииНаСервере(Отказ, СтандартнаяОбработка)
Если НЕ Объект.Ссылка.Пустая() Тогда
Если Объект.МестоХраненияФайла = Перечисления.МестоХраненияФайла.ВБазе Тогда
// Формируем навигационную ссылку напрямую к ресурсу регистра сведений
ПараметрыКлюча = Новый Структура("Сотрудник", Объект.Ссылка);
КлючЗаписи = РегистрыСведений.ФотографииСотрудников.СоздатьКлючЗаписи(ПараметрыКлюча);
Картинка = ПолучитьНавигационнуюСсылку(КлючЗаписи, "Фото");
Иначе
// Читаем файл с диска сервера и помещаем во временное хранилище формы
КаталогСервера = Константы.КаталогХраненияФотографий.Получить();
ПутьКФайлу = КаталогСервера + "\" + Объект.Ссылка.УникальныйИдентификатор + Объект.Ссылка.Расширение;
ФайлНаДиске = Новый Файл(ПутьКФайлу);
Если ФайлНаДиске.Существует() Тогда
ДанныеФайла = Новый ДвоичныеДанные(ПутьКФайлу);
Картинка = ПоместитьВоВременноеХранилище(ДанныеФайла, УникальныйИдентификатор);
КонецЕсли;
EndЕсли;
КонецЕсли;
КонецПроцедуры
При выносе файлов за пределы информационной базы на уровень операционной системы часто возникают неочевидные проблемы, которые могут парализовать работу пользователей. Проанализируем критически важные аспекты:
Записать() двоичных данных на сервере) выполняется от лица системного пользователя операционной системы, под которым запущен процесс rhost (обычно это USR1CV8 на Windows или usr1cv8 на Linux). У этого пользователя обязательно должны быть полные права (чтение, запись, изменение) на целевую директорию хранения файлов.C:\Photos) использовать нельзя. Серверные вызовы могут обрабатываться на разных физических серверах кластера. Каталог хранения должен быть организован в виде общей сетевой папки с использованием UNC-путей (например, \\FileServer\Photos\).Картинка:
Попытка
ПроверочнаяКартинка = Новый Картинка(ДвоичныеДанные);
Если ПроверочнаяКартинка.Формат() = ФорматКартинки.Неизвестный Тогда
ВызватьИсключение "Загруженный файл не является валидным изображением.";
КонецЕсли;
Исключение
Отказ = ИСТИНА;
Сообщить("Ошибка валидации файла: " + ОписаниеОшибки());
КонецПопытки;
Используя предложенные методики, мы гарантируем высокую надежность хранения данных, стабильное отображение графических элементов на управляемой форме и безопасность серверной инфраструктуры 1С.