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