Как загрузить данные из Excel в 1С с обновлением существующих записей по коду?

Программист 1С v8.3 (Управляемые формы) IT и автоматизация бизнеса
← На главную

При разработке обработок по импорту данных из внешних источников (таких как файлы Excel, CSV или XML) одной из наиболее частых задач является синхронизация данных. Нам необходимо не просто загрузить новые записи, но и корректно обновить уже существующие в базе данных 1С, чтобы избежать появления дублей.

В этой статье мы подробно разберем, как решить проблему обновления данных, проанализируем типичные ошибки начинающих разработчиков, исправим некорректный код из обсуждения на форуме, а также рассмотрим передовые методы оптимизации производительности и архитектуры управляемого приложения 1С для ускорения загрузки в десятки раз.

Постановка задачи и разбор базовой ошибки

В исходном сценарии у нас есть простая таблица Excel со следующими колонками: Код, Наименование и EMail. Нам необходимо перенести эти данные в справочник Пользователи (или ФизическиеЛица).

Первоначальный вариант кода загрузки выглядел так:


Функция ЗаполнительФизЛиц(ТаблицаДанных)
    Для Каждого Строк из ТаблицаДанных Цикл
        Элем = Справочники.Пользователи.СоздатьЭлемент();
        Элем.Код = Строк.Код;
        Элем.Наименование = Строк.Наименование;
        Элем.EMail = Строк.EMail;
        Элем.Записать();
    КонецЦикла;
КонецФункции

В чем заключается проблема этого кода? Метод СоздатьЭлемент() каждый раз создает абсолютно новый элемент в базе данных. Если в настройках справочника в конфигураторе включен контроль уникальности кодов, то при попытке записать элемент с уже существующим кодом система выдаст фатальную ошибку уникальности, и загрузка прервется. Если же контроль уникальности отключен, код выполнится, но в справочнике создадутся дублирующие записи с одинаковыми кодами.

Для решения этой проблемы нам необходимо внедрить логику проверки: "Если элемент с таким кодом уже есть в базе — обновить его реквизиты, если нет — создать новый".

Шаг 1. Исправление логики поиска и работы с объектами 1С

Для поиска существующего элемента логично использовать стандартный метод менеджера справочника НайтиПоКоду(). Однако здесь разработчики часто совершают синтаксические и логические ошибки.

Разберем типичную ошибку из сообщений форума:


// ОШИБОЧНЫЙ КОД
Поиск = Справочники.Пользователи.НайтиПоКоду(Строк.Код);
Если Поиск = Неопределено тогда
    Элем = Справочники.Пользователи.СоздатьЭлемент();
    // ... заполнение ...
Иначе
    Элем = Справочники.Пользователи.ПолучитьОбъект(Поиск); // Ошибка! Метод объекта не обнаружен.
    // ...
КонецЕсли;

В данном фрагменте допущены сразу две серьезные ошибки:

  1. Неверная проверка результата поиска: Метод НайтиПоКоду() возвращает не Неопределено, а пустую ссылку соответствующего типа (например, СправочникСсылка.Пользователи), если элемент не найден. Сравнение Поиск = Неопределено вернет истину только в редких случаях, когда длина кода в справочнике равна нулю. Правильно проверять результат поиска с помощью метода Пустая().
  2. Неправильный вызов ПолучитьОбъект(): Метод ПолучитьОбъект() принадлежит самому объекту ссылки, а не менеджеру справочника. Конструкция вида Справочники.Пользователи.ПолучитьОбъект(Поиск) вызовет ошибку выполнения. Правильный синтаксис: Поиск.ПолучитьОбъект().

С учетом этих замечаний, скорректированный и лаконичный вариант алгоритма обновления выглядит следующим образом:


Процедура ЗаполнитьФизЛиц(ТаблицаДанных)
    Для Каждого СтрокаТД Из ТаблицаДанных Цикл
        
        // Ищем существующий элемент по коду
        НайденнаяСсылка = Справочники.Пользователи.НайтиПоКоду(СтрокаТД.Код);
        
        Если НайденнаяСсылка.Пустая() Тогда
            // Если элемент не найден, создаем новый
            ЭлементОбъект = Справочники.Пользователи.СоздатьЭлемент();
            ЭлементОбъект.Код = СтрокаТД.Код; // Задаем код новому элементу
        Иначе
            // Если элемент найден, получаем его объект для редактирования
            ЭлементОбъект = НайденнаяСсылка.ПолучитьОбъект();
        EndЕсли;
        
        // Заполняем реквизиты (общая часть для создания и обновления)
        ЭлементОбъект.Наименование = СтрокаТД.Наименование;
        ЭлементОбъект.EMail        = СтрокаТД.EMail;
        
        // Записываем изменения в базу данных
        Попытка
            ЭлементОбъект.Записать();
        Исключение
            ОбщегоНазначенияКлиентСервер.СообщитьПользователю(
                НСтр("ru = 'Не удалось записать пользователя с кодом '") + СтрокаТД.Код + ". " + КраткоеПредставлениеОшибки(ИнформацияОбОшибке())
            );
        КонецПопытки;
        
    КонецЦикла;
