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