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