Как избежать дублирования строк в табличной части и синхронизировать её с регистром сведений в 1С?

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

При разработке конфигураций на платформе 1С:Предприятие 8 программисты часто сталкиваются с классической архитектурной задачей: данные из табличной части (ТЧ) справочника или документа должны транслироваться в регистр сведений (РС). На первый взгляд задача кажется тривиальной, но при детальной реализации всплывает ряд серьезных проблем.

Основная сложность заключается в фундаментальном различии хранения данных. Табличная часть справочника — это «плоская» таблица, которая по умолчанию позволяет вводить неограниченное количество абсолютно одинаковых строк. Регистр сведений, напротив, имеет строгую структуру. Уникальность его записей контролируется платформой по комбинации измерений. Если пользователь введет в табличную часть две строки с одинаковыми ключевыми параметрами, при стандартной построчной записи в регистр попадет только последнее значение. Более того, если пользователь просто удалит строку из табличной части, в независимом регистре сведений эта запись останется «висеть» навсегда, превращаясь в мусор.

В этой статье мы подробно разберем четыре практических метода борьбы с этой проблемой: от быстрой валидации на клиенте до изменения архитектуры работы с данными на управляемой форме.

Метод 1. Предотвращение ввода дублей на клиенте (в процессе редактирования)

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

Проанализируем ситуацию: в момент окончания редактирования строки нам нужно проверить всю остальную таблицу на наличие дублирующихся значений по ключевым реквизитам. Выясним причину возможной ошибки — если мы будем искать дубли через метод НайтиСтроки(), нам нужно убедиться, что система не считает дублем саму редактируемую строку.

Посмотрим на пример реализации данного обработчика на клиенте:


&НаКлиенте
Процедура ТаблицаРесурсыИзделияПередОкончаниемРедактирования(Элемент, НоваяСтрока, ОтменаРедактирования, Отказ)
    
    Если ОтменаРедактирования Тогда
        Возврат;
    КонецЕсли;
    
    ТекущиеДанныеСтроки = Элемент.ТекущиеДанные;
    Если ТекущиеДанныеСтроки = Неопределено Тогда
        Возврат;
    КонецЕсли;
    
    // Формируем структуру поиска по ключевым полям, которые в РС являются измерениями
    ПараметрыПоиска = Новый Структура;
    ПараметрыПоиска.Вставить("РесурсИзделия", ТекущиеДанныеСтроки.РесурсИзделия);
    
    // Осуществляем поиск совпадений в нашей табличной части на форме
    НайденныеСтроки = Объект.ТаблицаРесурсыИзделия.НайтиСтроки(ПараметрыПоиска);
    
    // Если найдено больше одной строки, значит, пользователь пытается ввести дубликат
    Если НайденныеСтроки.Количество() > 1 Тогда
        Отказ = Истина;
        ОбщегоНазначенияКлиентСервер.СообщитьПользователю(
            "Внимание! Данный ресурс изделия уже добавлен в спецификацию. Дублирование недопустимо.",
            ,
            "Объект.ТаблицаРесурсыИзделия[" + Объект.ТаблицаРесурсыИзделия.Индекс(ТекущиеДанныеСтроки) + "].РесурсИзделия"
        );
    КонецЕсли;
    
КонецПроцедуры

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

Метод 2. Сворачивание табличной части перед записью на сервере

Если бизнес-логика допускает автоматическое исправление ошибок пользователя без вывода предупреждений, мы можем принудительно очистить таблицу от дублирующих строк непосредственно перед сохранением объекта в базу данных. Для этого используется метод таблицы значений или табличной части Свернуть().

Рассмотрим подробнее этот подход. Метод Свернуть() принимает два параметра: список группировочных колонок (измерения нашего регистра) и список суммируемых колонок (ресурсы). При сворачивании строк дублирующиеся записи объединяются, а числовые показатели складываются.

Выполнить эту операцию необходимо в модуле объекта справочника или документа в предопределенном обработчике ПередЗаписью:


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

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

Метод 3. Проверка ТЧ на дубли запросом (классическая валидация)

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

Разберем по шагам, как это реализовать:

  1. Выгружаем табличную часть объекта во временную таблицу внутри запроса.
  2. Группируем строки по измерениям будущего регистра сведений.
  3. Используем условие ИМЕЮЩИЕ КОЛИЧЕСТВО(РесурсИзделия) > 1, чтобы отобрать только дублирующиеся строки.
  4. Если такие строки найдены, прерываем транзакцию записи с выводом подробного сообщения об ошибке.

Посмотрим на пример серверного кода в модуле объекта:


