При интеграции 1С с внешними системами часто возникает задача загрузки или обновления информации в справочниках из файлов формата DBF. Типичный сценарий — обновление наименований или других реквизитов уже существующих элементов, где в качестве ключа связи (синхронизации) выступает уникальный код элемента. На первый взгляд задача кажется простой, но при ее реализации разработчики часто сталкиваются с целым рядом неочевидных проблем: от ошибок блокировки файлов операционной системой до фатального падения производительности СУБД. В этой статье мы подробно разберем, как правильно реализовать загрузку данных из DBF, исправим частые ошибки новичков и представим оптимизированный шаблон кода.
Для начала проанализируем основные проблемы, с которыми сталкиваются разработчики при написании кода загрузки из DBF, на примере реального обсуждения с форума.
1. Попытка записать строковое значение в ссылочное поле (например, "Владелец")
При чтении данных из файла DBF с помощью встроенного объекта 1С XBase все значения полей считываются как простые типы данных (строка, число, дата или булево). Если в структуре вашего справочника есть реквизит со ссылочным типом данных (например, СправочникСсылка.Контрагенты или стандартное поле Владелец), вы не можете напрямую присвоить ему строку, считанную из файла:
// ОШИБОЧНЫЙ КОД
НовыйДокумент.Владелец = СокрЛП(База.POD); // База.POD возвращает строку!
Попытка записи такого элемента вызовет исключение: "Значение поля 'Владелец' не заполнено или заполнено неверно!". Платформа ожидает получить ссылку на элемент другого справочника, а получает обычную строку. Чтобы решить эту проблему, необходимо сначала найти элемент справочника-владельца по его коду или наименованию, и только затем присвоить полученную ссылку полю:
КодВладельца = СокрЛП(База.POD);
СсылкаНаВладельца = Справочники.Контрагенты.НайтиПоКоду(КодВладельца);
Если Не СсылкаНаВладельца.Пустая() Тогда
НовыйДокумент.Владелец = СсылкаНаВладельца;
Иначе
// Обработка ситуации, когда владелец не найден
КонецЕсли;
2. Путаница между Ссылкой и Объектом справочника
Для чтения информации об элементе справочника достаточно использовать СправочникСсылка, которую возвращают методы поиска (например, НайтиПоКоду() или НайтиПоНаименованию()). Однако ссылка предназначена только для чтения. Если вы попытаетесь изменить ее реквизиты напрямую, система выдаст ошибку. Для изменения и перезаписи существующих данных обязательно нужно получить объектную сущность элемента (СправочникОбъект) с помощью метода ПолучитьОбъект():
// Находим ссылку на элемент по коду из DBF-файла
СсылкаНаЭлемент = Справочники.ЛицевыеСчета.НайтиПоКоду(КодИзФайла);
Если Не СсылкаНаЭлемент.Пустая() Тогда
// Получаем объект для редактирования
ОбъектСправочника = СсылкаНаЭлемент.ПолучитьОбъект();
ОбъектСправочника.Наименование = СокрЛП(База.FIO);
ОбъектСправочника.Записать();
КонецЕсли;
3. Нарушение порядка работы с файловой системой (Блокировка файлов)
Еще одна частая ошибка — попытка удалить временный файл DBF до того, как объект XBase завершил работу с ним и закрыл его в операционной системе. Рассмотрим фрагмент кода:
// ОШИБОЧНЫЙ ПОРЯДОК
УдалитьФайлы(ВрФайл); // Попытка удаления занятого файла
База.ЗакрытьФайл(); // Закрытие файла в 1С
Такой код приведет к ошибке совместного доступа на уровне операционной системы, так как файл физически заблокирован процессом 1С. Метод ЗакрытьФайл() всегда должен вызываться строго до удаления файла с диска.
Также важно следить за чистотой временных папок. Если в процессе работы вы копируете исходный файл во временный каталог под новым именем, не забывайте удалять все созданные копии файлов по завершении операции, чтобы не захламлять диск сервера или клиентского компьютера.
Перейдем от теории к практике. Напишем надежный алгоритм загрузки данных. Мы будем использовать встроенный объект XBase для работы с DBF-файлом, организуем правильную проверку существующих элементов, корректное обновление реквизитов и безопасную работу с файловой системой через блок Попытка ... Исключение.
Ниже представлен базовый вариант реализации, который отлично подойдет для небольших файлов (до нескольких тысяч строк):
&НаСервере
Процедура ЗагрузитьДанныеИзDBFНаСервере(АдресХранилища)
// Получаем бинарные данные файла из временного хранилища
ДвоичныеДанныеФайла = ПолучитьИзВременногоХранилища(АдресХранилища);
// Создаем уникальные имена для временных файлов
ИмяВрФайла = ПолучитьИмяВременногоФайла("dbf");
ДвоичныеДанныеФайла.Записать(ИмяВрФайла);
// Инициализируем объект для работы с DBF
БазаDBF = Новый XBase;
БазаDBF.Кодировка = КодировкаXBase.OEM; // Обычно используется DOS-кодировка (OEM)
Попытка
БазаDBF.ОткрытьФайл(ИмяВрФайла);
Исключение
ВызватьИсключение "Не удалось открыть DBF-файл! Ошибка: " + ОписаниеОшибки();
КонецПопытки;
Если Не БазаDBF.Открыта() Тогда
Возврат;
КонецЕсли;
// Отключаем отображение удаленных записей в DBF
БазаDBF.OтображатьУдаленные = Ложь;
ВсегоЗаписей = БазаDBF.КоличествоЗаписей();
// Начинаем транзакцию для ускорения процесса записи в базу данных
НачатьТранзакцию();
Попытка
Для НомерСтроки = 1 По ВсегоЗаписей Цикл
БазаDBF.Перейти(НомерСтроки);
// Считываем код и новое наименование из текущей строки файла
ТекущийКод = СокрЛП(БазаDBF.LS);
НовоеНаименование = СокрЛП(БазаDBF.FIO);
// Ищем элемент в справочнике по коду
СсылкаНаЭлемент = Справочники.ЛицевыеСчета.НайтиПоКоду(ТекущийКод);
Если Не СсылкаНаЭлемент.Пустая() Тогда
// Элемент найден — обновляем его наименование
ОбъектСправочника = СсылкаНаЭлемент.ПолучитьОбъект();
ОбъектСправочника.Наименование = НовоеНаименование;
Попытка
ОбъектСправочника.Записать();
Исключение
ЗаписьЖурналаРегистрации("Загрузка из DBF",
УровеньЖурналаРегистрации.Предупреждение,,,,,
"Не удалось записать элемент с кодом: " + ТекущийКод + ". Ошибка: " + ОписаниеОшибки());
КонецПопытки;
Иначе
// Элемент не найден — при необходимости здесь можно реализовать его создание
// НовыйЭлемент = Справочники.ЛицевыеСчета.СоздатьЭлемент();
// НовыйЭлемент.Код = ТекущийКод;
// НовыйЭлемент.Наименование = НовоеНаименование;
// НовыйЭлемент.Записать();
КонецЕсли;
КонецЦикла;
// Фиксируем транзакцию, если все записи прошли успешно
ЗафиксироватьТранзакцию();
Сообщить("Загрузка успешно завершена. Обработано записей: " + ВсегоЗаписей);
Исключение
// В случае критической ошибки откатываем все изменения
ОтменитьТранзакцию();
Сообщить("Произошла ошибка при обработке данных. Изменения отменены! Описание: " + ОписаниеОшибки());
КонецПопытки;
// Обязательно закрываем файл перед его удалением!
БазаDBF.ЗакрытьФайл();
// Очищаем временные файлы с диска
Попытка
УдалитьФайлы(ИмяВрФайла);
Исключение
// Игнорируем ошибку удаления, если файл заблокирован системой
КонецПопытки;
КонецПроцедуры
Представленный выше алгоритм отлично работает на малых объемах данных. Однако, если ваш файл DBF содержит десятки тысяч строк, скорость загрузки резко упадет. Причиной тому — вызов метода НайтиПоКоду() внутри цикла. Каждый такой вызов провоцирует отдельный запрос СУБД к базе данных 1С. Поиск 10 000 элементов обернется 10 000 индивидуальными обращениями к серверу базы данных, что парализует работу системы.
Для оптимизации этого процесса применим правильный архитектурный подход:
Соответствие, где ключом будет выступать строковый Код, а значением — полученная Ссылка.Получить() объекта Соответствие, полностью минуя повторные обращения к СУБД.Рассмотрим, как реализовать этот высокопроизводительный метод:
&НаСервере
Процедура ОптимизированнаяЗагрузкаИзDBF(АдресХранилища)
ДвоичныеДанныеФайла = ПолучитьИзВременногоХранилища(АдресХранилища);
ИмяВрФайла = ПолучитьИмяВременногоФайла("dbf");
ДвоичныеДанныеФайла.Записать(ИмяВрФайла);
БазаDBF = Новый XBase;
БазаDBF.Кодировка = КодировкаXBase.OEM;
Если Не БазаDBF.ОткрытьФайл(ИмяВрФайла) Тогда
Возврат;
КонецЕсли;
БазаDBF.ОтображатьУдаленные = Ложь;
ВсегоЗаписей = БазаDBF.КоличествоЗаписей();
// Шаг 1. Собираем уникальные коды из DBF во временную таблицу значений
ТаблицаКодов = Новый ТаблицаЗначений;
ТаблицаКодов.Колонки.Добавить("Код", Новый ОписаниеТипов("Строка", Новый КвалификаторыСтроки(10)));
ТаблицаКодов.Колонки.Добавить("Наименование", Новый ОписаниеТипов("Строка", Новый КвалификаторыСтроки(150)));
Для НомерСтроки = 1 По ВсегоЗаписей Цикл
БазаDBF.Перейти(НомерСтроки);
НоваяСтрока = ТаблицаКодов.Добавить();
НоваяСтрока.Код = СокрЛП(БазаDBF.LS);
НоваяСтрока.Наименование = СокрЛП(БазаDBF.FIO);
КонецЦикла;
// Закрываем файл DBF, так как все данные уже перенесены в память 1С
БазаDBF.ЗакрытьФайл();
УдалитьФайлы(ИмяВрФайла);
// Шаг 2. Пакетный поиск существующих элементов через один запрос
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| ЛицевыеСчета.Ссылка КАК Ссылка,
| ЛицевыеСчета.Код КАК Код
|ИЗ
| Справочник.ЛицевыеСчета КАК ЛицевыеСчета
|ГДЕ
| ЛицевыеСчета.Код В(&МассивКодов)";
// Получаем массив уникальных кодов из таблицы значений
МассивКодов = ТаблицаКодов.ВыгрузитьКолонку("Код");
Запрос.УстановитьПараметр("МассивКодов", МассивКодов);
РезультатЗапроса = Запрос.Выполнить();
Выборка = РезультатЗапроса.Выбрать();
// Шаг 3. Заполняем Соответствие для сверхбыстрого сопоставления в ОЗУ
СоответствиеКодовИСсылок = Новый Соответствие;
Пока Выборка.Следующий() Цикл
СоответствиеКодовИСсылок.Вставить(СокрЛП(Выборка.Код), Выборка.Ссылка);
КонецЦикла;
// Шаг 4. Обновление элементов в единой транзакции
НачатьТранзакцию();
Попытка
Для Каждого СтрокаДанных Из ТаблицаКодов Цикл
// Ищем ссылку в соответствии в памяти без запросов к БД
СсылкаНаЭлемент = СоответствиеКодовИСсылок.Получить(СтрокаДанных.Код);
Если СсылкаНаЭлемент <> Неопределено Тогда
// Элемент найден — перезаписываем реквизит
ОбъектСправочника = СсылкаНаЭлемент.ПолучитьОбъект();
// Проверяем, изменилось ли наименование, чтобы избежать лишней перезаписи
Если ОбъектСправочника.Наименование <> СтрокаДанных.Наименование Тогда
ОбъектСправочника.Наименование = СтрокаДанных.Наименование;
ОбъектСправочника.Записать();
КонецЕсли;
КонецЕсли;
КонецЦикла;
ЗафиксироватьТранзакцию();
Сообщить("Данные успешно обновлены!");
Исключение
ОтменитьТранзакцию();
Сообщить("Ошибка при обновлении справочника: " + ОписаниеОшибки());
КонецПопытки;
КонецПроцедуры
Мы спроектировали оптимальный и безопасный алгоритм работы с DBF-файлами. Применяя на практике правила разделения контекстов ссылок и объектов, контролируя корректность типов данных (избегая записи строк вместо ссылок) и используя пакетные запросы вместе с транзакциями, вы сможете добиться высочайшей скорости и надежности работы ваших обработок по импорту данных в системе 1С:Предприятие.