Как избежать избыточных операций при обновлении записей в регистре сведений 1С:Предприятие 8.3?

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

При разработке и рефакторинге конфигураций 1С:Предприятие 8.3 программисты часто сталкиваются с различными подходами к работе с данными, в том числе с регистрами сведений. Иногда можно обнаружить код, который, на первый взгляд, выполняет нужную функцию, но при более глубоком рассмотрении оказывается неэффективным и избыточным. Давайте рассмотрим одну из таких ситуаций, когда некорректное использование объектов НаборЗаписей и МенеджерЗаписи приводит к лишним операциям с базой данных.

Проблемный код: наглядный пример

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

Вот как выглядел типичный фрагмент:


НаборЗаписей = РегистрыСведений.ЗначенияСвойствОбъектов.СоздатьНаборЗаписей();
НаборЗаписей.Отбор.Объект.Установить(ДокЗаказаПокупателя);
НаборЗаписей.Отбор.Свойство.Установить(СтатусCOF000047);
НаборЗаписей.Очистить();
НаборЗаписей.Записать();
    
Запись = РегистрыСведений.ЗначенияСвойствОбъектов.СоздатьМенеджерЗаписи();
Запись.Объект = ДокЗаказаПокупателя;
Запись.Свойство = СтатусCOF000047;
Запись.Значение = Справочники.А_СтатусыЗаказов.НайтиПоНаименованию("Возврат");
Запись.Записать();

А вот более обширный пример, демонстрирующий повторение этой логики:


СтатусCOF000050 = ПланыВидовХарактеристик.СвойстваОбъектов.НайтиПоКоду("COF000050"); //Строка 28 - [N] Новый 
    СтатусCOF000047 = ПланыВидовХарактеристик.СвойстваОбъектов.НайтиПоКоду("COF000047"); //СпрСтатус 29 - Новый
    СтатусCOF000027 = ПланыВидовХарактеристик.СвойстваОбъектов.НайтиПоКоду("COF000027"); //Строка 30 - N
    
    Если Строка(Статус) = "Новый" Тогда
        
        //28 строка
        НаборЗаписей = РегистрыСведений.ЗначенияСвойствОбъектов.СоздатьНаборЗаписей();
        НаборЗаписей.Отбор.Объект.Установить(ДокЗаказаПокупателя);
        НаборЗаписей.Отбор.Свойство.Установить(СтатусCOF000050);
        НаборЗаписей.Очистить();
        НаборЗаписей.Записать();
         Запись = РегистрыСведений.ЗначенияСвойствОбъектов.СоздатьМенеджерЗаписи();
        Запись.Объект = ДокЗаказаПокупателя;
        Запись.Свойство = СтатусCOF000050;
        Запись.Значение = "[N] Новый";
        Запись.Записать();
        
        //29 строка
        НаборЗаписей = РегистрыСведений.ЗначенияСвойствОбъектов.СоздатьНаборЗаписей();
        НаборЗаписей.Отбор.Объект.Установить(ДокЗаказаПокупателя);
        НаборЗаписей.Отбор.Свойство.Установить(СтатусCOF000047);
        НаборЗаписей.Очистить();
        НаборЗаписей.Записать();
        
        Запись = РегистрыСведений.ЗначенияСвойствОбъектов.СоздатьМенеджерЗаписи();
        Запись.Объект = ДокЗаказаПокупателя;
        Запись.Свойство = СтатусCOF000047;
        Запись.Значение = Справочники.А_СтатусыЗаказов.НайтиПоНаименованию("Новый");
        Запись.Записать();
        
        //30 строка
        НаборЗаписей = РегистрыСведений.ЗначенияСвойствОбъектов.СоздатьНаборЗаписей();
        НаборЗаписей.Отбор.Объект.Установить(ДокЗаказаПокупателя);
        НаборЗаписей.Отбор.Свойство.Установить(СтатусCOF000027);
        НаборЗаписей.Очистить();
        НаборЗаписей.Записать();
        
        Запись = РегистрыСведений.ЗначенияСвойствОбъектов.СоздатьМенеджерЗаписи();
        Запись.Объект = ДокЗаказаПокупателя;
        Запись.Свойство = СтатусCOF000027;
        Запись.Значение = "N";
        Запись.Записать();
        
    ИначеЕсли Строка(Статус) = "В обработке" Тогда

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

Анализ неэффективности: почему такая конструкция является избыточной?

Чтобы понять, почему представленный код неоптимален, давайте разберем, как работают объекты НаборЗаписей и МенеджерЗаписи в платформе 1С:Предприятие 8.3.

