Работа с регистрами сведений — одна из самых частых задач при разработке в системе 1С:Предприятие. Для записи данных программисты используют два основных инструмента: МенеджерЗаписи (для работы с одной строкой) и НаборЗаписей (для массовой работы с данными). Метод СоздатьНаборЗаписей() предоставляет огромные возможности по оптимизации записи, однако его некорректное использование может привести к фатальным последствиям, вплоть до полной потери данных в таблице базы данных.
В этой статье мы подробно разберем, как устроен механизм записи набора, выясним причины критических ошибок при записи в цикле, разберем поведение фильтров (отборов) и рассмотрим современные высокопроизводительные режимы замещения, появившиеся в последних версиях платформы 1С.
Давайте проанализируем, что происходит на уровне СУБД, когда мы вызываем метод Записать() у объекта набора записей. По умолчанию второй параметр метода Замещать имеет значение Истина. Это означает, что платформа перед физической записью новых строк пытается очистить старые данные.
Здесь кроется главная опасность для начинающих разработчиков. Платформа определяет область удаления (замещения) строго на основании установленного в наборе отбора:
DELETE FROM Регистр WHERE Отбор, удаляя только записи, соответствующие фильтру. Затем записываются новые строки из набора.TRUNCATE или полного DELETE без условий), после чего записывает в базу только те несколько строк, которые вы добавили в объект в оперативной памяти. Все остальные исторические данные регистра будут безвозвратно утеряны.Рассмотрим пример опасного кода:
// КРИТИЧЕСКАЯ ОШИБКА: Этот код удалит все данные из регистра!
НаборЗаписей = РегистрыСведений.ЦеныНоменклатуры.СоздатьНаборЗаписей();
НоваяЗапись = НаборЗаписей.Добавить();
НоваяЗапись.Номенклатура = Справочники.Номенклатура.НайтиПоКоду("001");
НоваяЗапись.Цена = 100;
НаборЗаписей.Записать(Истина); // Будет записана 1 строка, все остальные данные регистра СТЕРТЫ
Чтобы избежать этой проблемы, всегда четко определяйте область действия набора записей с помощью свойства Отбор. Если вам нужно дописать новые данные без удаления старых, используйте параметр замещения в значении Ложь (хотя этот подход имеет свои ограничения, которые мы разберем далее).
Посмотрим на ситуацию, с которой столкнулся автор темы на форуме. Он пытался обойти выборку запроса в цикле и записывать данные порциями. Рассмотрим классический ошибочный вариант кода, при котором метод Записать() вызывается на каждой итерации цикла:
// НЕПРАВИЛЬНО: Вызов записи внутри цикла
Выборка = Запрос.Выполнить().Выбрать();
Пока Выборка.Следующий() Цикл
Набор = РегистрыСведений.МойРегистр.СоздатьНаборЗаписей();
Набор.Отбор.КлючСвязи.Установить(Выборка.КлючСвязи);
НоваяСтрока = Набор.Добавить();
ЗаполнитьЗначенияСвойств(НоваяСтрока, Выборка);
Набор.Записать(); // Ошибка производительности! Вызов СУБД на каждом шаге
КонецЦикла;
Почему этот код считается антипаттерном в 1С? Выясним ключевые причины:
Записать() открывает отдельную неявную транзакцию в СУБД. Это создает колоссальную нагрузку на дисковую подсистему и процессор сервера БД.ПередЗаписью, ПриЗаписи) и все зарегистрированные в системе подписки на события.Правильным решением является подготовка всего объема данных в оперативной памяти и выполнение однократной записи вне цикла. Разберем, как выглядит оптимизированный алгоритм:
// ПРАВИЛЬНО: Накопление данных и одна транзакция записи
Набор = РегистрыСведений.МойРегистр.СоздатьНаборЗаписей();
// Если нужно перезаписать только определенный сегмент данных, ставим общий отбор:
// Набор.Отбор.Организация.Установить(ПараметрОрганизация);
Выборка = Запрос.Выполнить().Выбрать();
Пока Выборка.Следующий() Цикл
НоваяСтрока = Набор.Добавить();
ЗаполнитьЗначенияСвойств(НоваяСтрока, Выборка);
КонецЦикла;
// Записываем весь пакет данных одной транзакцией
Набор.Записать(Истина);
Если объем данных в выборке исчисляется десятками или сотнями тысяч строк, единовременная запись может переполнить лог транзакций СУБД. В таких случаях мы рекомендуем разбивать запись на пакеты (порции) по 1 000 – 5 000 записей в одной транзакции.
Если по логике задачи вам все же необходимо производить запись порциями в цикле, используя один и тот же объект НаборЗаписей, крайне важно управлять его содержимым в памяти.
Объект типа РегистрСведенийНаборЗаписей ведет себя как внутренняя коллекция строк (таблица). Когда вы вызываете метод Добавить(), строка помещается в память объекта. Если на следующей итерации цикла вы не очистите набор, новые строки просто добавятся к строкам из предыдущей итерации.
При вызове метода Записать() система попытается заново отправить в базу данных и старые, уже записанные строки, и новые. Это приведет либо к бессмысленному дублированию сетевого трафика и операций записи, либо к ошибкам нарушения уникальности ключевых полей в СУБД.
Для предотвращения этой ситуации используйте метод Очистить():
Набор = РегистрыСведений.МойРегистр.СоздатьНаборЗаписей();
ВыборкаГрупп = Справочники.Группы.Выбрать();
Пока ВыборкаГрупп.Следующий() Цикл
// Очищаем данные предыдущей итерации из памяти объекта
Набор.Очистить();
Набор.Отбор.Группа.Установить(ВыборкаГрупп.Ссылка);
// Заполняем новые данные для конкретной группы
ВыборкаЭлементов = ПолучитьЭлементыГруппы(ВыборкаГрупп.Ссылка);
Для Каждого СтрокаДанных Из ВыборкаЭлементов Цикл
НоваяЗапись = Набор.Добавить();
ЗаполнитьЗначенияСвойств(НоваяЗапись, СтрокаДанных);
КонецЦикла;
Набор.Записать(Истина); // Перезапишет данные только по текущей группе
КонецЦикла;
Альтернативный вариант — переинициализировать переменную набора в теле цикла (писать Набор = РегистрыСведений.МойРегистр.СоздатьНаборЗаписей(); внутри цикла). Однако использование метода Очистить() для уже существующего объекта в памяти работает быстрее и эффективнее с точки зрения сборщика мусора 1С.
В современных версиях платформы 1С:Предприятие (начиная с 8.3.25, 8.3.26 и 8.3.27) работа с наборами записей получила революционное развитие. Теперь вместо простой передачи булева значения (Истина / Ложь) в параметр Замещать метода Записать(), разработчики могут использовать специальное системное перечисление РежимЗамещения.
Посмотрим на новые интеллектуальные режимы, которые кардинально упрощают разработку:
РежимЗамещения.Замещение — стандартное классическое поведение (аналог Истина). Полное удаление старых записей по отбору и запись новых.РежимЗамещения.Добавление — стандартное поведение добавления (аналог Ложь). Записи просто дописываются в базу. Если ключ уникальности совпадает, транзакция падает с ошибкой.РежимЗамещения.Слияние (доступен для независимых регистров сведений с версии 8.3.25) — интеллектуальный режим "Upsert". Система анализирует ключевые поля (измерения + период). Если запись с такими ключевыми полями уже существует в БД, платформа обновляет ее ресурсы и реквизиты. Если такой записи нет — она создается. Это полностью исключает необходимость предварительного чтения базы данных для проверки существования строки.РежимЗамещения.Удаление (доступен с версии 8.3.25) — позволяет выполнять массовое удаление определенных строк. Вам достаточно поместить в набор только ключевые поля (измерения) удаляемых записей и вызвать метод записи с этим режимом. Работает значительно быстрее построчного удаления через менеджеры записи.РежимЗамещения.Обновление (доступен с версии 8.3.27) — обновляет значения ресурсов и реквизитов только у тех записей, ключи которых уже присутствуют в базе данных. Новые уникальные ключи при этом игнорируются и в регистр не добавляются.Проанализируем пример использования режима слияния в современном коде:
Набор = РегистрыСведений.ЦеныНоменклатуры.СоздатьНаборЗаписей();
НоваяСтрока = Набор.Добавить();
НоваяСтрока.Номенклатура = СсылкаНоменклатура;
НоваяСтрока.Цена = 150;
// Платформа сама разберется: обновить цену у существующей позиции или добавить новую строку
Набор.Записать(РежимЗамещения.Слияние);
Начиная с версии платформы 8.3.26, эти инновационные режимы записи перестали быть привилегией исключительно независимых регистров сведений. Теперь их можно применять также для подчиненных регистратору регистров сведений, регистров накопления, регистров бухгалтерии и расчета.
Для того чтобы задействовать расширенные режимы замещения в подчиненных регистрах, у объекта набора записей необходимо предварительно взвести специальное свойство:
НаборДвижений = Движения.ОстаткиТоваров; // Регистр накопления
НаборДвижений.РасширенныеРежимыЗамещения = Истина;
// Теперь мы можем выполнять частичное обновление или слияние движений документа!
НаборДвижений.Записать(РежимЗамещения.Слияние);
Это нововведение позволяет значительно поднять производительность систем при проведении "тяжелых" документов или при оптимизации процедур закрытия месяца, избавляя сервер от необходимости полной перезаписи огромных таблиц движений.
Используйте эти правила в своей ежедневной практике разработки, контролируйте наложение отборов и всегда отдавайте предпочтение пакетной записи данных вместо выполнения операций в циклах. Это гарантирует надежность и высокую скорость работы ваших прикладных решений.