Часто при разработке на платформе 1С возникает задача обновить данные в регистре сведений на основе информации из табличной части документа или другой таблицы. На первый взгляд, задача простая: нужно перебрать строки в цикле и для каждой строки создать или обновить запись в регистре. Однако иногда возникает ситуация, когда новые записи создаются, а существующие не обновляются, хотя код кажется абсолютно правильным. Рассмотрим такую ситуацию на конкретном примере, разберем причины такого поведения и изучим оптимальные способы решения задачи.
Представим, что нам нужно обновить координаты адресов контрагентов, которые хранятся в непериодическом независимом регистре сведений АдресаКоординаты с измерениями Контрагент и Адрес. Данные для обновления поступают из табличной части документа ДанныеЗаказов. Изначальный код для решения этой задачи может выглядеть так:
для каждого строка из объект.ДанныеЗаказов цикл
НоваяЗапись = РегистрыСведений.АдресаКоординаты.СоздатьМенеджерЗаписи();
НоваяЗапись.Контрагент = строка.контрагент;
НоваяЗапись.Адрес = строка.адрес;
НоваяЗапись.Прочитать();
Если НоваяЗапись.Выбран() Тогда
// Если запись найдена, обновляем ресурсы
НоваяЗапись.Широта = строка.Широта;
НоваяЗапись.Долгота = строка.Долгота;
НоваяЗапись.Записать();
Иначе
// Если запись не найдена, заполняем все поля и создаем новую
НоваяЗапись.Контрагент = строка.Контрагент;
НоваяЗапись.Адрес = строка.Адрес;
НоваяЗапись.Широта = строка.Широта;
НоваяЗапись.Долгота = строка.Долгота;
НоваяЗапись.Записать();
КонецЕсли;
КонецЦикла;
Проанализируем этот код. Логика кажется верной: создаем менеджер записи, заполняем ключевые поля (измерения), пытаемся прочитать запись из базы данных. Если она существует (НоваяЗапись.Выбран() возвращает Истина), мы обновляем только ресурсы. Если не существует — заполняем все поля и создаем новую запись. Но при выполнении этого кода автор столкнулся с проблемой: старые данные в регистре не замещались.
В чем здесь подвох? На самом деле, конструкция Если ... Иначе в данном случае избыточна. Метод Записать() для менеджера записи непериодического регистра сведений устроен так, что он сам определяет, нужно ли создавать новую запись или обновлять существующую. Он ориентируется на значения измерений. Если в базе данных уже есть запись с таким же набором измерений, она будет перезаписана. Если нет — будет создана новая. Поэтому предварительный вызов Прочитать() и проверка Выбран() — это лишнее обращение к базе данных, которое снижает производительность.
Код можно значительно упростить, убрав ненужную проверку. Он станет не только короче, но и эффективнее.
для каждого строка из объект.ДанныеЗаказов цикл
НоваяЗапись = РегистрыСведений.АдресаКоординаты.СоздатьМенеджерЗаписи();
НоваяЗапись.Контрагент = строка.контрагент;
НоваяЗапись.Адрес = строка.адрес;
НоваяЗапись.Широта = строка.Широта;
НоваяЗапись.Долгота = строка.Долгота;
НоваяЗапись.Записать(); // Система сама определит: обновить или создать
КонецЦикла;
Метод Записать() имеет необязательный параметр Замещать. По умолчанию он равен Истина, что и обеспечивает поведение "обновить, если существует". Если передать Ложь (Записать(Ложь)), то при попытке записи с уже существующим ключом система выдаст ошибку.
Для еще большего сокращения кода можно использовать процедуру ЗаполнитьЗначенияСвойств(), если имена колонок в исходной табличной части совпадают с именами измерений и ресурсов регистра.
для каждого строка из объект.ДанныеЗаказов цикл
НоваяЗапись = РегистрыСведений.АдресаКоординаты.СоздатьМенеджерЗаписи();
// Автоматически заполняет поля менеджера записи (Контрагент, Адрес, Широта, Долгота)
// из свойств строки табличной части с такими же именами.
ЗаполнитьЗначенияСвойств(НоваяЗапись, строка);
НоваяЗапись.Записать();
КонецЦикла;
Несмотря на то, что оптимизированный код работает правильно, он все еще не решал исходную проблему автора. Это говорит о том, что корень проблемы лежал не в алгоритме записи, а в чем-то другом.
Как выяснилось в ходе обсуждения, проблема заключалась в исходных данных. В табличной части объект.ДанныеЗаказов содержались дублирующиеся строки по ключевым полям (Контрагент + Адрес).
Давайте разберем по шагам, что происходило:
В итоге, после завершения цикла, в регистре сведений остается результат обработки последней строки с дублирующимся ключом. У разработчика создается полная иллюзия, что "замещение не происходит", хотя на самом деле оно происходит, и даже несколько раз, просто итоговый результат оказывается не тем, который ожидался.
Чтобы избежать подобных логических ошибок и заодно повысить производительность кода, следуйте этим рекомендациям.
Перед записью данных в регистр всегда убеждайтесь в уникальности ключей в источнике. Самый простой способ — выгрузить табличную часть в таблицу значений и "свернуть" ее.
Пример со сворачиванием:
// Выгружаем табличную часть во временную таблицу
ТЗ_Данные = Объект.ДанныеЗаказов.Выгрузить();
// Сворачиваем таблицу по ключевым полям (измерениям).
// Для колонок с ресурсами (Широта, Долгота) будут взяты значения из последней
// встреченной строки группы, что соответствует поведению цикла.
ТЗ_Данные.Свернуть("Контрагент, Адрес", "Широта, Долгота");
// Теперь итерируем по свернутой таблице, где нет дублей по ключу
Для Каждого СтрокаТЗ из ТЗ_Данные Цикл
НоваяЗапись = РегистрыСведений.АдресаКоординаты.СоздатьМенеджерЗаписи();
ЗаполнитьЗначенияСвойств(НоваяЗапись, СтрокаТЗ);
НоваяЗапись.Записать();
КонецЦикла;
Если нужно, например, взять не последние, а максимальные значения, лучше использовать запрос с группировкой и агрегатными функциями (МАКСИМУМ()).
Использование менеджера записи (СоздатьМенеджерЗаписи()) в цикле — неэффективно для больших объемов данных. Каждый вызов Записать() — это отдельное обращение к базе данных. Для массовых операций предназначен объект НаборЗаписей.
Пример с набором записей:
// Сначала подготавливаем уникальные данные
ТЗ_Данные = Объект.ДанныеЗаказов.Выгрузить();
ТЗ_Данные.Свернуть("Контрагент, Адрес", "Широта, Долгота");
// Создаем пустой набор записей
НаборЗаписей = РегистрыСведений.АдресаКоординаты.СоздатьНаборЗаписей();
// Загружаем в него всю нашу подготовленную таблицу
НаборЗаписей.Загрузить(ТЗ_Данные);
// Записываем весь набор одной транзакцией в базу данных.
// Параметр Истина означает, что существующие записи с такими ключами будут замещены.
НаборЗаписей.Записать(Истина);
Этот подход работает в разы быстрее, так как выполняет только одну операцию записи в базу данных для всего пакета данных, а не по одной записи за раз.
В ходе отладки автор также пробовал удалять записи через менеджер, и это не работало. Метод Удалить() требует, чтобы в менеджере записи были корректно заполнены все измерения, составляющие ключ записи. Если хотя бы одно измерение не заполнено или заполнено неверно, система просто не найдет запись для удаления и ничего не сделает, при этом не вызвав ошибки. Это важное отличие от метода Записать(), который может создать запись, даже если она не существовала.