КонецПроцедуры

Этот код полностью работоспособен для небольших объемов данных. Однако, если мы загружаем файл, содержащий тысячи строк, данное решение будет работать крайне медленно. Давайте разберем, как оптимизировать этот процесс до профессионального уровня.

Шаг 2. Оптимизация производительности и уход от "Запросов в цикле"

Основным "узким местом" предыдущего кода является выполнение метода НайтиПоКоду() внутри цикла Для Каждого. Каждый вызов этого метода заставляет систему отправлять отдельный SQL-запрос к базе данных. Если в файле Excel содержится 5 000 строк, система сделает 5 000 транзакционных запросов поиска, что вызовет сильные задержки и блокировки таблиц СУБД.

Для кардинального ускорения мы применим классический паттерн оптимизации в 1С: пакетный запрос с выгрузкой в Соответствие. Мы прочитаем все коды из таблицы Excel, за один запрос выберем из базы все соответствующие ссылки и сохраним их в оперативной памяти.

Проанализируем этот подход на примере кода:


Процедура ЗаполнитьФизЛицОптимизировано(ТаблицаДанных)
    
    // 1. Собираем все коды из загруженной таблицы в массив для пакетного запроса
    МассивКодов = ТаблицаДанных.ВыгрузитьКолонку("Код");
    
    // 2. Одним запросом выбираем существующие элементы из базы данных
    Запрос = Новый Запрос;
    Запрос.Текст = 
        "ВЫБРАТЬ
        |	Пользователи.Ссылка КАК Ссылка,
        |	Пользователи.Код КАК Код
        |ИЗ
        |	Справочник.Пользователи КАК Пользователи
        |ГДЕ
        |	Пользователи.Код В (&МассивКодов)";
    
    Запрос.УстановитьПараметр("МассивКодов", МассивКодов);
    РезультатЗапроса = Запрос.Выполнить();
    
    // 3. Заполняем Соответствие (Map) для мгновенного поиска в памяти
    // Ключ - Код (строка), Значение - Ссылка на элемент справочника
    СоответствиеКодов = Новый Соответствие;
    Выборка = РезультатЗапроса.Выбрать();
    Пока Выборка.Следующий() Цикл
        СоответствиеКодов.Вставить(Выборка.Код, Выборка.Ссылка);
    КонецЦикла;
    
    // 4. Основной цикл модификации данных
    Для Каждого СтрокаТД Из ТаблицаДанных Цикл
        
        // Пытаемся получить ссылку из соответствия в памяти (без обращения к СУБД)
        НайденнаяСсылка = СоответствиеКодов.Получить(СтрокаТД.Код);
        
        Если НайденнаяСсылка = Неопределено Тогда
            // Элемента с таким кодом нет в базе, создаем новый
            ЭлементОбъект = Справочники.Пользователи.СоздатьЭлемент();
            ЭлементОбъект.Код = СтрокаТД.Код;
        Иначе
            // Элемент найден, получаем объект для редактирования
            ЭлементОбъект = НайденнаяСсылка.ПолучитьОбъект();
        КонецЕсли;
        
        // Обновляем реквизиты
        ЭлементОбъект.Наименование = СтрокаТД.Наименование;
        ЭлементОбъект.EMail        = СтрокаТД.EMail;
        
        Попытка
            ЭлементОбъект.Записать();
        Исключение
            // Логируем ошибку записи конкретного элемента, не прерывая общую загрузку
            ЗаписьЖурналаРегистрации("ЗагрузкаДанных", УровеньЖурналаРегистрации.Ошибка, 
                Метаданные.Справочники.Пользователи, , 
                "Ошибка записи кода " + СтрокаТД.Код + ": " + ПодробноеПредставлениеОшибки(ИнформацияОбОшибке()));
        КонецПопытки;
        
    КонецЦикла;
    
КонецПроцедуры

Благодаря этому подходу скорость поиска увеличивается в десятки раз, так как вместо тысяч обращений к серверу СУБД мы делаем всего один легкий запрос.

Шаг 3. Архитектура управляемых форм: Клиент-Серверное разделение

В современных конфигурациях 1С на платформе 8.3 используется интерфейс "Управляемое приложение" (Управляемые формы). В этой архитектуре действует строгое ограничение: на клиенте запрещена любая работа со ссылочными типами данных и запись в базу данных.

В исходном коде автора процедура загрузки вызывалась прямо на клиенте, что привело к ошибкам при обращении к серверным методам. Чтобы сделать код полностью соответствующим стандартам разработки 1С, нам необходимо разделить процесс на два этапа:

  1. На клиенте (&НаКлиенте): Пользователь выбирает файл Excel на своем компьютере, после чего файл передается на сервер во временное хранилище.
  2. На сервере (&НаСервере): Файл извлекается из временного хранилища, считывается, преобразуется в таблицу и записывается в базу данных.

Посмотрим, как выглядит архитектурно правильная схема передачи файла:


