При работе с регистрами сведений в 1С программисты часто сталкиваются с ситуацией, когда попытка записи набора данных прерывается ошибкой уникальности: «Запись с такими ключевыми полями существует!». Особенно остро эта проблема проявляется при первичном заполнении регистра из внешних источников или других объектов системы, когда данных много (сотни тысяч или миллионы строк) и они не нормализованы. Разберем подробно, почему возникает эта ошибка, как ее диагностировать и какие методы программной записи наиболее эффективны.
В системе 1С:Предприятие независимый регистр сведений обеспечивает уникальность записей по совокупности своих измерений (и периода, если регистр периодический). Ошибка возникает в тот момент, когда метод Записать() пытается зафиксировать в базе данных строку, значения ключевых полей которой (измерений) полностью совпадают с уже имеющейся записью в базе или с другой строкой в текущем записываемом наборе.
Рассмотрим ситуацию, описанную в теме: автор пытается записать данные в регистр с измерениями ЛицевойСчет и Телефон. Даже если визуально в исходной таблице значения кажутся разными, платформа может считать их идентичными. Выясним основные причины такого поведения:
Чтобы избежать ошибок при массовом заполнении, проанализируем алгоритм предварительной очистки данных. Рассмотрим пример кода, который нормализует номера телефонов и устраняет потенциальные дубли:
// Предположим, у нас есть ТаблицаЗначений "СписокТелефонов" с колонками ЛицевойСчет и Телефон
// 1. Очистка и нормализация данных в цикле
Для Каждого Стр Из СписокТелефонов Цикл
// Удаляем лишние пробелы по краям
Стр.Телефон = СокрЛП(Стр.Телефон);
// Если ЛицевойСчет - это ссылка, проверяем на пустое значение
Если Стр.ЛицевойСчет.Пустая() Тогда
// Обработка пустых значений, которые тоже могут создать дубли
КонецЕсли;
КонецЦикла;
// 2. Сворачивание таблицы по ключевым полям (измерениям)
// Это гарантирует уникальность комбинации ЛицевойСчет + Телефон внутри самой таблицы
СписокТелефонов.Свернуть("ЛицевойСчет, Телефон", "СуммаРесурса");
Метод Свернуть() крайне важен. Если в вашем регистре есть ресурсы, их нужно указать во втором параметре метода, чтобы система просуммировала их (если это числа) или оставила первое встретившееся значение.
Для массовой записи данных в независимый регистр сведений использование цикла с созданием менеджера записи на каждую строку является крайне непроизводительным. Правильнее использовать объект НаборЗаписей. Рассмотрим оптимальный способ записи:
Процедура ЗаполнитьРегистрТелефонов(ТЗДанных)
// Создаем объект набора записей
НаборЗаписей = РегистрыСведений.Модус_ТелефоныАбонентов.СоздатьНаборЗаписей();
// Если нужно полностью перезаписать регистр, фильтры не устанавливаем.
// Метод Загрузить() переносит данные из ТЗ в память набора
НаборЗаписей.Загрузить(ТЗДанных);
// Записываем набор в базу данных.
// Параметр Истина означает замещение существующих записей в рамках установленного фильтра
Попытка
НаборЗаписей.Записать(Истина);
Сообщить("Данные успешно записаны.");
Исключение
Инфо = ИнформацияОбОшибке();
Сообщить("Ошибка при записи: " + Инфо.Описание);
КонецПопытки;
КонецПроцедуры
Важный момент, который часто упускают: метод НаборЗаписей.Загрузить(ТЗДанных) требует, чтобы имена колонок в таблице значений в точности совпадали с именами измерений и ресурсов регистра. Если в ТЗ есть лишние колонки, они будут проигнорированы, но все обязательные поля должны присутствовать.
В обсуждении упоминалось, что объем данных составляет около 1,5 миллиона записей. Попытка записать такой объем одним набором может привести к критическому потреблению оперативной памяти или переполнению лога транзакций SQL-сервера. Рассмотрим, как проанализировать ситуацию и применить порционную запись.
Разделим процесс на порции (транзакции) по 10 000 — 50 000 записей. Это позволит системе «дышать» и не блокировать таблицу регистра на длительное время.
// Пример порционной записи данных
РазмерПорции = 10000;
ВсегоСтрок = ТЗДанных.Количество();
ТекущаяСтрока = 0;
Пока ТекущаяСтрока < ВсегоСтрок Цикл
Набор = РегистрыСведений.Модус_ТелефоныАбонентов.СоздатьНаборЗаписей();
// Наполняем набор только частью данных
Для Счётчик = 1 По РазмерПорции Цикл
Если ТекущаяСтрока >= ВсегоСтрок Тогда
Прервать;
КонецЕсли;
СтрокаТЗ = ТЗДанных[ТекущаяСтрока];
НоваяЗапись = Набор.Добавить();
ЗаполнитьЗначенияСвойств(НоваяЗапись, СтрокаТЗ);
ТекущаяСтрока = ТекущаяСтрока + 1;
КонецЦикла;
// Записываем порцию с параметром Ложь, чтобы не затирать весь регистр,
// а добавлять новые записи (если регистр был пуст или мы уверены в отсутствии дублей)
Набор.Записать(Ложь);
КонецЦикла;
Как выяснилось в ходе обсуждения, наиболее коварная причина ошибки «Запись с такими ключевыми полями существует» кроется в настройках метаданных. Если измерение Телефон имеет длину 20 символов, а вы пытаетесь записать туда список телефонов через запятую (например, «+79001112233, +79004445566»), 1С обрежет строку до 20 знаков.
Проанализируем пример:
Строка 1: «+79001112233, +79000000001» -> Обрезка до 20: «+79001112233, +79000»
Строка 2: «+79001112233, +79000000002» -> Обрезка до 20: «+79001112233, +79000»
В результате в базу данных пытаются записаться две абсолютно одинаковые строки с точки зрения ключа. Решение: всегда проверяйте соответствие максимальной длины входящих данных и длины, установленной в свойствах измерения регистра. Для хранения нормализованных телефонов достаточно 15-20 символов, но данные должны быть предварительно очищены от «хвостов» и перечислений.
Если ошибка сохраняется, не пренебрегайте использованием отладчика. В момент возникновения исключения перед вызовом Записать() можно проверить содержимое набора. Для этого в табло или в окне вычисления выражений введите НаборЗаписей.Выгрузить(). Полученную таблицу можно сохранить в файл и с помощью стандартных средств поиска дублей (например, в Excel) быстро найти проблемные строки, которые вызывают конфликт уникальности.
Подводя итог, можно сказать, что стабильная запись в регистры сведений требует строгого соблюдения уникальности ключевых полей, предварительной нормализации строковых данных (использование СокрЛП и контроля длины) и грамотного выбора способа записи (через объекты НаборЗаписей).