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

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

При работе с большими объемами данных, например, 25 000 элементов, регулярная полная перезапись регистра сведений из внешнего XML-файла может быть неэффективной и занимать много времени. Возникает задача: как загружать только те данные, которые действительно изменились, добавились или были удалены из источника? Это позволит существенно сократить время выполнения операции и уменьшить нагрузку на базу данных.

Разберем методы, которые помогут нам реализовать "дельту" загрузки, то есть обрабатывать только различия между данными в XML-файле и существующими записями в регистре сведений.

Определение критериев изменения данных

Прежде чем приступать к загрузке, нам необходимо четко определить, что именно мы считаем "измененными" данными. Это критически важный шаг, так как от него зависит вся логика сравнения.

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

  2. Поля для отслеживания изменений: После того как мы определили уникальный ключ, нам нужно понять, какие другие поля в записи могут меняться. Если для существующей записи с тем же ключом (НомерДоговора, НомерОбъекта, КодСкана) изменились какие-либо другие реквизиты, например, ОписаниеСкана, ДатаЗагрузкиСкана и т.д., то такую запись мы будем считать "измененной" и требующей обновления.

Таким образом, мы будем иметь три категории изменений:

Основной подход: Использование временных таблиц для сравнения и дельты

Одним из наиболее эффективных и гибких способов реализации дельта-загрузки является использование временных таблиц 1С. Этот метод позволяет нам работать с данными из XML-файла как с полноценной таблицей базы данных, применяя мощь языка запросов 1С для сравнения.

