При реализации механизмов регулярного обмена данными или импорта из внешних систем мы часто сталкиваемся с проблемой избыточной записи объектов. Когда в базу загружается 10 000 и более документов, перезапись каждого из них (даже если данные не изменились) создает колоссальную нагрузку на сервер приложений, базу данных и дисковую подсистему. Особенно критично это становится в случаях, когда каждый документ должен быть перепроведен, что инициирует цепочку расчетов в регистрах.
В этой статье мы подробно разберем, как эффективно определить факт изменения объекта, сравним различные технические подходы и выберем оптимальные инструменты для минимизации лишних операций записи.
Проанализируем самый "ленивый", но при этом эффективный способ, заложенный в саму платформу 1С:Предприятие 8. У каждого ссылочного объекта (документа, справочника) существует скрытый реквизит ВерсияДанных. На уровне СУБД (например, MS SQL Server) это поле типа timestamp или rowversion.
Как это работает: платформа автоматически обновляет это значение при любой записи объекта в базу данных. Рассмотрим алгоритм его применения:
ВерсияДанных во внешней системе или в специальном регистре сведений.Однако у этого метода есть существенный минус для задачи импорта: если внешние данные изменились, ВерсияДанных не подскажет нам, идентичны ли пришедшие данные тем, что уже лежат в базе. Она лишь говорит о факте физической записи в прошлом.
Если нам нужно сравнить "живой" объект, созданный в памяти из внешнего файла, с тем, что уже хранится в базе, одним из самых быстрых способов является сериализация обоих объектов в двоичные данные и их побитовое сравнение. Рассмотрим пример кода, использующий формат FastInfoset (бинарный XML), который работает быстрее обычного текстового XML.
Функция СравнитьОбъекты(Объект1, Объект2)
// Объект1 - прочитанный из базы (Ссылка.ПолучитьОбъект())
// Объект2 - сформированный программно из файла загрузки
ЗаписьXML = Новый ЗаписьFastInfoset;
ЗаписьXML.УстановитьДвоичныеДанные();
ЗаписьXML.ЗаписатьОбъявлениеXML();
ЗаписатьXML(ЗаписьXML, Объект1, НазначениеТипаXML.Явное);
ДанныеДвоичные1 = ЗаписьXML.Закрыть();
ЗаписьXML = Новый ЗаписьFastInfoset;
ЗаписьXML.УстановитьДвоичныеДанные();
ЗаписьXML.ЗаписатьОбъявлениеXML();
ЗаписатьXML(ЗаписьXML, Объект2, НазначениеТипаXML.Явное);
ДанныеДвоичные2 = ЗаписьXML.Закрыть();
Возврат (ДанныеДвоичные1 = ДанныеДвоичные2);
КонецФункции
Важное примечание: при использовании этого метода следует помнить о служебных реквизитах. Если в одном объекте заполнена дата модификации или ссылка на автора, а в другом — нет, двоичные данные будут разными, даже если табличные части и основные реквизиты идентичны. Перед сравнением рекомендуется "очищать" или приводить к общему знаменателю такие поля в обоих объектах.
Если мы хотим оптимизировать не только процесс записи, но и объем передаваемых данных, логично использовать хеширование. Выясним причину популярности этого метода: вместо сравнения длинных строк или тяжелых объектов мы сравниваем короткую строку (хеш) длиной в несколько десятков символов.
Разберем этапы реализации с использованием объекта ХешированиеДанных:
Функция ПолучитьХешОбъекта(Объект)
ЗаписьJSON = Новый ЗаписьJSON;
ПараметрыЗаписиJSON = Новый ПараметрыЗаписиJSON(ПереносСтрокJSON.Нет);
ЗаписьJSON.УстановитьСтроку(ПараметрыЗаписиJSON);
СериализаторXDTO.ЗаписатьJSON(ЗаписьJSON, Объект, НазначениеТипаXML.Явное);
СтрокаJSON = ЗаписьJSON.Закрыть();
Хеширование = Новый ХешированиеДанных(АлгоритмХеширования.MD5);
Хеширование.Добавить(СтрокаJSON);
Возврат Строка(Хеширование.ХешСумма);
КонецФункции
Этот метод идеально подходит для табличных частей. Нам не нужно писать сложные циклы обхода строк — сериализатор сделает это за нас, а хеш-функция мгновенно выдаст результат проверки целостности всех данных сразу.
Рассмотрим еще один инструмент — ЗначениеВСтрокуВнутр(). Он работает крайне быстро, так как обращается к внутреннему представлению объекта в памяти. Однако будьте осторожны: этот метод чувствителен к внутренним идентификаторам. Если вы сравниваете объект, полученный из базы, с объектом, созданным методом СоздатьДокумент(), их внутренние строки могут различаться из-за разных GUID или порядка полей в памяти. Этот способ лучше всего подходит для сравнения двух состояний одного и того же экземпляра объекта в рамках одной транзакции.
Проанализируем ситуацию с производительностью при массовой проверке. Часто программисты совершают ошибку, обращаясь к реквизитам документа через точку от ссылки в цикле:
// ТАК ДЕЛАТЬ НЕЛЬЗЯ ПРИ МАССОВОЙ ЗАГРУЗКЕ
Для Каждого СсылкаИзСписка Из СписокСсылок Цикл
Если СсылкаИзСписка.СуммаДокумента <> НоваяСумма Тогда
// ... что-то делаем
КонецЕсли;
КонецЦикла;
Выясним причину проблемы: при обращении Ссылка.Реквизит платформа неявно считывает весь объект из базы данных (включая все его табличные части!). Для 10 000 документов это приведет к катастрофическому замедлению.
Правильный подход — использование запроса для получения только необходимых для сравнения полей. Мы можем одним запросом выгрузить данные всех существующих документов во временную таблицу или Соответствие, и затем проводить сравнение в оперативной памяти.
В процессе обсуждения на форумах часто всплывает вопрос о предварительной пометке на удаление. Разберем, почему это может замедлять процесс:
ОбъектыВерсионирования. Там уже реализованы механизмы сравнения объектов, учитывающие исключаемые поля (такие как дата обновления).Для максимально быстрой проверки изменений при загрузке 1С-разработчику следует придерживаться следующей стратегии:
JSON или FastInfoset с последующим вычислением хеша через ХешированиеДанных.Автор, ДатаМодификации и другие "шумные" реквизиты, которые не влияют на бизнес-логику документа.Применяя эти подходы, можно добиться сокращения времени загрузки данных в несколько раз, избегая ненужных транзакций записи и перепроведения документов.