Объект НаборЗаписей:

  1. Создание НаборЗаписей = РегистрыСведений.ЗначенияСвойствОбъектов.СоздатьНаборЗаписей();
  2. Установка отбора: НаборЗаписей.Отбор.Объект.Установить(ДокЗаказаПокупателя); НаборЗаписей.Отбор.Свойство.Установить(СтатусCOF000047); — эти строки определяют, какие записи из регистра будут затрагиваться операциями с этим набором.
  3. Вызов НаборЗаписей.Очистить(); — эта команда удаляет *все* записи из самого объекта НаборЗаписей. После этого объект НаборЗаписей становится пустым.
  4. Вызов НаборЗаписей.Записать(); — этот метод применяет изменения, накопленные в объекте НаборЗаписей, к базе данных. Если в наборе установлен отбор, и сам набор пуст (как после Очистить()), то при вызове Записать() из базы данных удаляются все записи, соответствующие установленному отбору. То есть, происходит операция удаления. Если отбор не установлен и набор пуст, то Записать() приведет к полной очистке всего регистра!

Объект МенеджерЗаписи:

  1. Создание Запись = РегистрыСведений.ЗначенияСвойствОбъектов.СоздатьМенеджерЗаписи(); — создаем объект для работы с одной записью регистра.
  2. Заполнение измерений и ресурсов: Запись.Объект = ДокЗаказаПокупателя; Запись.Свойство = СтатусCOF000047; Запись.Значение = Справочники.А_СтатусыЗаказов.НайтиПоНаименованию("Возврат");
  3. Вызов Запись.Записать(); — этот метод осуществляет операцию записи одной записи в базу данных. Если в регистре уже существует запись с такими же значениями измерений (ключевых полей), то эта существующая запись будет обновлена. Если такой записи нет, она будет добавлена как новая.

Таким образом, в представленном проблемном коде мы наблюдаем следующую последовательность действий:

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

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

Правильное использование МенеджераЗаписи и НабораЗаписей

Для эффективной работы с регистрами сведений важно понимать назначение и сценарии использования МенеджераЗаписи и НабораЗаписей.

  1. Работа с одной записью: используем МенеджерЗаписи

    Когда нам нужно создать, обновить или прочитать одну уникальную запись регистра сведений (уникальность определяется набором значений всех измерений), наиболее подходящим инструментом является МенеджерЗаписи. Он сам позаботится о том, чтобы либо обновить существующую запись (если измерения совпадают), либо создать новую.

    Пример корректного использования МенеджераЗаписи:

    
    // Определяем значения для измерений и ресурса
    ДокЗаказаПокупателя = Документы.ЗаказПокупателя.НайтиПоНомер("00000001", Дата("20230101")); // Пример получения документа
    СтатусCOF000047 = ПланыВидовХарактеристик.СвойстваОбъектов.НайтиПоКоду("COF000047");
    ЗначениеСтатуса = Справочники.А_СтатусыЗаказов.НайтиПоНаименованию("Возврат");
    
    // Создаем менеджер записи
    Запись = РегистрыСведений.ЗначенияСвойствОбъектов.СоздатьМенеджерЗаписи();
    
    // Заполняем измерения
    Запись.Объект = ДокЗаказаПокупателя;
    Запись.Свойство = СтатусCOF000047;
    
    // Заполняем ресурс
    Запись.Значение = ЗначениеСтатуса;
    
    // Записываем запись в регистр.
    // Если запись с такими же измерениями уже существует, она будет обновлена.
    // Если не существует, будет создана новая.
    Запись.Записать();
    

    Такой подход является единственно верным и эффективным для операций с одиночной записью.

  2. Работа с коллекцией записей: используем НаборЗаписей

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

    Важное поведение НаборЗаписей.Записать():

    Метод НаборЗаписей.Записать() по умолчанию работает в режиме замещения. Это означает, что:

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

    Таким образом, если ваша цель — обновить или добавить несколько записей, и вы используете НаборЗаписей, вам не нужно предварительно вызывать Очистить(), а затем Записать(). Достаточно сформировать набор записей с нужным отбором и новыми/измененными данными, а затем однократно вызвать Записать().

    Пример корректного использования НабораЗаписей для массового обновления/добавления:

    
    // Определяем общие измерения для отбора
    ДокЗаказаПокупателя = Документы.ЗаказПокупателя.НайтиПоНомер("00000001", Дата("20230101")); // Пример получения документа
    
    // Определяем свойства
    СтатусCOF000050 = ПланыВидовХарактеристик.СвойстваОбъектов.НайтиПоКоду("COF000050");
    СтатусCOF000047 = ПланыВидовХарактеристик.СвойстваОбъектов.НайтиПоКоду("COF000047");
    СтатусCOF000027 = ПланыВидовХарактеристик.СвойстваОбъектов.НайтиПоКоду("COF000027");
    
    // Создаем набор записей
    НаборЗаписей = РегистрыСведений.ЗначенияСвойствОбъектов.СоздатьНаборЗаписей();
    НаборЗаписей.Отбор.Объект.Установить(ДокЗаказаПокупателя); // Устанавливаем отбор по объекту
    
    // Читаем текущие записи по отбору (если нужно их изменить)
    // НаборЗаписей.Прочитать(); 
    // Если цель - полностью перезаписать свойства объекта, то Прочитать() не требуется
    
    // Добавляем или изменяем записи в наборе
    // Запись 1
    НоваяЗапись1 = НаборЗаписей.Добавить();
    НоваяЗапись1.Объект = ДокЗаказаПокупателя;
    НоваяЗапись1.Свойство = СтатусCOF000050;
    НоваяЗапись1.Значение = "[N] Новый";
    
    // Запись 2
    НоваяЗапись2 = НаборЗаписей.Добавить();
    НоваяЗапись2.Объект = ДокЗаказаПокупателя;
    НоваяЗапись2.Свойство = СтатусCOF000047;
    НоваяЗапись2.Значение = Справочники.А_СтатусыЗаказов.НайтиПоНаименованию("Новый");
            
    // Запись 3
    НоваяЗапись3 = НаборЗаписей.Добавить();
    НоваяЗапись3.Объект = ДокЗаказаПокупателя;
    НоваяЗапись3.Свойство = СтатусCOF000027;
    НоваяЗапись3.Значение = "N";
    
    // Записываем набор в регистр. 
    // Все записи регистра, соответствующие отбору (по ДокЗаказаПокупателя), 
    // будут заменены на записи из НабораЗаписей.
    НаборЗаписей.Записать();
    

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

