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