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

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

При разработке в среде 1С:Предприятие 8.3 программисты часто сталкиваются с задачей реализации логики «переключателя» (или радиокнопки) в табличной части или таблице значений на форме. Типичный пример: в списке ресурсов изделия пользователь должен выбрать только один «основной» ресурс, установив соответствующий флажок в колонке с типом Булево. При этом у всех остальных строк флаг должен автоматически сниматься, а измененные данные должны корректно записываться в регистр сведений.

В этой статье мы подробно разберем, почему возникает популярная ошибка «Запись с такими полями уже существует» при работе с регистрами сведений, проанализируем критические архитектурные ошибки в коде начинающих разработчиков и напишем оптимальный, производительный алгоритм решения этой задачи.

Анализ исходных ошибок в коде

Рассмотрим код, который часто пишут новички, и выясним причины его неработоспособности. Типичный сценарий выглядит так: в событии изменения флажка запускается цикл по строкам таблицы, внутри которого для каждой строки вызывается серверная процедура для записи в регистр сведений. Посмотрим на этот код подробнее:


Процедура РесурсыИзделияПризнакОсновногоРесурсаПриИзменении(Элемент)
    ТекущиеДанные = Элементы.ТаблицаРесурсыИзделия.ТекущиеДанные;
    Если ТекущиеДанные = Неопределено Тогда
        Возврат;
    КонецЕсли;
    
    ПризнакОсновногоРесурса = ТекущиеДанные.ПризнакОсновногоРесурса;
    Для Каждого СтрокаТЧ Из ТаблицаРесурсыИзделия Цикл
        СтрокаТЧ.ПризнакОсновногоРесурса = Ложь;
        СтруктураЗаписи = Новый Структура("Изделие,РесурсыИзделия,ЕдиницыИзмерения,СпособыХранения,СрокСлужбы,СрокХранения,УстановленныйРесурс,ПризнакОсновногоРесурса,Примечание");
        СтруктураЗаписи.Вставить("Изделие", Объект.Ссылка);
        ЗаполнитьЗначенияСвойств(СтруктураЗаписи, ТекущиеДанные);
        РесурсыИзделияПризнакОсновногоРесурсаПриИзмененииНаСервере(СтруктураЗаписи);
    КонецЦикла;
    ТекущиеДанные.ПризнакОсновногоРесурса = ПризнакОсновногоРесурса;
КонецПроцедуры

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

  1. Вызов сервера в цикле клиентского обхода. Это серьезный антипаттерн разработки под управляемые формы. Каждый проход цикла Для Каждого СтрокаТЧ Из ТаблицаРесурсыИзделия инициирует полноценный сетевой вызов процедуры РесурсыИзделияПризнакОсновногоРесурсаПриИзмененииНаСервере. Если в таблице будет 100 строк, система совершит 100 обращений к серверу. Это приведет к колоссальным задержкам интерфейса.
  2. Неправильный обход структуры на сервере. Посмотрим на серверную часть кода:
    
    Для Каждого Строка Из СтруктураЗаписи Цикл
        СтрокаРС = РС.Добавить();
        СтрокаРС.Изделие = СтруктураЗаписи.Изделие;
        // ... заполнение остальных полей
    КонецЦикла;
    
    Объект типа Структура представляет собой коллекцию пар «Ключ-Значение». Итератор Для Каждого по структуре возвращает элементы этой структуры, а не строки таблицы. Попытка обращаться внутри такого цикла к полям как к строкам таблицы значений бессмысленна и приводит к логическим сбоям.
  3. Некорректная очистка и перезапись регистра сведений. На сервере каждый раз создается новый набор записей, устанавливается отбор, вызывается метод Прочитать(), затем набор очищается и записывается. Из-за того, что процедура вызывается в цикле для каждой строки отдельно, при каждой итерации данные предыдущей строки просто затираются. В итоге в регистре останется только результат обработки последней строки.
  4. Ошибка уникальности записей. Ошибка «Запись с такими полями уже существует» возникает, когда в регистр сведений пытаются записать две строки, у которых совпадают ключевые поля (измерения). Если в коде не очистить предварительно старые записи по ключевым измерениям или передать в набор дублирующие строки, платформа заблокирует транзакцию записи.

Шаг 1. Реализация логики «Переключателя» на клиенте

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

Проанализируем оптимальный алгоритм работы клиентской процедуры:


&НаКлиенте
Процедура РесурсыИзделияПризнакОсновногоРесурсаПриИзменении(Элемент)
    
    ТекущиеДанные = Элементы.ТаблицаРесурсыИзделия.ТекущиеДанные;
    Если ТекущиеДанные = Неопределено Тогда
        Возврат;
    КонецЕсли;
    
    // Если пользователь установил флаг "Истина", сбрасываем его у всех остальных строк
    Если ТекущиеДанные.ПризнакОсновногоРесурса Тогда
        
        Для Каждого СтрокаТЧ Из Объект.ТаблицаРесурсыИзделия Цикл
            // Сравниваем по идентификатору строки, чтобы не сбросить флаг у только что измененной строки
            Если СтрокаТЧ.ПолучитьИдентификатор() <> ТекущиеДанные.ПолучитьИдентификатор() Тогда
                СтрокаТЧ.ПризнакОсновногоРесурса = Ложь;
            КонецЕсли;
        КонецЦикла;
        
    КонецЕсли;
    
    // Подготовим данные для отправки на сервер ОДНИМ вызовом
    ЗаписатьДанныеТаблицыВРегистр();
    