&НаКлиенте
Процедура Загрузить(Команда)
    
    // Настройка диалога выбора файла
    Диалог = Новый ДиалогВыбораФайла(РежимДиалогаВыбораФайла.Открытие);
    Диалог.Фильтр = "Файлы Excel (*.xls;*.xlsx)|*.xls;*.xlsx";
    Диалог.МножественныйВыбор = Ложь;
    Диалог.Заголовок = "Выберите файл для загрузки";
    
    Если Диалог.Выбрать() Тогда
        ПутьКФайлу = Диалог.ПолноеИмяФайла;
        
        // Помещаем файл в двоичном виде во временное хранилище на сервере
        ДвоичныеДанныеФайла = Новый ДвоичныеДанные(ПутьКФайлу);
        АдресХранилища = ПоместитьВоВременноеХранилище(ДвоичныеДанныеФайла, ЭтаФорма.УникальныйИдентификатор);
        
        // Вызываем серверный метод для обработки файла
        ЗагрузитьНаСервере(АдресХранилища);
        ПоказатьПредупреждение(, "Загрузка успешно завершена!");
    КонецЕсли;
    
КонецПроцедуры

&НаСервере
Процедура ЗагрузитьНаСервере(АдресХранилища)
    
    // Получаем файл из временного хранилища на сервере
    ДвоичныеДанныеФайла = ПолучитьИзВременногоХранилища(АдресХранилища);
    ИмяВременногоФайла = ПолучитьИмяВременногоФайла("xlsx");
    ДвоичныеДанныеФайла.Записать(ИмяВременногоФайла);
    
    // Вызываем чтение файла и обработку данных
    ТаблицаДанных = ПрочитатьExcelФайл(ИмяВременногоФайла);
    ЗаполнитьФизЛицОптимизировано(ТаблицаДанных);
    
    // Удаляем временный файл с сервера после обработки
    Попытка
        УдалитьФайлы(ИмяВременногоФайла);
    Исключение
        // Ошибка удаления временного файла не является критической
    КонецПопытки;
    
КонецПроцедуры

Шаг 4. Отказ от COMОбъект("Excel.Application") в пользу кроссплатформенности

В коде из обсуждения для чтения файлов Excel использовался COMОбъект("Excel.Application"). Хотя этот способ традиционен, он имеет ряд критических недостатков:

Современное и эффективное решение: использование встроенного объекта платформы 1С — ТабличныйДокумент. Начиная с версии платформы 8.3, метод Прочитать() позволяет открывать файлы форматов XLS и XLSX напрямую, без использования сторонних офисных программ. Это работает на любой ОС (включая Linux-серверы), абсолютно бесплатно и в десятки раз быстрее работы по COM.

Рассмотрим процедуру быстрого чтения файла с помощью ТабличногоДокумента:


&НаСервере
Функция ПрочитатьExcelФайл(ИмяФайла)
    
    ТабДок = Новый ТабличныйДокумент;
    
    Попытка
        // Читаем файл Excel напрямую средствами платформы 1С
        ТабДок.Прочитать(ИмяФайла, СпособЧтенияЗначенийТабличногоДокумента.Значение);
    Исключение
        ВызватьИсключение "Не удалось прочитать файл Excel. Возможно, файл поврежден или заблокирован. " + ОписаниеОшибки();
    КонецПопытки;
    
    // Создаем структуру таблицы значений для возврата данных
    ТаблицаРезультат = Новый ТаблицаЗначений;
    ТаблицаРезультат.Колонки.Добавить("Код", Новый ОписаниеТипов("Строка"));
    ТаблицаРезультат.Колонки.Добавить("Наименование", Новый ОписаниеТипов("Строка"));
    ТаблицаРезультат.Колонки.Добавить("EMail", Новый ОписаниеТипов("Строка"));
    
    // Предположим, что данные начинаются со 2-й строки (1-я строка - шапка таблицы)
    КоличествоСтрок = ТабДок.ВысотаТаблицы;
    
    Для НомерСтроки = 2 По КоличествоСтрок Цикл
        
        КодValue = СокрЛП(ТабДок.ПолучитьОбласть(НомерСтроки, 1, НомерСтроки, 1).ТекущаяОбласть.Текст);
        
        // Если код пустой - завершаем чтение (предполагаем окончание данных)
        Если ПустаяСтрока(КодValue) Тогда
            Продолжить;
        КонецЕсли;
        
        НоваяСтрока = ТаблицаРезультат.Добавить();
        НоваяСтрока.Код          = КодValue;
        НоваяСтрока.Наименование = СокрЛП(ТабДок.ПолучитьОбласть(НомерСтроки, 2, НомерСтроки, 2).ТекущаяОбласть.Текст);
        НоваяСтрока.EMail        = СокрЛП(ТабДок.ПолучитьОбласть(НомерСтроки, 3, НомерСтроки, 3).ТекущаяОбласть.Текст);
        
    КонецЦикла;
    
    Возврат ТаблицаРезультат;
    
КонецФункции

Резюме и выводы

Мы детально разобрали процесс загрузки данных с обновлением существующих записей. Подводя итог, выделим ключевые правила построения надежной интеграции:

← На главную