При разработке в среде 1С:Предприятие часто возникает задача, когда данные из табличной части справочника должны дублироваться или транслироваться в независимый регистр сведений. С этим сталкиваются программисты при проектировании сложных систем, где регистры используются для быстрого поиска, среза последних или интеграции с другими подсистемами.
Однако при простейшей реализации записи «из ТЧ в регистр» разработчики наступают на классические грабли: если пользователь удаляет строку из табличной части справочника или изменяет её состав, в регистре сведений эти данные остаются нетронутыми. Происходит опасная рассинхронизация базы данных. Выясним причину этой проблемы и разберем по шагам, как решить её правильно и красиво.
Рассмотрим стандартный жизненный цикл объекта справочника. Когда пользователь нажимает кнопку «Записать», платформа считывает данные из экранной формы, помещает их в оперативную память в виде объекта справочника и производит запись в СУБД. Если в коде прописано простое добавление новых записей в регистр сведений, система послушно добавит их.
Но СУБД не знает, какие строки были в табличной части до её изменения на форме. Если пользователь удалил третью строку из ТЧ, объект справочника запишется в базу уже без неё. При этом в регистре сведений запись, соответствующая этой третьей строке, так и останется лежать, поскольку команда на её удаление из регистра не передавалась. Наша задача — сделать так, чтобы состав записей в регистре всегда в точности соответствовал текущему составу табличной части справочника.
Разберем основные методы решения этой проблемы: от классического программного способа до современного архитектурного подхода.
Самый простой, надежный и быстрый способ синхронизации — это полная перезапись набора записей регистра сведений по текущему элементу справочника (владельцу). Вместо того чтобы писать сложные алгоритмы сравнения строк, запросы с полным соединением или ручной поиск удаленных позиций, мы воспользуемся встроенным механизмом платформы 1С — объектом РегистрСведенийНаборЗаписей.
Идея метода заключается в следующем: при каждой записи справочника мы полностью очищаем в регистре сведений все записи, относящиеся к данному элементу (по отбору), и записываем туда актуальное состояние табличной части «с чистого листа». Всё это происходит в рамках одной транзакции базы данных.
Реализуем этот алгоритм в модуле объекта нашего справочника. Перейдем в модуль объекта и создадим обработчик события ПередЗаписью. Нам потребуется выгрузить табличную часть справочника в таблицу значений, подготовить набор записей регистра, установить жесткий отбор и загрузить новые данные.
Рассмотрим подробнее программный код, реализующий эту логику:
Процедура ПередЗаписью(Отказ, РежимЗаписи, РежимПроведения)
// Предотвращаем выполнение кода при обмене данными, если это необходимо
Если ОбменДанными.Загрузка Тогда
Возврат;
КонецЕсли;
// Если это новый элемент справочника, у него еще нет ссылки в БД.
// Набор записей мы сможем записать только после того, как ссылка будет физически сформирована.
// Поэтому проверку наличия ссылки и саму запись лучше делать в обработчике ПриЗаписи,
// либо получить ссылку заранее с помощью метода ПолучитьСсылкуНового().
// Для надежности перенесем основную логику в обработчик ПриЗаписи.
КонецПроцедуры
Процедура ПриЗаписи(Отказ)
Если ОбменДанными.Загрузка Тогда
Возврат;
КонецЕсли;
// Разберем по шагам создание и заполнение набора записей регистра сведений
// Шаг 1. Создаем объект Набор Записей для нашего регистра
НаборЗаписейРесурсов = РегистрыСведений.Ресурсы.СоздатьНаборЗаписей();
// Шаг 2. Устанавливаем отбор по ключевому измерению (ссылке на наш справочник)
// Это критически важно: отбор гарантирует, что мы затрем записи только по текущему элементу!
НаборЗаписейРесурсов.Отбор.Изделие.Установить(Ссылка);
// Шаг 3. Подготавливаем данные для записи. Выгружаем ТЧ справочника в Таблицу Значений.
// Имена колонок в табличной части должны совпадать с именами ресурсов и измерений регистра.
ТаблицаДанных = ТаблицаРесурсыИзделия.Выгрузить();
// Шаг 4. В выгруженной таблице значений ТЧ нет колонки связи "Изделие" (поскольку это реквизит владельца).
// Нам необходимо добавить эту колонку в таблицу значений и заполнить ее ссылкой на текущий справочник.
ТаблицаДанных.Колонки.Добавить("Изделие");
ТаблицаДанных.ЗаполнитьЗначения(Ссылка, "Изделие");
// Шаг 5. Загружаем подготовленную таблицу значений в набор записей.
// Метод Загрузить() очищает все строки, которые сейчас находятся в оперативной памяти набора,
// и заполняет его строками из нашей таблицы значений.
НаборЗаписейРесурсов.Загрузить(ТаблицаДанных);
// Шаг 6. Производим запись набора в базу данных.
// Параметр "Замещать = Истина" (используется по умолчанию) указывает платформе,
// что все старые записи в БД по установленному отбору (Изделие = Ссылка) будут безвозвратно удалены,
// а на их место запишутся строки, которые мы только что загрузили из ТЧ.
Попытка
НаборЗаписейРесурсов.Записать(Истина);
Исключение
// В случае непредвиденных ошибок отменяем транзакцию записи справочника
Отказ = Истина;
ВызватьИсключение "Не удалось записать связанные данные в регистр сведений! Ошибка: " + ОписаниеОшибки();
КонецПопытки;
КонецПроцедуры
Посмотрим на преимущества этого решения. Во-первых, оно чрезвычайно производительно. Платформа 1С оптимизирует работу с СУБД при записи наборов, минимизируя количество SQL-запросов. Во-вторых, код лаконичен и легко читаем. Нам не нужно заботиться о ручном поиске удаленных строк — метод Записать(Истина) делает это автоматически «под капотом» платформы.
Проанализируем ситуацию с точки зрения архитектуры базы данных. Зачем хранить одни и те же данные в двух местах одновременно (в табличной части справочника и в независимом регистре сведений)? Это классический антипаттерн «избыточность данных».
Дублирование данных ведет к неоправданному росту размера базы данных, усложняет её поддержку и повышает риски возникновения логических ошибок при чтении информации сторонними отчетами. Если вам нужен регистр сведений для аналитики, правильнее будет полностью удалить табличную часть из справочника, а на форме элемента справочника организовать прямую работу с записями регистра.
Разберем по шагам, как реализовать этот современный подход на управляемых формах:
ТаблицаРесурсыИзделия из метаданных справочника. Данные теперь будут храниться исключительно в регистре сведений Ресурсы.РесурсыИзделияРегистр.ПроизвольныйЗапрос и напишите простой запрос к вашему регистру сведений:
ВЫБРАТЬ
Ресурсы.РесурсИзделия КАК РесурсИзделия,
Ресурсы.СрокСлужбы КАК СрокСлужбы,
Ресурсы.СрокХранения КАК СрокХранения,
Ресурсы.ЕдиницаИзмерения КАК ЕдиницаИзмерения,
Ресурсы.УстановленныйРесурс КАК УстановленныйРесурс,
Ресурсы.УсловияХранения КАК УсловияХранения,
Ресурсы.ПризнакОсновногоРесурса КАК ПризнакОсновногоРесурса,
Ресурсы.Примечание КАК Примечание
ИЗ
РегистрСведений.Ресурсы КАК Ресурсы
ГДЕ
Ресурсы.Изделие = &Изделие
ПриСозданииНаСервере передайте параметр ссылки текущего элемента справочника в динамический список:
&НаСервере
Процедура ПриСозданииНаСервере(Отказ, СтандартнаяОбработка)
// Устанавливаем параметр запроса для фильтрации данных по текущему элементу
ЭлементОтбора = СписокРесурсыИзделия.Параметры.УстановитьЗначениеПараметра("Изделие", Объект.Ссылка);
КонецПроцедуры
Ресурсы, где пользователь сможет вносить изменения.Важный нюанс: при работе с динамическим списком изменения вступают в силу сразу при записи регистра (не нужно нажимать кнопку «Записать» в самом справочнике). Это существенно упрощает интерфейс и снижает нагрузку на базу данных.
Обратите внимание на работу с новыми объектами: Если пользователь только что нажал кнопку «Создать справочник», у него еще нет физической ссылки в базе данных (значение Объект.Ссылка пустое). В таком случае добавлять записи в регистр сведений нельзя, так как поле измерения Изделие (ссылка на владельца) не может быть заполнено.
Чтобы избежать ошибок, заблокируйте таблицу динамического списка на форме, если объект является новым. Сделать это можно в обработчике ПриОткрытии на клиенте:
&НаКлиенте
Процедура ПриОткрытии(Отказ)
// Если элемент справочника еще ни разу не записывался в базу данных
Если Не ЗначениеЗаполнено(Объект.Ссылка) Тогда
// Отключаем доступность таблицы регистра сведений
Элементы.ТаблицаРесурсыИзделияРегистр.Доступность = Ложь;
// Можно вывести пользователю подсказку
ПоказатьПредупреждение(, "Ввод ресурсных показателей доступен только после записи элемента справочника!");
КонецЕсли;
КонецПроцедуры
Иногда возникают ситуации, когда табличная часть справочника содержит тысячи строк, а изменения происходят лишь в одной-двух. Полное затирание и перезапись набора записей (как в Решении №1) в этом случае может вызывать кратковременные блокировки таблиц СУБД или избыточную нагрузку на дисковую подсистему.
В таких редких сценариях имеет смысл применить алгоритм точечного сравнения. Проанализируем, как реализовать этот подход. Мы можем использовать пакетный запрос, который соединит данные из оперативной памяти (наша ТЧ) и данные из базы (текущий регистр сведений по отбору) с помощью механизма ПОЛНОЕ СОЕДИНЕНИЕ.
Давайте посмотрим на логику построения такого запроса:
// Пример логики точечного сравнения в модуле объекта справочника
Запрос = Новый Запрос;
Запрос.МенеджерВременныхТаблиц = Новый МенеджерВременныхТаблиц;
// 1. Помещаем текущую ТЧ из оперативной памяти во временную таблицу
Запрос.Текст =
"ВЫБРАТЬ * ПОМЕСТИТЬ ВТ_ТекущаяТЧ ИЗ &ТаблицаТЧ КАК ТаблицаТЧ";
Запрос.УстановитьПараметр("ТаблицаТЧ", ТаблицаРесурсыИзделия.Выгрузить());
Запрос.Выполнить();
// 2. Делаем полное соединение временной таблицы и реальной таблицы регистра
Запрос.Текст =
"ВЫБРАТЬ
| ЕСТЬNULL(ТЧ.РесурсИзделия, РС.РесурсИзделия) КАК РесурсИзделия,
| ВЫБОР
| КОГДА ТЧ.РесурсИзделия ЕСТЬ NULL ТОГДА ""Удалить""
| КОГДА РС.РесурсИзделия ЕСТЬ NULL ТОГДА ""Добавить""
| ИНАЧЕ ""Изменить""
| КОНЕЦ КАК Действие,
| ТЧ.СрокСлужбы КАК СрокСлужбыНовый,
| РС.СрокСлужбы КАК СрокСлужбыСтарый
|ИЗ
| ВТ_ТекущаяТЧ КАК ТЧ
| ПОЛНОЕ СОЕДИНЕНИЕ РегистрСведений.Ресурсы КАК РС
| ПО ТЧ.РесурсИзделия = РС.РесурсИзделия И РС.Изделие = &Ссылка
|ГДЕ
| РС.Изделие = &Ссылка ИЛИ РС.Изделие ЕСТЬ NULL";
Запрос.УстановитьПараметр("Ссылка", Ссылка);
РезультатЗапроса = Запрос.Выполнить().Выгрузить();
// 3. Обходим результат и выполняем точечную запись только измененных строк
// (Для экономии места полный обход опущен, так как для большинства стандартных задач
// этот метод является избыточно сложным по сравнению с простым использованием Набора Записей)
Подводя итог, можно сказать следующее: для 95% задач в системе 1С:Предприятие 8.3 идеальным решением является Решение №1 (перезапись набора записей в обработчике ПриЗаписи) благодаря простоте реализации и надежности транзакций СУБД. Если же вы проектируете систему с нуля и стремитесь к идеальной архитектуре без дублирования данных — смело выбирайте Решение №2 (Динамический список).