КонецПроцедуры

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

Шаг 2. Подготовка данных и оптимизация серверного вызова

Поскольку мы хотим использовать быстрый контекстный или внеконтекстный серверный вызов, нам нужно передать данные всей таблицы на сервер за один раз. Передавать напрямую элементы формы на сервер без контекста нельзя. Самый производительный и правильный способ — выгрузить данные таблицы в массив структур на клиенте и передать этот массив серверной процедуре.

Разберем, как подготовить данные на клиенте:


&НаКлиенте
Процедура ЗаписатьДанныеТаблицыВРегистр()
    
    МассивСтрок = Новый Массив;
    
    // Собираем данные всех строк таблицы в массив простых структур
    Для Каждого СтрокаТЧ Из Объект.ТаблицаРесурсыИзделия Цикл
        
        СтруктураСтроки = Новый Структура;
        СтруктураСтроки.Вставить("РесурсыИзделия",          СтрокаТЧ.РесурсыИзделия);
        СтруктураСтроки.Вставить("ЕдиницыИзмерения",        СтрокаТЧ.ЕдиницыИзмерения);
        СтруктураСтроки.Вставить("СпособыХранения",        СтрокаТЧ.СпособыХранения);
        СтруктураСтроки.Вставить("СрокСлужбы",              СтрокаТЧ.СрокСлужбы);
        СтруктураСтроки.Вставить("СрокХранения",            СтрокаТЧ.СрокХранения);
        СтруктураСтроки.Вставить("УстановленныйРесурс",     СтрокаТЧ.УстановленныйРесурс);
        СтруктураСтроки.Вставить("ПризнакОсновногоРесурса", СтрокаТЧ.ПризнакОсновногоРесурса);
        СтруктураСтроки.Вставить("Примечание",              СтрокаТЧ.Примечание);
        
        МассивСтрок.Добавить(СтруктураСтроки);
        
    КонецЦикла;
    
    // Передаем ссылку на владельца (Изделие) и подготовленный массив данных на сервер
    ЗаписатьРесурсыНаСервере(Объект.Ссылка, МассивСтрок);
    
КонецПроцедуры

Шаг 3. Безопасная запись данных в Регистр Сведений

Теперь рассмотрим работу серверного метода. Для записи данных мы будем использовать объект РегистрСведенийНаборЗаписей. Это наиболее безопасный способ работы с периодическими или непериодическими регистрами сведений, подчиненными какому-либо измерению.

Суть механизма набора записей заключается в том, что при установке отбора по ключевому измерению (в нашем случае это измерение Изделие), последующая запись набора автоматически удаляет из базы данных все старые записи, соответствующие этому отбору, и записывает новые. Это полностью исключает появление дублей и появление ошибки неуникальности по данному измерению.

Посмотрим на программную реализацию серверной процедуры:


&НаСервереБезКонтекста
Процедура ЗаписатьРесурсыНаСервере(СсылкаНаИзделие, МассивСтрок)
    
    // Инициализируем набор записей нужного регистра сведений
    НаборЗаписей = РегистрыСведений.Ресурсы.СоздатьНаборЗаписей();
    
    // Важно: Устанавливаем жесткий отбор по измерению "Изделие"
    НаборЗаписей.Отбор.Изделие.Установить(СсылкаНаИзделие);
    
    // Заполняем набор новыми строками из переданного массива
    Для Каждого СтруктураСтроки Из МассивСтрок Цикл
        
        // Добавляем пустую строку в набор записей
        НоваяЗапись = НаборЗаписей.Добавить();
        
        // Заполняем ведущее измерение, по которому установлен отбор
        НоваяЗапись.Изделие = СсылкаНаИзделие;
        
        // Заполняем остальные реквизиты и измерения
        ЗаполнитьЗначенияСвойств(НоваяЗапись, СтруктураСтроки);
        
    КонецЦикла;
    
    // Записываем набор данных. Метод Записать(Истина) производит замещение старых данных
    Попытка
        НаборЗаписей.Записать(Истина);
    Исключение
        // Логируем ошибку и выводим пользователю понятное сообщение
        ТекстОшибки = "Не удалось сохранить ресурсы изделия в регистр. Подробности: " + КраткоеПредставлениеОшибки(ИнформацияОбОшибке());
        ЗаписатьЛогОшибки(ТекстОшибки);
        ВызватьИсключение ТекстОшибки;
    КонецПопытки;
    
КонецПроцедуры

Как гарантированно избежать ошибки дублирования записей