Разберем процесс по шагам:

  1. Загрузка данных из XML во временную таблицу (ВТ).

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

    
    // Пример создания временной таблицы и ее заполнения (псевдокод)
    МенеджерВременныхТаблиц = Новый МенеджерВременныхТаблиц();
    
    Запрос = Новый Запрос();
    Запрос.МенеджерВременныхТаблиц = МенеджерВременныхТаблиц;
    
    // Определяем структуру временной таблицы, которая будет хранить данные из XML
    // и добавляем ее в МенеджерВременныхТаблиц
    Запрос.Текст = 
    "ВЫБРАТЬ
        |    &НомерДоговора КАК НомерДоговора,
        |    &НомерОбъекта КАК НомерОбъекта,
        |    &КодСкана КАК КодСкана,
        |    &ОписаниеСкана КАК ОписаниеСкана,
        |    &ДатаЗагрузкиСкана КАК ДатаЗагрузкиСкана
        |ПОМЕСТИТЬ ВТ_ДанныеИзXML"; // Эта часть запроса выполнится несколько раз для каждой записи из XML
    
    // Создаем временную таблицу, но пока без данных
    Запрос.Выполнить(); 
    
    // Итерация по XML-файлу и заполнение ВТ
    // (Предполагаем, что у нас есть функция для чтения XML и получения данных)
    ДанныеИзXML = ПрочитатьДанныеИзXML(ПутьКФайлуXML); 
    
    Для Каждого СтрокаXML Из ДанныеИзXML Цикл
        Запрос.УстановитьПараметр("НомерДоговора", СтрокаXML.НомерДоговора);
        Запрос.УстановитьПараметр("НомерОбъекта", СтрокаXML.НомерОбъекта);
        Запрос.УстановитьПараметр("КодСкана", СтрокаXML.КодСкана);
        Запрос.УстановитьПараметр("ОписаниеСкана", СтрокаXML.ОписаниеСкана);
        Запрос.УстановитьПараметр("ДатаЗагрузкиСкана", СтрокаXML.ДатаЗагрузкиСкана);
        Запрос.Текст = 
        "ВСТАВИТЬ В ВТ_ДанныеИзXML
        |(НомерДоговора, НомерОбъекта, КодСкана, ОписаниеСкана, ДатаЗагрузкиСкана)
        |ЗНАЧЕНИЯ
        |(
        |    &НомерДоговора,
        |    &НомерОбъекта,
        |    &КодСкана,
        |    &ОписаниеСкана,
        |    &ДатаЗагрузкиСкана
        |)";
        Запрос.Выполнить(); // Каждая итерация добавляет строку во ВТ
    КонецЦикла;
    

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

    
    // Пример заполнения временной таблицы из ТаблицыЗначений
    МенеджерВременныхТаблиц = Новый МенеджерВременныхТаблиц();
    ТаблицаДанныхИзXML = ПрочитатьДанныеИзXMLВТаблицуЗначений(ПутьКФайлуXML);
    
    Запрос = Новый Запрос();
    Запрос.МенеджерВременныхТаблиц = МенеджерВременныхТаблиц;
    Запрос.УстановитьПараметр("ТаблицаДанныхИзXML", ТаблицаДанныхИзXML);
    
    Запрос.Текст = 
    "ВЫБРАТЬ
    |    Данные.НомерДоговора,
    |    Данные.НомерОбъекта,
    |    Данные.КодСкана,
    |    Данные.ОписаниеСкана,
    |    Данные.ДатаЗагрузкиСкана
    |ПОМЕСТИТЬ ВТ_ДанныеИзXML
    |ИЗ
    |    &ТаблицаДанныхИзXML КАК Данные";
    
    Запрос.Выполнить();
    
  2. Сравнение ВТ с существующими данными регистра сведений.

    Теперь, когда у нас есть данные из XML во временной таблице, мы можем выполнить запросы для определения всех трех категорий изменений.

  3. Определение "новых" элементов для добавления.

    Чтобы найти новые записи, мы должны выбрать все записи из временной таблицы, для которых не найдется соответствующей записи в регистре сведений по нашему составному ключу (НомерДоговора, НомерОбъекта, КодСкана). Мы можем использовать оператор ЛЕВОЕ СОЕДИНЕНИЕ с проверкой на NULL.

    
    Запрос = Новый Запрос();
    Запрос.МенеджерВременныхТаблиц = МенеджерВременныхТаблиц; // Используем тот же менеджер
    
    Запрос.Текст = 
    "ВЫБРАТЬ
    |    ВТ.НомерДоговора,
    |    ВТ.НомерОбъекта,
    |    ВТ.КодСкана,
    |    ВТ.ОписаниеСкана,
    |    ВТ.ДатаЗагрузкиСкана
    |ИЗ
    |    ВТ_ДанныеИзXML КАК ВТ
    |        ЛЕВОЕ СОЕДИНЕНИЕ РегистрСведений.НашРегистр КАК РС
    |        ПО ВТ.НомерДоговора = РС.НомерДоговора
    |        И ВТ.НомерОбъекта = РС.НомерОбъекта
    |        И ВТ.КодСкана = РС.КодСкана
    |ГДЕ
    |    РС.НомерДоговора ЕСТЬ NULL"; // Если в РС не нашлось совпадения, то это новая запись
    
    НовыеЗаписи = Запрос.Выполнить().Выгрузить();
    

    Полученные записи мы добавим в регистр сведений.

  4. Определение "измененных" элементов для обновления.

    Измененные записи — это те, у которых ключи совпадают в ВТ и регистре, но хотя бы один из остальных реквизитов отличается. Для этого мы выполняем внутреннее соединение ВТ и регистра по ключам и затем проверяем неравенство реквизитов.

    
    Запрос = Новый Запрос();
    Запрос.МенеджерВременныхТаблиц = МенеджерВременныхТаблиц;
    
    Запрос.Текст = 
    "ВЫБРАТЬ
    |    ВТ.НомерДоговора,
    |    ВТ.НомерОбъекта,
    |    ВТ.КодСкана,
    |    ВТ.ОписаниеСкана,
    |    ВТ.ДатаЗагрузкиСкана
    |ИЗ
    |    ВТ_ДанныеИзXML КАК ВТ
    |        ВНУТРЕННЕЕ СОЕДИНЕНИЕ РегистрСведений.НашРегистр КАК РС
    |        ПО ВТ.НомерДоговора = РС.НомерДоговора
    |        И ВТ.НомерОбъекта = РС.НомерОбъекта
    |        И ВТ.КодСкана = РС.КодСкана
    |ГДЕ
    |    ВТ.ОписаниеСкана <> РС.ОписаниеСкана
    |    ИЛИ ВТ.ДатаЗагрузкиСкана <> РС.ДатаЗагрузкиСкана
    |    // ... или другие реквизиты, которые могут изменяться
    |";
    
    ИзмененныеЗаписи = Запрос.Выполнить().Выгрузить();
    

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

  5. Определение "удаленных" элементов для удаления.

    Чтобы найти записи, которые были удалены из источника (XML-файла), мы должны выбрать все записи из регистра сведений, для которых не найдется соответствующей записи во временной таблице по составному ключу. Мы снова используем ЛЕВОЕ СОЕДИНЕНИЕ и проверку на NULL, но на этот раз "левой" стороной будет наш регистр.

    
    Запрос = Новый Запрос();
    Запрос.МенеджерВременныхТаблиц = МенеджерВременныхТаблиц;
    
    Запрос.Текст = 
    "ВЫБРАТЬ
    |    РС.НомерДоговора,
    |    РС.НомерОбъекта,
    |    РС.КодСкана
    |ИЗ
    |    РегистрСведений.НашРегистр КАК РС
    |        ЛЕВОЕ СОЕДИНЕНИЕ ВТ_ДанныеИзXML КАК ВТ
    |        ПО РС.НомерДоговора = ВТ.НомерДоговора
    |        И РС.НомерОбъекта = ВТ.НомерОбъекта
    |        И РС.КодСкана = ВТ.КодСкана
    |ГДЕ
    |    ВТ.НомерДоговора ЕСТЬ NULL"; // Если в ВТ не нашлось совпадения, то запись из РС должна быть удалена
    
    УдаляемыеЗаписи = Запрос.Выполнить().Выгрузить();
    

    Эти записи мы должны будем удалить из регистра сведений.

