При разработке конфигураций в 1С мы часто сталкиваемся с необходимостью программного управления данными в регистрах сведений. Одной из типовых задач является добавление новых записей и обновление (замещение) уже существующих, если записи с аналогичным набором измерений уже присутствуют. Рассмотрим, как эффективно решить эту проблему, используя различные подходы и учитывая важные нюансы.
Прежде чем перейти к программным решениям, давайте кратко разберем, что представляют собой регистры сведений и как они функционируют:
Хранение дополнительной информации: Регистры сведений предназначены для хранения произвольной информации в разрезе нескольких измерений. Они могут быть периодическими (хранить историю изменений) или непериодическими (хранить актуальные значения).
Уникальность записей: Для непериодических регистров сведений, как в нашем случае, комбинация значений всех измерений определяет уникальность записи. Это означает, что не может существовать две записи с одинаковым набором значений измерений.
Независимые и подчиненные регистратору:
Независимые регистры сведений хранят информацию сами по себе, без привязки к конкретным документам. Изменение или удаление записей в них происходит программно или вручную.
Подчиненные регистратору регистры сведений тесно связаны с документами. Записи в них создаются при проведении документов и автоматически удаляются при их отмене. Для таких регистров в их состав всегда включается измерение Регистратор.
В нашей задаче мы имеем дело с непериодическим регистром сведений ОсновныеПоставщики, который имеет измерение Номенклатура и ресурс Поставщик. Основная цель – для каждой номенклатуры из списка Товары установить или обновить поставщика, при этом записи с новыми номенклатурами должны добавляться, а с существующими – замещаться.
Для программной работы с записями регистров сведений в 1С мы можем использовать два основных объекта:
РегистрСведенийМенеджерЗаписи (МенеджерЗаписи): Предназначен для работы с одной конкретной записью регистра. Идеально подходит, когда нужно выполнить операцию над отдельной записью.
РегистрСведенийНаборЗаписей (НаборЗаписей): Используется для работы с коллекцией записей, которые соответствуют определенному отбору (например, по регистратору или по набору измерений). Эффективен для массовых операций.
Для задачи добавления новых записей и замещения существующих по уникальному набору измерений, объект МенеджерЗаписи является наиболее интуитивным и простым в использовании. Он позволяет системе 1С автоматически определить, существует ли уже запись с заданными значениями измерений, и выполнить соответствующее действие: добавить новую запись или обновить ресурс существующей.
Как это работает на практике?
Мы создаем менеджер записи, устанавливаем значения измерений (в нашем случае Номенклатура) и ресурсов (Поставщик), а затем вызываем метод Записать(). Если записи с такой комбинацией измерений не существует, она будет создана. Если существует, то ее ресурс будет обновлен.
Рассмотрим пример кода, который решает поставленную задачу:
Для Каждого Строка Из Товары Цикл
МенеджерЗаписи = РегистрыСведений.ОсновныеПоставщики.СоздатьМенеджерЗаписи();
// Устанавливаем значение измерения
МенеджерЗаписи.Номенклатура = Строка.Номенклатура;
// Устанавливаем значение ресурса
МенеджерЗаписи.Поставщик = Контрагент;
// Записываем запись в регистр сведений.
// Если запись с такой Номенклатурой уже существует, она будет замещена.
// Если не существует, будет добавлена новая.
МенеджерЗаписи.Записать();
КонецЦикла;
Объясняем код по шагам:
Мы используем цикл Для Каждого Строка Из Товары Цикл для перебора каждой строки из коллекции Товары (предполагаем, что это табличная часть документа или другая коллекция данных).
Внутри цикла для каждой строки создается новый экземпляр МенеджерЗаписи с помощью метода РегистрыСведений.ОсновныеПоставщики.СоздатьМенеджерЗаписи().
Затем мы присваиваем значения свойствам МенеджераЗаписи, соответствующим измерениям и ресурсам нашего регистра. В данном случае МенеджерЗаписи.Номенклатура = Строка.Номенклатура; устанавливает измерение, а МенеджерЗаписи.Поставщик = Контрагент; устанавливает ресурс.
Наконец, вызывается метод МенеджерЗаписи.Записать(). Именно этот метод выполняет всю магию: он проверяет наличие записи с указанным значением измерения Номенклатура. Если запись найдена, он обновляет ее ресурс Поставщик. Если запись не найдена, он создает новую запись.
Почему именно МенеджерЗаписи обеспечивает требуемое поведение (добавить/заместить)?
Это происходит благодаря внутреннему механизму работы МенеджераЗаписи с непериодическими регистрами сведений. Он использует значения измерений в качестве уникального ключа. Если ключ уже существует, происходит обновление. Если нет, создание. Это очень удобно для работы с одной записью или при обработке данных в цикле, когда логика "добавить или обновить" нужна для каждого элемента.
Важный нюанс: Транзакции
Когда мы выполняем операции записи в цикле через МенеджерЗаписи, каждая операция Записать() выполняется как отдельная транзакция на уровне СУБД, если это не обернуто в явную транзакцию. Это может снизить производительность при большом количестве записей. Для обеспечения целостности данных и повышения производительности при работе с большим объемом записей в цикле, рекомендуется оборачивать такие операции в единую транзакцию:
НачатьТранзакцию();
Попытка
Для Каждого Строка Из Товары Цикл
МенеджерЗаписи = РегистрыСведений.ОсновныеПоставщики.СоздатьМенеджерЗаписи();
МенеджерЗаписи.Номенклатура = Строка.Номенклатура;
МенеджерЗаписи.Поставщик = Контрагент;
МенеджерЗаписи.Записать();
КонецЦикла;
ЗафиксироватьТранзакцию();
Исключение
ОтменитьТранзакцию();
ВызватьИсключение; // Перевыбросить исключение, чтобы сообщить о проблеме
КонецПопытки;
Ограничения использования МенеджераЗаписи
Следует помнить, что МенеджерЗаписи доступен только для независимых регистров сведений. Для регистров, подчиненных регистратору, его использование невозможно, так как там всегда требуется контекст регистратора.
Исходная проблема автора возникла при попытке использовать НаборЗаписей. При вызове НаборЗаписи.Записать() без установленного отбора система 1С замещает все содержимое регистра новыми записями, которые были добавлены в набор. Это не то поведение, которое нам нужно, так как мы хотим только добавить/заместить конкретные записи, а не стирать весь регистр.
Если же использовать НаборЗаписи.Записать(ЛОЖЬ), система попытается добавить записи, и если какая-либо запись с такими же измерениями уже существует, произойдет ошибка нарушения уникальности.
Объект НаборЗаписей более эффективен для массовых операций, когда нам нужно работать с коллекцией записей, соответствующих определенным критериям, например, все записи за определенный период или все записи, относящиеся к конкретному регистратору. Чтобы достичь цели "добавить новые и заместить существующие" с помощью НаборЗаписей, нам потребуется более сложная логика, включающая установку отбора и чтение существующих данных.
Как правильно использовать НаборЗаписей для выборочного добавления и замещения:
Для того чтобы НаборЗаписей работал как "добавить или обновить" для конкретных измерений, нам необходимо выполнить следующие шаги:
Создаем НаборЗаписей: Инстанцируем объект НаборЗаписей для нашего регистра.
НаборЗаписи = РегистрыСведений.ОсновныеПоставщики.СоздатьНаборЗаписей();
Устанавливаем отбор: Это ключевой шаг. Мы должны указать, какие записи регистра мы хотим затронуть. В нашем случае, это номенклатуры, которые присутствуют в коллекции Товары. Сначала соберем все эти номенклатуры в массив.
МассивНоменклатур = Новый Массив();
Для Каждого СтрокаТЧ Из Товары Цикл
МассивНоменклатур.Добавить(СтрокаТЧ.Номенклатура);
КонецЦикла;
Если МассивНоменклатур.Количество() > 0 Тогда
НаборЗаписи.Отбор.Номенклатура.Установить(МассивНоменклатур);
КонецЕсли;
Прочитываем существующие записи: После установки отбора, метод Прочитать() загрузит в наш НаборЗаписей все записи, которые соответствуют установленному отбору. Теперь НаборЗаписи содержит текущие данные для тех номенклатур, которые мы собираемся обрабатывать.
Если МассивНоменклатур.Количество() > 0 Тогда
// ... (установка отбора) ...
НаборЗаписи.Прочитать(); // Загружаем текущие записи по отбору
КонецЕсли;
Формируем новые записи: Теперь НаборЗаписи содержит "старые" данные для наших номенклатур. Чтобы произвести "замещение", мы можем очистить набор и добавить в него новые данные. Важно: При таком подходе записи, которые были в регистре по нашему отбору, но не были заново добавлены в набор, будут удалены при записи.
// Очищаем набор, чтобы добавить актуальные данные
НаборЗаписи.Очистить();
// Заполняем набор записями из нашей коллекции Товары
Для Каждого СтрокаТЧ Из Товары Цикл
СтрЗаписи = НаборЗаписи.Добавить();
СтрЗаписи.Номенклатура = СтрокаТЧ.Номенклатура;
СтрЗаписи.Поставщик = Контрагент;
КонецЦикла;
Записываем набор: Вызов НаборЗаписи.Записать() (по умолчанию с параметром Истина, что означает замещение) теперь будет работать только в рамках установленного отбора. Все записи в регистре, не попадающие под отбор, останутся нетронутыми. Записи, попадающие под отбор, будут заменены содержимым нашего набора.
НаборЗаписи.Записать();
Полный пример кода с использованием НаборЗаписей для замещения по отбору:
НачатьТранзакцию();
Попытка
НаборЗаписи = РегистрыСведений.ОсновныеПоставщики.СоздатьНаборЗаписей();
// 1. Собираем все номенклатуры, которые будут участвовать в обновлении/добавлении
МассивНоменклатур = Новый Массив();
Для Каждого СтрокаТЧ Из Товары Цикл
МассивНоменклатур.Добавить(СтрокаТЧ.Номенклатура);
КонецЦикла;
Если МассивНоменклатур.Количество() > 0 Тогда
// 2. Устанавливаем отбор по номенклатурам, которые мы хотим изменить/добавить
НаборЗаписи.Отбор.Номенклатура.Установить(МассивНоменклатур);
// 3. Прочитываем существующие записи, соответствующие отбору.
// Это важно, если мы хотим сохранить какие-либо данные, не связанные с текущими изменениями,
// но в данном случае мы будем их очищать и перезаписывать.
НаборЗаписи.Прочитать();
КонецЕсли;
// 4. Очищаем набор, чтобы сформировать его заново с актуальными данными.
// Записи, которые были в регистре по отбору, но не будут добавлены в набор, будут удалены при Записи.
НаборЗаписи.Очистить();
// 5. Заполняем набор записями из нашей коллекции Товары
Для Каждого СтрокаТЧ Из Товары Цикл
СтрЗаписи = НаборЗаписи.Добавить();
СтрЗаписи.Номенклатура = СтрокаТЧ.Номенклатура;
СтрЗаписи.Поставщик = Контрагент;
КонецЦикла;
// 6. Записываем набор. Параметр Истина (по умолчанию) означает замещение.
// Поскольку отбор установлен, замещению подвергнутся только записи,
// попадающие под этот отбор. Записи вне отбора останутся без изменений.
НаборЗаписи.Записать();
ЗафиксироватьТранзакцию();
Исключение
ОтменитьТранзакцию();
ВызватьИсключение;
КонецПопытки;
Этот подход более сложен для реализации логики "добавить/заместить конкретный элемент" по сравнению с МенеджеромЗаписи, но может быть более производительным для очень больших объемов данных, когда мы массово обновляем или заменяем целый сегмент регистра.
При работе с регистрами сведений стоит учитывать несколько дополнительных моментов:
Если ваш регистр сведений является подчиненным регистратору (например, документу), то при любой операции записи через НаборЗаписей вы обязаны установить отбор по регистратору. Без этого отбора любая запись будет либо некорректной, либо повлияет на весь регистр, что почти всегда нежелательно для подчиненных регистров.
Пример установки отбора для подчиненного регистра:
НаборЗаписи = РегистрыСведений.ОсновныеПоставщики.СоздатьНаборЗаписей();
НаборЗаписи.Отбор.Регистратор.Установить(ЭтотОбъект.Ссылка); // Где ЭтотОбъект - ссылка на текущий документ
// Далее Прочитать(), Очистить() и заполнить набор, как показано выше
НаборЗаписи.Записать();
В случае подчиненных регистров, если код пишется в обработчике проведения документа, платформа автоматически управляет записями, связанными с данным регистратором, так что зачастую достаточно просто добавить записи в набор и вызвать Записать(), а платформа уже сама применит отбор по текущему регистратору и корректно удалит старые записи.
В сообщении 3 верно подмечено, что "структура регистра плохая. В общем случае может быть больше одного поставщика одной номенклатуры". Текущая структура регистра ОсновныеПоставщики (Номенклатура - измерение, Поставщик - ресурс) подразумевает, что для каждой номенклатуры может быть только один основной поставщик, так как измерения определяют уникальность записи. Если вам необходимо хранить нескольких поставщиков для одной и той же номенклатуры, структура регистра должна быть изменена.
Возможные варианты решения:
Добавить Поставщик в измерения: Если вы добавите Поставщик в измерения, то комбинация Номенклатура + Поставщик станет уникальной, и вы сможете хранить несколько поставщиков для одной номенклатуры (например, с дополнительным ресурсом, указывающим приоритет или процент закупок).
Создать дополнительный регистр: Иногда, если логика становится слишком сложной, проще создать отдельный регистр или справочник для хранения множественных связей.
Как уже было упомянуто, при массовых операциях (тысячи и десятки тысяч записей) использование НаборЗаписей с отбором и единой транзакцией обычно является более производительным, чем многократный вызов МенеджерЗаписи.Записать() в цикле. Каждый вызов МенеджерЗаписи.Записать() может инициировать отдельную транзакцию и выполнение нескольких операций (чтение, удаление, добавление) на уровне СУБД, что замедляет процесс.
Для ускорения записи в регистры сведений при массовых операциях также возможно временное отключение итогов регистра (если регистр их ведет), но это критически важно оборачивать в транзакцию: отключение, запись, включение итогов – все должно быть в одной транзакции, чтобы избежать повреждения данных.
В новых версиях платформы 1С (начиная с 8.3.25) были добавлены новые режимы записи для наборов записей независимых регистров сведений. Помимо стандартных режимов Добавление (Append) и Замещение (Replace), появились режимы Слияние (Merge) и Удаление (Delete). Режим Слияние может быть особенно полезен, так как он позволяет более гибко управлять процессом объединения данных, добавляя новые записи и обновляя существующие, без необходимости вручную очищать и формировать набор.
Однако для большинства типовых задач, таких как наша, подходы с МенеджеромЗаписи или НаборомЗаписей с явным отбором и Прочитать()/Очистить() остаются актуальными и хорошо зарекомендовавшими себя.
Таким образом, для решения задачи "добавить новые измерения, заместить существующие" наиболее простым и понятным способом является использование МенеджераЗаписи в цикле, обернув его в транзакцию для оптимизации производительности. Если же мы работаем с большими объемами данных и хотим контролировать замещение определенного среза данных, то подход с НаборЗаписей, отбором и предварительным чтением/очисткой будет более подходящим.