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