Рекомендации по оптимизации и рефакторингу

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

  1. Для одной записи используйте МенеджерЗаписи: Если ваша задача — установить или изменить значение одного ресурса для одной записи регистра сведений, где уникальность определяется всеми измерениями, используйте МенеджерЗаписи. Это наиболее прямой и производительный способ.
  2. Для множества записей используйте НаборЗаписей: Если вам нужно массово добавить, изменить или удалить несколько записей, объединенных общим отбором, используйте НаборЗаписей. Сформируйте все необходимые записи в наборе, а затем однократно вызовите Записать().
  3. Избегайте ненужных операций Очистить(): Метод Очистить() у НабораЗаписей редко требуется в комбинации с последующим добавлением записей, так как Записать() сам по себе имеет замещающее поведение по умолчанию. Используйте Очистить() только если ваша единственная цель — удалить все записи по установленному отбору, не добавляя ничего взамен.
  4. Учитывайте производительность: Многократная запись наборов записей или отдельных записей в цикле по одной записи значительно медленнее, чем формирование и запись одного набора. Старайтесь выполнять операции с базой данных пакетно.
  5. Новые режимы замещения (платформа 8.3.25+): Для повышения производительности и большей гибкости, начиная с версии платформы 8.3.25, появились новые режимы замещения для НабораЗаписей. Они позволяют более тонко управлять тем, как записи в наборе взаимодействуют с существующими записями в базе данных.
    • РежимЗамещения.Добавление: Добавляет новые записи, игнорируя существующие.
    • РежимЗамещения.Замещение (по умолчанию): Удаляет по отбору и записывает все записи из набора.
    • РежимЗамещения.Слияние: Обновляет записи с совпадающими ключевыми полями и добавляет новые. Это часто наиболее оптимальный режим, когда требуется обновить часть записей и добавить новые без полной перезаписи всего сегмента.
    • РежимЗамещения.Удаление: Удаляет записи, совпадающие по ключевым полям с записями в наборе.

    Использование РежимЗамещения.Слияние, например, может значительно оптимизировать операции, когда изменяется лишь небольшая часть записей в большом сегменте данных.

    Пример использования РежимЗамещения.Слияние:

    
    НаборЗаписей = РегистрыСведений.ЗначенияСвойствОбъектов.СоздатьНаборЗаписей();
    НаборЗаписей.Отбор.Объект.Установить(ДокЗаказаПокупателя);
    
    // ... Добавляем или изменяем записи в НаборЗаписей ...
    
    НаборЗаписей.Записать(Истина, РежимЗамещения.Слияние);
    

Заключение

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

← На главную