Обработка изменений порциями (батчами)

Даже если мы определили дельту, непосредственная запись или удаление 25 000 элементов по одному может быть неэффективной. Для оптимизации операций записи и удаления в регистре сведений рекомендуется использовать методы работы с наборами записей.

  1. Добавление и обновление записей:

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

    
    // Добавление новых записей
    НаборНовыхЗаписей = РегистрыСведений.НашРегистр.СоздатьНаборЗаписей();
    Для Каждого СтрокаТЗ Из НовыеЗаписи Цикл
        НоваяЗапись = НаборНовыхЗаписей.Добавить();
        НоваяЗапись.НомерДоговора = СтрокаТЗ.НомерДоговора;
        НоваяЗапись.НомерОбъекта = СтрокаТЗ.НомерОбъекта;
        НоваяЗапись.КодСкана = СтрокаТЗ.КодСкана;
        НоваяЗапись.ОписаниеСкана = СтрокаТЗ.ОписаниеСкана;
        НоваяЗапись.ДатаЗагрузкиСкана = СтрокаТЗ.ДатаЗагрузкиСкана;
    КонецЦикла;
    НаборНовыхЗаписей.Записать();
    
    // Обновление измененных записей
    Для Каждого СтрокаТЗ Из ИзмененныеЗаписи Цикл
        // Для обновления лучше использовать точечную запись или набор записей с отбором
        // Пример точечного обновления:
        ЗаписьРегистра = РегистрыСведений.НашРегистр.СоздатьМенеджерЗаписи();
        ЗаписьРегистра.НомерДоговора = СтрокаТЗ.НомерДоговора;
        ЗаписьРегистра.НомерОбъекта = СтрокаТЗ.НомерОбъекта;
        ЗаписьРегистра.КодСкана = СтрокаТЗ.КодСкана;
        ЗаписьРегистра.Прочитать(); // Читаем существующую запись по ключам
        Если ЗаписьРегистра.Найдена Тогда
            ЗаписьРегистра.ОписаниеСкана = СтрокаТЗ.ОписаниеСкана;
            ЗаписьРегистра.ДатаЗагрузкиСкана = СтрокаТЗ.ДатаЗагрузкиСкана;
            ЗаписьРегистра.Записать(); // Записываем изменения
        КонецЕсли;
    КонецЦикла;
    
    // Альтернативный, более производительный способ для обновления:
    // Сформировать набор записей с отбором по ключам всех измененных записей,
    // загрузить их в НаборЗаписей, изменить, и затем записать.
    // Или удалить старые и добавить новые (как "новые"), если обновление через НаборЗаписей затруднено.
    
  2. Удаление записей:

    Для массового удаления можно использовать метод РегистрСведенийМенеджер.Удалить(), передавая в него отбор по ключевым полям для батчевого удаления. Это значительно быстрее, чем удалять каждую запись по отдельности.

    
    // Удаление записей
    Для Каждого СтрокаТЗ Из УдаляемыеЗаписи Цикл
        Отбор = Новый Структура();
        Отбор.Вставить("НомерДоговора", СтрокаТЗ.НомерДоговора);
        Отбор.Вставить("НомерОбъекта", СтрокаТЗ.НомерОбъекта);
        Отбор.Вставить("КодСкана", СтрокаТЗ.КодСкана);
        РегистрыСведений.НашРегистр.Удалить(Отбор); // Удаляем по отбору
    КонецЦикла;
    
    // Более производительное удаление
    // Мы можем сформировать ВТ с ключами для удаления и удалить их одним запросом,
    // но прямой метод Удалить() с отбором в цикле также приемлем для не слишком огромных объемов удалений
    // Или можно удалить все записи из регистра, затем добавить только те, что остались в XML
    // (это по сути полная перезагрузка, но часто быстрее, чем дельта для очень изменчивых данных).
    