Если вы применили вышеописанный код, но ошибка «Запись с такими полями уже существует» все равно появляется при выполнении метода НаборЗаписей.Записать(Истина), это означает одну из двух проблем в вашей метаструктуре данных:

Для предотвращения первой проблемы (дубликатов внутри передаваемой таблицы) мы можем перед отправкой данных на сервер выполнить быструю проверку на уникальность ключевых полей с помощью объекта ТаблицаЗначений и метода Свернуть():


&НаСервереБезКонтекста
Функция ТаблицаСодержитДубликаты(МассивСтрок)
    
    // Создаем временную таблицу значений для проверки уникальности
    ТЗ = Новый ТаблицаЗначений;
    ТЗ.Колонки.Добавить("РесурсыИзделия");
    
    Для Каждого СтруктураСтроки Из МассивСтрок Цикл
        НоваяСтрока = ТЗ.Добавить();
        НоваяСтрока.РесурсыИзделия = СтруктураСтроки.РесурсыИзделия;
    КонецЦикла;
    
    КоличествоДоСворачивания = ТЗ.Количество();
    
    // Сворачиваем таблицу по ключевому измерению
    ТЗ.Свернуть("РесурсыИзделия");
    
    КоличествоПослеСворачивания = ТЗ.Количество();
    
    // Если количество строк уменьшилось, значит были дубликаты
    Возврат КоличествоДоСворачивания <> КоличествоПослеСворачивания;
    
КонецФункции

Архитектурная рекомендация: перенос записи на транзакционный уровень

Запись в регистр сведений непосредственно в момент интерактивного изменения флажка пользователем на форме (в событии ПриИзменении) — это спорное архитектурное решение. Если пользователь случайно нажмет кнопку «Закрыть без сохранения» на форме элемента справочника или документа, то данные в справочнике останутся старыми, а изменения в регистре сведений уже применятся к базе данных. Это приведет к рассинхронизации данных.

Правильный подход:

  1. При изменении флажка на форме мы изменяем только данные в оперативной памяти (в коллекции Объект.ТаблицаРесурсыИзделия) на клиенте. Никаких мгновенных серверных вызовов записи в регистр делать не нужно.
  2. Запись в регистр сведений переносится в модуль объекта справочника или документа (в обработчики событий ПередЗаписью или ПриЗаписи).

Реализуем этот надежный паттерн. В модуле формы оставляем только клиентскую часть интерактивного изменения переключателя:


&НаКлиенте
Процедура РесурсыИзделияПризнакОсновногоРесурсаПриИзменении(Элемент)
    
    ТекущиеДанные = Элементы.ТаблицаРесурсыИзделия.ТекущиеДанные;
    Если ТекущиеДанные = Неопределено Тогда
        Возврат;
    КонецЕсли;
    
    Если ТекущиеДанные.ПризнакОсновногоРесурса Тогда
        Для Каждого СтрокаТЧ Из Объект.ТаблицаРесурсыИзделия Цикл
            Если СтрокаТЧ.ПолучитьИдентификатор() <> ТекущиеДанные.ПолучитьИдентификатор() Тогда
                СтрокаТЧ.ПризнакОсновногоРесурса = Ложь;
            КонецЕсли;
        КонецЦикла;
    КонецЕсли;
    
    // Форма помечается как измененная, кнопка "Записать" становится активной
    ЭтаФорма.Модифицированность = Истина;
    
КонецПроцедуры

Затем в модуле объекта (например, справочника ЭФО) определяем процедуру ПриЗаписи, которая будет автоматически транслировать данные табличной части в регистр сведений в рамках единой транзакции базы данных:


Процедура ПриЗаписи(Отказ)
    
    // Блокируем запись регистра в рамках общей транзакции
    НаборЗаписей = РегистрыСведений.Ресурсы.СоздатьНаборЗаписей();
    НаборЗаписей.Отбор.Изделие.Установить(Ссылка);
    
    Для Каждого СтрокаТЧ Из ТаблицаРесурсыИзделия Цикл
        
        НоваяЗапись = НаборЗаписей.Добавить();
        НоваяЗапись.Изделие = Ссылка;
        ЗаполнитьЗначенияСвойств(НоваяЗапись, СтрокаТЧ);
        
    КонецЦикла;
    
    Попытка
        // Записываем набор в той же транзакции, что и сам объект
        НаборЗаписей.Записать(Истина);
    Исключение
        Отказ = Истина;
        ЗаписатьЗаписьВЖурналРегистрации("ОшибкиЗаписиРесурсов", УровеньЖурналаРегистрации.Ошибка, , , ПодробноеПредставлениеОшибки(ИнформацияОбОшибке()));
        ВызыватьИсключение "Не удалось сохранить связанные ресурсы изделия. Обратитесь к администратору.";
    КонецПопытки;
    
КонецПроцедуры

Такой подход гарантирует целостность базы данных: если запись самого объекта сорвется по какой-то причине, изменения в связанном регистре сведений также автоматически откатятся назад.

← На главную