При работе с системой 1С:Предприятие, особенно на платформе 8.3, часто возникает задача, когда при записи одной сущности или одной записи регистра сведений требуется фактически добавить в регистр несколько связанных записей. Например, при проведении документа, который, казалось бы, должен создать одну запись в регистре, по какой-то бизнес-логике необходимо сформировать целый набор данных. Разберем эту ситуацию подробнее, выясним, почему возникают сложности, и как правильно реализовать подобную логику.
Прежде всего, давайте рассмотрим два ключевых объекта для работы с регистрами сведений в 1С:Предприятии — МенеджерЗаписи и НаборЗаписей. Понимание их различий критически важно для корректной реализации нашей задачи.
МенеджерЗаписи (МенеджерЗаписиРегистраСведений)
Этот объект предназначен для работы с одной уникальной записью регистра сведений. Мы используем его, когда нужно создать, изменить или удалить конкретную запись, однозначно идентифицируемую по ее измерениям. Например, если у нас есть независимый регистр сведений "НастройкиПользователей" с измерениями "Пользователь" и "Настройка", и мы хотим изменить значение одной конкретной настройки для одного пользователя, мы создадим менеджер записи, заполним измерения, ресурсы и вызовем метод Записать(). Важно понимать, что внутри себя менеджер записи также может использовать наборы записей: один для удаления старой записи (если измерения изменились) и один для записи новой. Это удобно для интерактивного редактирования отдельных записей — для этого подойдёт универсальный редактор регистров для ручной корректировки.
НаборЗаписей (НаборЗаписейРегистраСведений)
Объект НаборЗаписей предназначен для работы с коллекцией записей регистра сведений, отобранных по определенным условиям. Это основной инструмент для массового чтения, изменения и записи данных. Если нам необходимо добавить, удалить или изменить сразу несколько записей, особенно когда они объединены общим признаком (например, относятся к одному регистратору), мы всегда используем НаборЗаписей. Он позволяет сформировать целую коллекцию записей в памяти, а затем одним вызовом метода Записать() сохранить их в базу данных, что значительно эффективнее, чем записывать каждую запись по отдельности.
Как показывает опыт, в исходной задаче, скорее всего, использовался МенеджерЗаписи, или подразумевалась запись одной записи, а фактически требовалось создать несколько. Для решения задачи добавления нескольких записей нам однозначно потребуется работать с НаборЗаписей.
Одна из наиболее частых ошибок, с которой сталкиваются разработчики при попытке добавить записи в регистр сведений, особенно в обработчиках событий, это "набор не соответствует установленному отбору". Разберем, почему она возникает и как ее избежать.
Эта ошибка появляется в нескольких случаях (выявить зависимости между объектами поможет Analyzer 1C):
Не установлен отбор по регистратору (для подчиненных регистров). Если регистр сведений является подчиненным регистратору (например, документу), то при работе с его записями через НаборЗаписей необходимо обязательно установить отбор по этому регистратору. Без этого система не сможет определить, к какому документу относятся записи, и выдаст ошибку. Каждая запись в таком регистре также должна иметь заполненное поле Регистратор, соответствующее установленному отбору.
Записи в наборе не соответствуют условиям отбора. Если мы установили какие-либо условия отбора для НаборЗаписей (например, по определенному измерению), но пытаемся добавить в этот набор записи, которые не удовлетворяют этим условиям, система также выдаст ошибку. Все записи в наборе должны соответствовать заданному отбору.
Некорректная манипуляция с набором в событии ПередЗаписью. Если в обработчике события ПередЗаписью регистра сведений мы пытаемся удалить единственную строку из набора, который собирается записаться, это может привести к тому, что набор станет пустым, но при этом будет ожидаться запись, связанная с изначальными измерениями. В таком сценарии также может возникнуть подобная ошибка, так как система ожидает соответствия.
Для добавления нескольких записей в регистр сведений наиболее надежным и рекомендуемым подходом является работа с объектом НаборЗаписей (подготовить каркас кода поможет консоль кода с ИИ — для этого есть консоль кода и запросов с ИИ). Рассмотрим шаги, которые мы должны предпринять:
Создание объекта НаборЗаписей. Мы начинаем с создания нового экземпляра НаборЗаписей для нужного нам регистра сведений.
// Создаем новый набор записей для регистра 'МойРегистрСведений'
НаборЗаписейРС = РегистрыСведений.МойРегистрСведений.СоздатьНаборЗаписей();
Установка отбора. Если наш регистр сведений подчинен регистратору (например, документу, который мы сейчас проводим), то мы обязательно должны установить отбор по этому регистратору. Это критически важный шаг для подчиненных регистров.
// Для подчиненных регистров сведений обязательно устанавливаем отбор по регистратору
// Предположим, что 'ДокументОбъект' - это ссылка на объект документа, который записывает регистр
НаборЗаписейРС.Отбор.Регистратор.Установить(ДокументОбъект.Ссылка);
Если регистр независимый, отбор может быть установлен по другим измерениям, или не установлен вовсе (для программного анализа сложных запросов в таких случаях подойдет процессор схемы запроса).
Добавление новых записей. Теперь мы можем добавлять новые записи в коллекцию НаборЗаписей, используя метод Добавить() (обработать выбранные запросом объекты можно через интерактивный обработчик). Каждая новая запись будет представлять собой объект типа МенеджерЗаписиРегистраСведений (хотя мы работаем с ним в контексте набора).
// Добавляем первую запись
НоваяЗапись1 = НаборЗаписейРС.Добавить();
НоваяЗапись1.Измерение1 = "Значение_А";
НоваяЗапись1.Измерение2 = "Параметр_1";
НоваяЗапись1.Ресурс1 = 10;
// Для подчиненных регистров поле Регистратор также должно быть заполнено в каждой записи
НоваяЗапись1.Регистратор = ДокументОбъект.Ссылка;
// Добавляем вторую запись
НоваяЗапись2 = НаборЗаписейРС.Добавить();
НоваяЗапись2.Измерение1 = "Значение_Б";
НоваяЗапись2.Измерение2 = "Параметр_2";
НоваяЗапись2.Ресурс1 = 25;
НоваяЗапись2.Регистратор = ДокументОбъект.Ссылка;
Заполнение полей записей. Для каждой добавленной записи мы заполняем ее измерения, ресурсы и реквизиты в соответствии с нашей бизнес-логикой. Помним про обязательность заполнения Регистратор для подчиненных регистров.
Сохранение набора записей. После того как все необходимые записи добавлены в коллекцию НаборЗаписей и их поля заполнены, мы вызываем метод Записать(). Это сохранит все записи из коллекции в базу данных.
Попытка
НаборЗаписейРС.Записать(); // Записываем весь сформированный набор
Исключение
Сообщить("Ошибка при записи регистра сведений 'МойРегистрСведений': " + ОписаниеОшибки());
// Дополнительная обработка ошибок, если требуется
КонецПопытки;
Если нам нужно сначала удалить все существующие записи по установленному отбору, а затем записать новые, мы можем использовать методы Прочитать() (для загрузки существующих записей по отбору) и Очистить() (для очистки загруженного набора в памяти, что при последующем Записать() приведет к удалению старых и вставке новых):
// Если нужно сначала удалить все записи по отбору, а затем записать новые
НаборЗаписейРС.Прочитать(); // Загружаем все существующие записи по установленному отбору
НаборЗаписейРС.Очистить(); // Очищаем набор в памяти. При Записать() старые записи будут удалены
// ... здесь добавляем новые записи, как показано выше ...
НаборЗаписейРС.Записать(); // Записываем новые записи
Важно: Метод Очистить() очищает только коллекцию записей в объекте НаборЗаписей в памяти. Чтобы изменения отразились в базе данных, необходимо вызвать Записать().
Попытка добавить новые записи в регистр сведений непосредственно в обработчиках событий того же регистра (например, ПередЗаписью или ПриЗаписи в модуле набора записей или менеджера записи) является сомнительной концепцией и может привести к серьезным проблемам:
Рекурсия. Если в обработчике ПередЗаписью мы попытаемся записать дополнительные записи в тот же регистр, это вызовет повторный вызов обработчика ПередЗаписью, создавая рекурсивный цикл. Это может привести к переполнению стека и аварийному завершению работы программы, или к непредсказуемым результатам, если логика выхода из рекурсии не предусмотрена.
Представьте: "Я записываю регистр. Перед записью, нужно записать ещё. Перед записью ещё одной записи, нужно записать ещё...". Это бесконечный цикл.
Ошибка "набор не соответствует установленному отбору". Как мы уже выяснили, при работе с НаборЗаписей требуется строгий отбор. Внутри обработчиков ПередЗаписью или ПриЗаписи мы работаем с набором записей, который уже находится в процессе записи. Попытка добавить в него записи с другими параметрами отбора или создать новый набор и записать его может конфликтовать с текущей транзакцией или привести к вышеупомянутой ошибке.
Рекомендации по работе с событиями:
ПередЗаписью и ПриЗаписи регистра сведений лучше использовать для:
Иными словами, эти события предназначены для валидации и модификации уже существующих в наборе или менеджере записи данных, а не для создания совершенно новых, независимых записей.
Вызов отдельной процедуры. Если вам необходимо добавить дополнительные записи, обусловленные записью основной, сделайте это вне обработчика событий самого регистра или, по крайней мере, в отдельной процедуре (функции), которая создает свой собственный, изолированный НаборЗаписей и записывает его. Эту процедуру следует вызывать из:
При этом важно, чтобы вызов этой процедуры происходил в контексте, который не вызовет рекурсию в обработчиках основного регистра.
// Пример в модуле объекта документа
Процедура ОбработкаПроведения(Отказ, РежимПроведения)
// ... Ваш основной код проведения документа ...
// Здесь формируются и записываются основные движения документа, в том числе в РегистрСведений.МойРегистрСведений
ДокументОбъект = ЭтотОбъект;
// Предположим, что здесь уже выполнена основная запись в РС
// Если требуется добавить ДОПОЛНИТЕЛЬНЫЕ записи по какому-то условию,
// вызываем отдельную процедуру, которая не конфликтует с текущей транзакцией записи.
Если ДокументОбъект.ТипОперации = Перечисления.ТипыОпераций.НеобходимыДопЗаписи Тогда
// Вызываем отдельную процедуру в общем модуле
// Важно: эта процедура должна создавать СВОЙ НаборЗаписей и записывать его
МодулиСервера.ДобавитьСвязанныеЗаписиВРегистр(ДокументОбъект.Ссылка);
КонецЕсли;
// ... Дальнейший код проведения ...
КонецПроцедуры
// Пример в общем модуле (МодулиСервера), где происходит добавление
Процедура ДобавитьСвязанныеЗаписиВРегистр(РегистраторДокумента) Экспорт
НаборДопЗаписей = РегистрыСведений.МойРегистрСведений.СоздатьНаборЗаписей();
НаборДопЗаписей.Отбор.Регистратор.Установить(РегистраторДокумента);
// Пример добавления нескольких записей на основе данных документа
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| ТабличнаяЧасть.Поле1,
| ТабличнаяЧасть.Поле2,
| ТабличнаяЧасть.Поле3
|ИЗ
| Документ.МойДокумент.ТабличнаяЧастьСДопДанными КАК ТабличнаяЧасть
|ГДЕ
| ТабличнаяЧасть.Ссылка = &Регистратор";
Запрос.УстановитьПараметр("Регистратор", РегистраторДокумента);
Выборка = Запрос.Выполнить().Выбрать();
Пока Выборка.Следующий() Цикл
НоваяЗапись = НаборДопЗаписей.Добавить();
НоваяЗапись.Измерение1 = Выборка.Поле1;
НоваяЗапись.Измерение2 = Выборка.Поле2;
НоваяЗапись.Ресурс1 = Выборка.Поле3;
НоваяЗапись.Регистратор = РегистраторДокумента;
КонецЦикла;
Попытка
НаборДопЗаписей.Записать();
Исключение
Сообщить("Ошибка при добавлении связанных записей для регистратора "
+ РегистраторДокумента + ": " + ОписаниеОшибки());
// Можно записать в ЖурналРегистрации или предпринять другие действия
КонецПопытки;
КонецПроцедуры
В этом примере логика формирования дополнительных записей вынесена в отдельную процедуру в общем модуле, которая вызывается из модуля проведения документа. Она создает свой собственный объект НаборЗаписей, устанавливает для него отбор и записывает данные, что позволяет избежать рекурсии и конфликтов.
При работе с наборами записей крайне важно учитывать производительность, особенно когда речь идет о большом количестве данных. Рассмотрим несколько практических советов:
Избегайте записи по одной или нескольким записям в цикле. Многократный вызов метода Записать() для отдельных записей или маленьких наборов внутри цикла значительно замедляет работу системы. Каждый вызов Записать() инициирует транзакцию записи в базу данных, что является ресурсоемкой операцией.
Записывайте весь набор целиком. Для оптимальной производительности всегда старайтесь собрать все необходимые записи в один объект НаборЗаписей, а затем вызвать Записать() один раз.
Порционная запись для очень больших объемов. Если количество записей исчисляется десятками и сотнями тысяч, то сбор их всех в один НаборЗаписей может привести к чрезмерному потреблению оперативной памяти. В таких случаях мы можем использовать порционную запись – разбивать весь объем данных на части (например, по 1000 или 5000 записей), формировать НаборЗаписей для каждой порции и записывать ее. После записи каждой порции не забывайте очищать набор методом Очистить() для освобождения памяти и подготовки к следующей порции.
Осторожность при перезаписи. Перезапись большого набора записей, в котором меняется менее 30% данных, часто неоптимальна. Метод Записать() при наличии отбора по умолчанию удаляет все существующие записи по этому отбору, а затем вставляет новые. Это медленнее, чем точечное изменение только измененных данных. Если регистр сведений непериодический и не подчинен регистратору, иногда бывает эффективнее работать через МенеджерЗаписи для точечных изменений, если мы знаем измерения изменяемых записей.
Новые режимы замещения (начиная с 8.3.26). В новых версиях платформы 1С:Предприятие появились новые режимы замещения для объектов НаборЗаписей (РежимЗамещения). Они позволяют более гибко управлять операциями с данными:
Слияние: объединяет записи текущего набора с существующими в базе данных.Удаление: удаляет записи из базы данных по установленному отбору.Обновление: обновляет существующие записи и добавляет новые.Использование этих режимов может значительно оптимизировать пакетную обработку записей в зависимости от конкретной задачи.
В очередной раз подчеркнем, что для регистров сведений, которые подчинены регистратору, крайне важно соблюдать следующие правила:
Всегда устанавливайте отбор по регистратору. При создании НаборЗаписей первым делом установите отбор по ссылке на документ-регистратор. Это позволит системе корректно связать записи с документом.
Заполняйте поле Регистратор в каждой записи. Каждая запись, добавляемая в НаборЗаписей подчиненного регистра, должна иметь заполненное поле Регистратор, значение которого должно совпадать с тем, по которому установлен отбор. Иначе вы получите ошибку "набор не соответствует установленному отбору".
Следуя этим рекомендациям, вы сможете эффективно и безопасно добавлять несколько записей в регистр сведений, избегая распространенных ошибок и проблем с производительностью.