Хеширование данных для быстрого сравнения

Когда количество полей, которые могут изменяться, велико, построчное сравнение каждого реквизита в условии запроса (ВТ.ОписаниеСкана <> РС.ОписаниеСкана ИЛИ ...) становится громоздким и может снижать производительность. В таких случаях нам на помощь приходит хеширование.

  1. Дополнительное поле для хеша:

    Мы можем добавить в регистр сведений (и во временную таблицу) дополнительный строковый реквизит, например, ХешЗначенийРеквизитов. В этом поле мы будем хранить хеш-сумму всех значений реквизитов, которые мы отслеживаем на предмет изменений.

  2. Процесс сравнения с использованием хеша:

    При формировании данных из XML во временную таблицу, для каждой записи вычисляем хеш значений всех отслеживаемых реквизитов. Аналогично, при записи в регистр сведений мы сохраняем этот хеш.

    Тогда запрос для определения "измененных" записей значительно упрощается:

    
    Запрос = Новый Запрос();
    Запрос.МенеджерВременныхТаблиц = МенеджерВременныхТаблиц;
    
    Запрос.Текст = 
    "ВЫБРАТЬ
    |    ВТ.НомерДоговора,
    |    ВТ.НомерОбъекта,
    |    ВТ.КодСкана,
    |    ВТ.ОписаниеСкана,
    |    ВТ.ДатаЗагрузкиСкана
    |ИЗ
    |    ВТ_ДанныеИзXML КАК ВТ
    |        ВНУТРЕННЕЕ СОЕДИНЕНИЕ РегистрСведений.НашРегистр КАК РС
    |        ПО ВТ.НомерДоговора = РС.НомерДоговора
    |        И ВТ.НомерОбъекта = РС.НомерОбъекта
    |        И ВТ.КодСкана = РС.КодСкана
    |ГДЕ
    |    ВТ.ХешЗначенийРеквизитов <> РС.ХешЗначенийРеквизитов"; // Сравниваем только хеши
    
    ИзмененныеЗаписи = Запрос.Выполнить().Выгрузить();
    

    Это делает запрос более читаемым и, что более важно, более производительным, так как сравнение одной строковой (или числовой) хеш-суммы значительно быстрее, чем сравнение множества полей разных типов.

  3. Реализация хеширования:

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

    
    // Пример вычисления хеша (вспомогательная функция)
    Функция ПолучитьХешЗаписи(СтрокаДанных)
        СтрокаДляХеша = СтрШаблон(
            "%1|%2|%3",
            Строка(СтрокаДанных.ОписаниеСкана),
            Формат(СтрокаДанных.ДатаЗагрузкиСкана, "ДФ=yyyyMMddHHmmss"),
            // ... и так далее для всех отслеживаемых реквизитов
            "любой_другой_реквизит" // Добавьте все поля, которые могут меняться
        );
        Возврат ХешСумма(СтрокаДляХеша);
    КонецФункции;
    
    // В цикле при заполнении ВТ или при записи в регистр:
    // СтрокаXML.ХешЗначенийРеквизитов = ПолучитьХешЗаписи(СтрокаXML);
    