Процедура ПередЗаписью(Отказ)
    
    Если ЭтотОбъект.ОбменДанными.Загрузка Тогда
        Возврат;
    КонецЕсли;
    
    // Проверим наличие строк в ТЧ, чтобы зря не запускать запрос
    Если ЭтотОбъект.ТаблицаРесурсыИзделия.Количество() = 0 Тогда
        Возврат;
    КонецЕсли;
    
    // Подготавливаем запрос для проверки дублей
    Запрос = Новый Запрос;
    Запрос.УстановитьПараметр("ТаблицаТЧ", ЭтотОбъект.ТаблицаРесурсыИзделия.Выгрузить());
    
    Запрос.Текст = 
        "ВЫБРАТЬ
        |   ВременнаяТаблица.НомерСтроки КАК НомерСтроки,
        |   ВременнаяТаблица.РесурсИзделия КАК РесурсИзделия
        |ПОМЕСТИТЬ ВТ_Данные
        |ИЗ
        |   &ТаблицаТЧ КАК ВременнаяТаблица
        |;
        |
        |////////////////////////////////////////////////////////////////////////////////
        |ВЫБРАТЬ
        |   ВТ_Данные.РесурсИзделия КАК РесурсИзделия,
        |   КОЛИЧЕСТВО(ВТ_Данные.НомерСтроки) КАК КоличествоДублей
        |ИЗ
        |   ВТ_Данные КАК ВТ_Данные
        |
        |СГРУППИРОВАТЬ ПО
        |   ВТ_Данные.РесурсИзделия
        |
        |ИМЕЮЩИЕ
        |   КОЛИЧЕСТВО(ВТ_Данные.НомерСтроки) > 1";
        
    РезультатЗапроса = Запрос.Выполнить();
    Если НЕ РезультатЗапроса.Пустой() Тогда
        
        Отказ = Истина;
        Выборка = РезультатЗапроса.Выбрать();
        Пока Выборка.Следующий() Цикл
            Сообщение = Новый СообщениеПользователю;
            Сообщение.Текст = "Запись невозможна! Обнаружено дублирование ресурса: """ 
                + Выборка.РесурсИзделия + """ в табличной части справочника.";
            Сообщение.Сообщить();
        КонецЦикла;
        
    КонецЕсли;
    
КонецПроцедуры

Метод 4. Полная перезапись регистра через Набор Записей

Одной из главных проблем, озвученных на форуме, является рассинхронизация при удалении строк: пользователь удалил строку из табличной части, нажал «Записать», но запись в регистре осталась. Это происходит из-за некорректного алгоритма записи в РС (например, построчного добавления новых записей без очистки старых).

Для гарантированной синхронизации табличной части и регистра сведений необходимо использовать объект типа РегистрСведенийНаборЗаписей с установленным отбором по ведущему измерению (в нашем случае — по ссылке на элемент справочника). При записи набора старые данные по этому измерению полностью заменяются новыми.

Проанализируем пример кода в обработчике ПриЗаписи модуля объекта справочника:


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

Этот подход элегантно решает проблему синхронизации: если табличная часть пуста, регистр для данной ссылки будет очищен; если строки изменились или удалились — регистр мгновенно примет актуальный вид.

Рекомендация эксперта: Отказ от ТЧ в пользу Набора Записей на форме

Рассмотренные выше методы решают проблему технически, но с точки зрения архитектуры 1С:Предприятия 8 хранение дублирующих данных в табличной части справочника и регистре сведений является избыточным (денормализация данных). Это увеличивает размер базы данных и создает риск рассинхронизации.

Наиболее профессиональным и изящным решением является полный отказ от табличной части справочника. Вместо этого на управляемую форму элемента выводится непосредственно набор записей регистра сведений.

Для реализации этого паттерна выполните следующие шаги:

  1. Удалите табличную часть из метаданных справочника.
  2. На форме элемента справочника добавьте новый реквизит с типом РегистрСведенийНаборЗаписей.Ресурсы (назовите его, например, РесурсыНабор). В свойствах реквизита укажите связь по типу с измерением Изделие.
  3. Перетащите этот реквизит на форму в виде таблицы. Платформа автоматически сформирует интерфейс для редактирования регистра сведений.
  4. В модуле формы настройте чтение и запись набора записей в стандартных обработчиках событий:

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

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

При таком подходе пользователь работает напрямую с регистром через интерфейс формы справочника. Все проверки на уникальность записей (по измерениям регистра) платформа выполнит автоматически на уровне СУБД, а вам не придется писать сложный код для поиска дублей и удаления «лишних» строк. Это экономит ресурсы сервера и гарантирует абсолютную чистоту данных в системе.

← На главную