Универсальные механизмы обмена данными (Планы обмена)

Если ваша задача обмена данными более сложна и включает не только одностороннюю загрузку из XML, но и, возможно, выгрузку, двусторонний обмен или интеграцию с несколькими системами, вы можете рассмотреть использование встроенных механизмов 1С, таких как "Планы обмена".

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

Для нашей текущей задачи простой дельта-загрузки из XML, использование временных таблиц обычно является более легким и быстрым в реализации вариантом, чем полноценное внедрение планов обмена.

Производительность и оптимизация

Для обеспечения максимальной производительности при работе с большими объемами данных всегда обращайте внимание на следующие аспекты:

  1. Индексы: Убедитесь, что все ключевые поля регистра сведений, по которым происходит поиск, соединение и сравнение (в нашем случае это НомерДоговора, НомерОбъекта, КодСкана), проиндексированы. Это абсолютно критично для скорости выполнения запросов. Отсутствие индексов на ключевых полях превратит быстрые операции поиска в очень долгие сканирования всей таблицы.

  2. Транзакции: Группируйте операции записи/удаления в транзакции. Например, все добавления новых записей можно выполнить в одной транзакции, все обновления – в другой, все удаления – в третьей. Открытие и закрытие транзакции имеет накладные расходы, поэтому выполнение множества мелких транзакций будет медленнее, чем несколько крупных. Транзакции также обеспечивают целостность данных: если что-то пойдет не так во время операции, все изменения можно будет откатить.

    
    НачатьТранзакцию();
    Попытка
        // Операции добавления
        // Операции обновления
        // Операции удаления
        ЗафиксироватьТранзакцию();
    Исключение
        ОтменитьТранзакцию();
        ВызватьИсключение;
    КонецПопытки;
    
  3. Блокировки: При работе с большими объемами данных и возможной одновременной работой пользователей, учтите механизмы блокировок. Массовые операции записи/удаления могут вызывать длительные блокировки таблиц, мешая работе других пользователей. В некоторых случаях может потребоваться явное управление блокировками (например, с помощью объекта БлокировкаДанных) или выполнение операции в монопольном режиме для избежания конфликтов.

Альтернативные подходы: Перезагрузка или хранение вне регистра

Не всегда стоит усложнять логику дельта-загрузки. Иногда, как было замечено в обсуждении, для объемов вроде 25 000 элементов полная перезагрузка может оказаться быстрее и проще в реализации, чем сложная логика определения изменений.

  1. Полная перезагрузка:

    Если данные в регистре сведений могут быть полностью удалены и заново загружены из XML без потери критичной информации (например, если нет необходимости в истории изменений или связях с другими объектами, которые бы нарушились при удалении), мы можем использовать следующий упрощенный алгоритм:

    
    // 1. Полностью очищаем регистр сведений
    РегистрыСведений.НашРегистр.Очистить(); 
    
    // 2. Загружаем все данные из XML и добавляем их в регистр
    НаборВсехЗаписей = РегистрыСведений.НашРегистр.СоздатьНаборЗаписей();
    ДанныеИзXML = ПрочитатьДанныеИзXMLВТаблицуЗначений(ПутьКФайлуXML);
    Для Каждого СтрокаТЗ Из ДанныеИзXML Цикл
        НоваяЗапись = НаборВсехЗаписей.Добавить();
        НоваяЗапись.НомерДоговора = СтрокаТЗ.НомерДоговора;
        НоваяЗапись.НомерОбъекта = СтрокаТЗ.НомерОбъекта;
        НоваяЗапись.КодСкана = СтрокаТЗ.КодСкана;
        НоваяЗапись.ОписаниеСкана = СтрокаТЗ.ОписаниеСкана;
        НоваяЗапись.ДатаЗагрузкиСкана = СтрокаТЗ.ДатаЗагрузкиСкана;
    КонецЦикла;
    НаборВсехЗаписей.Записать();
    

    Этот подход проще, но требует, чтобы регистр сведений не имел жестких зависимостей и чтобы время на полную перезагрузку было приемлемым. Для 25 000 записей это часто является жизнеспособным вариантом.

  2. Хранение данных вне регистра:

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

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

← На главную